multi: add custom nonce rand support to MuSig2 sessions
What changed, and why it matters
This change adds a hidden switch that lets developers plug in a custom random source when creating MuSig2 signing nonces, mainly so tests can produce exactly the same signatures every time. In normal operation the switch is left empty, so the code still uses the operating system's cryptographic random generator. The patch itself is a test-infrastructure feature, not a fix for an active security bug, but any future misuse of the switch could weaken signature security.
Treat as a low-risk feature commit. If reviewing for hardening, consider adding a compile-time or runtime guard (e.g., a build tag or test-only constructor) so the custom random source cannot be activated in production binaries, and document entropy requirements for any custom reader.
Security signals we found
New optional custom random source for MuSig2 nonce generation
Default call sites explicitly pass empty option, preserving CSPRNG behavior
Code comments state the option is intended only for reproducible test vectors
No validation of entropy quality from the custom reader
No access-control or feature-gating preventing main-code use of the option
Evidence from the diff
The commit threads an optional io.Reader through MusigSession, MusigSessionCfg, MusigPairSession, and a new WithCustomSigningRand channel option. When the option is present, musig2.WithCustomRand(r) is passed to musig2.GenNonces during JIT nonce generation in SignCommit. All production call sites pass fn.Noneio.Reader, preserving crypto/rand behavior. The change is explicitly documented as test-only.
Changed components
lnwallet/musig_session.golnwallet/channel.gopeer/musig_chan_closer.goInspect captured patch +44 / −19
diff --git a/lnwallet/channel.go b/lnwallet/channel.go
index 52310a6..866628d 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -6885,7 +6885,7 @@ func GetSignedCommitTx(inputs SignedCommitTxInputs,
musigSession := NewPartialMusigSession(
*localNonce, inputs.OurKey, inputs.TheirKey, signer,
inputs.SignDesc.Output, LocalMusigCommit,
- tapscriptTweak,
+ tapscriptTweak, fn.None[io.Reader](),
)
var remoteSig lnwire.PartialSigWithNonce
@@ -10261,13 +10261,14 @@ func (lc *LightningChannel) InitRemoteMusigNonces(remoteNonce *musig2.Nonces,
// TODO(roasbeef): propagate rename of signing and verification nonces
sessionCfg := &MusigSessionCfg{
- LocalKey: localChanCfg.MultiSigKey,
- RemoteKey: remoteChanCfg.MultiSigKey,
- LocalNonce: *localNonce,
- RemoteNonce: *remoteNonce,
- Signer: lc.Signer,
- InputTxOut: &lc.fundingOutput,
- TapscriptTweak: lc.channelState.TapscriptRoot,
+ LocalKey: localChanCfg.MultiSigKey,
+ RemoteKey: remoteChanCfg.MultiSigKey,
+ LocalNonce: *localNonce,
+ RemoteNonce: *remoteNonce,
+ Signer: lc.Signer,
+ InputTxOut: &lc.fundingOutput,
+ TapscriptTweak: lc.channelState.TapscriptRoot,
+ CustomNonceRand: lc.opts.customSigningRand,
}
lc.musigSessions = NewMusigPairSession(
sessionCfg,
diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go
index 748e5fa..4c4c1a0 100644
--- a/lnwallet/musig_session.go
+++ b/lnwallet/musig_session.go
@@ -237,6 +237,11 @@ type MusigSession struct {
// instead of the normal BIP 86 tweak when creating the MuSig2
// aggregate key and session.
tapscriptTweak fn.Option[input.MuSig2Tweaks]
+
+ // customNonceRand is an optional custom random source used to generate
+ // deterministic JIT signing nonces. This should only be set in tests
+ // that need reproducible MuSig2 signatures.
+ customNonceRand fn.Option[io.Reader]
}
// NewPartialMusigSession creates a new musig2 session given only the
@@ -245,7 +250,8 @@ type MusigSession struct {
func NewPartialMusigSession(verificationNonce musig2.Nonces,
localKey, remoteKey keychain.KeyDescriptor, signer input.MuSig2Signer,
inputTxOut *wire.TxOut, commitType MusigCommitType,
- tapscriptTweak fn.Option[input.MuSig2Tweaks]) *MusigSession {
+ tapscriptTweak fn.Option[input.MuSig2Tweaks],
+ customNonceRand fn.Option[io.Reader]) *MusigSession {
signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey}
@@ -254,14 +260,15 @@ func NewPartialMusigSession(verificationNonce musig2.Nonces,
}
return &MusigSession{
- nonces: nonces,
- remoteKey: remoteKey,
- localKey: localKey,
- inputTxOut: inputTxOut,
- signerKeys: signerKeys,
- signer: signer,
- commitType: commitType,
- tapscriptTweak: tapscriptTweak,
+ nonces: nonces,
+ remoteKey: remoteKey,
+ localKey: localKey,
+ inputTxOut: inputTxOut,
+ signerKeys: signerKeys,
+ signer: signer,
+ commitType: commitType,
+ tapscriptTweak: tapscriptTweak,
+ customNonceRand: customNonceRand,
}
}
@@ -351,10 +358,17 @@ func (m *MusigSession) SignCommit(tx *wire.MsgTx) (*MusigPartialSig, error) {
// a fresh nonce that'll be sent along side our signature. With
// the nonce in hand, we can finalize the session.
txHash := tx.TxHash()
- signingNonce, err := musig2.GenNonces(
+ nonceOpts := []musig2.NonceGenOption{
musig2.WithPublicKey(m.localKey.PubKey),
musig2.WithNonceAuxInput(txHash[:]),
- )
+ }
+ m.customNonceRand.WhenSome(func(r io.Reader) {
+ nonceOpts = append(
+ nonceOpts,
+ musig2.WithCustomRand(r),
+ )
+ })
+ signingNonce, err := musig2.GenNonces(nonceOpts...)
if err != nil {
return nil, err
}
@@ -409,6 +423,7 @@ func (m *MusigSession) Refresh(verificationNonce *musig2.Nonces,
return NewPartialMusigSession(
*verificationNonce, m.localKey, m.remoteKey, m.signer,
m.inputTxOut, m.commitType, m.tapscriptTweak,
+ m.customNonceRand,
), nil
}
@@ -587,6 +602,11 @@ type MusigSessionCfg struct {
// TapscriptTweak is an optional tweak that can be used to modify the
// MuSig2 public key used in the session.
TapscriptTweak fn.Option[chainhash.Hash]
+
+ // CustomNonceRand is an optional custom random source for generating
+ // deterministic JIT signing nonces. This should only be set in tests
+ // that need reproducible MuSig2 signatures.
+ CustomNonceRand fn.Option[io.Reader]
}
// MusigPairSession houses the two musig2 sessions needed to do funding and
@@ -615,10 +635,12 @@ func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession {
localSession := NewPartialMusigSession(
cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey, cfg.Signer,
cfg.InputTxOut, LocalMusigCommit, tapscriptTweak,
+ cfg.CustomNonceRand,
)
remoteSession := NewPartialMusigSession(
cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey, cfg.Signer,
cfg.InputTxOut, RemoteMusigCommit, tapscriptTweak,
+ cfg.CustomNonceRand,
)
return &MusigPairSession{
diff --git a/peer/musig_chan_closer.go b/peer/musig_chan_closer.go
index 5aa21a5..78911ec 100644
--- a/peer/musig_chan_closer.go
+++ b/peer/musig_chan_closer.go
@@ -2,6 +2,7 @@ package peer
import (
"fmt"
+ "io"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/lightningnetwork/lnd/fn/v2"
@@ -53,6 +54,7 @@ func (m *MusigChanCloser) ProposalClosingOpts() (
*m.remoteNonce, localKey, remoteKey,
m.channel.Signer, m.channel.FundingTxOut(),
lnwallet.RemoteMusigCommit, tapscriptTweak,
+ fn.None[io.Reader](),
)
err := m.musigSession.FinalizeSession(*m.localNonce)
Why this scored 19/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.