wallet: Keep secnonces in DescriptorScriptPubKeyMan
What changed, and why it matters
This commit changes how Bitcoin Core wallets store temporary secret signing data (MuSig2 'secnonces') for multi-signature transactions. Previously, these secrets may have been kept in a more general signing provider; now they are kept inside the wallet's descriptor key manager. The change is defensive: it ensures these one-time secrets stay only in memory and are never written to disk, which prevents a dangerous nonce-reuse bug that could leak private keys. The commit itself is a small code move/addition, not a complete fix, and the commit message does not call it a security vulnerability.
Treat this as a hardening/defensive change rather than an active vulnerability. Review the full MuSig2 signing flow to confirm secnonces are generated once per unique session id, never serialized to disk, and cleared after use. If auditing, verify that `m_musig2_secnonces` is not accidentally persisted through any serialization, logging, or crash-dump path.
Security signals we found
MuSig2 secret nonce (secnonce) lifecycle management
In-memory-only storage to prevent nonce reuse
Descriptor key manager isolation of signing material
Comment explicitly warns that secnonces must not be reused and must not be written to disk
Evidence from the diff
The patch adds an in-memory m_musig2_secnonces map to DescriptorScriptPubKeyMan and always exposes it via GetSigningProvider. MuSig2 secnonces are secret values that must be used exactly once per signing session; reuse can leak participant private keys. By holding the map in memory only (not persisted to the wallet database) and tying it to the descriptor key manager, the code reduces the risk that a stale or persisted secnonce is reused. The session id is derived from aggregate x-only pubkey + participant pubkey + sighash. The change is partial/preparatory: it establishes where secnonces live but does not by itself guarantee all callers handle them safely.
Changed components
src/wallet/scriptpubkeyman.cppsrc/wallet/scriptpubkeyman.hBitcoin Core wallet descriptor key managerMuSig2 PSBT signing pathInspect captured patch +18 / −0
diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
index 730667db..ff18265d 100644
--- a/src/wallet/scriptpubkeyman.cpp
+++ b/src/wallet/scriptpubkeyman.cpp
@@ -1256,6 +1256,10 @@ std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvid
FlatSigningProvider master_provider;
master_provider.keys = GetKeys();
m_wallet_descriptor.descriptor->ExpandPrivate(index, master_provider, *out_keys);
+
+ // Always include musig_secnonces as this descriptor may have a participant private key
+ // but not a musig() descriptor
+ out_keys->musig2_secnonces = &m_musig2_secnonces;
}
return out_keys;
diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
index c6f6e37f..ee2acdfb 100644
--- a/src/wallet/scriptpubkeyman.h
+++ b/src/wallet/scriptpubkeyman.h
@@ -10,6 +10,7 @@
#include <common/signmessage.h>
#include <common/types.h>
#include <logging.h>
+#include <musig.h>
#include <node/types.h>
#include <psbt.h>
#include <script/descriptor.h>
@@ -295,6 +296,19 @@ private:
//! Number of pre-generated keys/scripts (part of the look-ahead process, used to detect payments)
int64_t m_keypool_size GUARDED_BY(cs_desc_man){DEFAULT_KEYPOOL_SIZE};
+ /** Map of a session id to MuSig2 secnonce
+ *
+ * Stores MuSig2 secnonces while the MuSig2 signing session is still ongoing.
+ * Note that these secnonces must not be reused. In order to avoid being tricked into
+ * reusing a nonce, this map is held only in memory and must not be written to disk.
+ * The side effect is that signing sessions cannot persist across restarts, but this
+ * 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.
+ */
+ mutable std::map<uint256, MuSig2SecNonce> m_musig2_secnonces;
+
bool AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey) EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
KeyMap GetKeys() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
Why this scored 33/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.