lnwallet/chancloser: fix MuSig2 nonce reuse across RBF rounds
What changed, and why it matters
This patch fixes a critical cryptographic bug in LND's taproot cooperative channel close feature. During RBF (fee-bump) rounds, the same secret random number (nonce) was being reused to sign different closing transactions. In MuSig2/taproot signing, reusing a secret nonce with different messages lets an attacker who sees the partial signatures recover your private key. The fix forces a fresh nonce for every RBF round and also stores the partial signature so it isn't regenerated (which would also reuse the nonce).
Upgrade LND nodes running taproot channels with RBF cooperative close to a release containing this commit. Rotate any potentially affected private keys if there is evidence of nonce reuse or exposure of partial signatures to untrusted parties. Review logs for repeated RBF cooperative close rounds prior to the patch.
Security signals we found
MuSig2 secret nonce reuse across signing rounds
Different sighashes signed with same secret nonce
Private key extraction risk via partial-signature linear algebra
Taproot cooperative close RBF path affected
btcd per-session nonce guard bypassed by fresh session per round
Fix includes interface change (InvalidateNonce) and state-machine nonce invalidation
Evidence from the diff
MusigChanCloser.ClosingNonce() previously cached m.localNonce and returned it on every call. Each RBF round created a new MuSig2 session via ProposalClosingOpts() but passed the same SecNonce, bypassing btcd’s per-session nonce-reuse guard because the session object was fresh. Signing different closing transactions (different fees/sighashes) with the same secret nonce enables private-key extraction via the standard two-signature nonce-reuse linear algebra attack on Schnorr/MuSig2 partial signatures. The patch adds InvalidateNonce() to the MusigSession interface, clears the cached nonce and session after each round, stores the full MusigPartialSig in LocalOfferSent so combining signatures does not require re-signing, and fixes extraction of NextCloseeNonce from ClosingSig messages for subsequent rounds.
Changed components
lnwallet/chancloser/rbf_coop_transitions.golnwallet/chancloser/rbf_coop_states.golnwallet/chancloser/interface.gopeer/musig_chan_closer.golnwire/closing_sig.gotaproot cooperative close / RBF fee negotiationInspect captured patch +127 / −114
diff --git a/itest/lnd_coop_close_rbf_test.go b/itest/lnd_coop_close_rbf_test.go
index e73057b..e4af6ea 100644
--- a/itest/lnd_coop_close_rbf_test.go
+++ b/itest/lnd_coop_close_rbf_test.go
@@ -158,8 +158,12 @@ func testCoopCloseRbf(ht *lntest.HarnessTest) {
// Build node config with commitment type args and RBF
// flag.
- baseArgs := lntest.NodeArgsForCommitType(chanType.commitType)
- nodeArgs := append(baseArgs, "--protocol.rbf-coop-close")
+ baseArgs := lntest.NodeArgsForCommitType(
+ chanType.commitType,
+ )
+ nodeArgs := append(
+ baseArgs, "--protocol.rbf-coop-close",
+ )
cfgs := [][]string{nodeArgs, nodeArgs}
// For taproot channels, we need to make them private.
diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go
index 850a162..5d96b96 100644
--- a/lnwallet/chancloser/chancloser_test.go
+++ b/lnwallet/chancloser/chancloser_test.go
@@ -300,6 +300,8 @@ func (m *mockMusigSession) InitRemoteNonce(nonce *musig2.Nonces) {
m.remoteNonce = *nonce
}
+func (m *mockMusigSession) InvalidateNonce() {}
+
func (m *mockMusigSession) ClosingNonce() (*musig2.Nonces, error) {
return &musig2.Nonces{
PubNonce: [66]byte{1, 2, 3},
diff --git a/lnwallet/chancloser/interface.go b/lnwallet/chancloser/interface.go
index 74f0969..f55eec8 100644
--- a/lnwallet/chancloser/interface.go
+++ b/lnwallet/chancloser/interface.go
@@ -132,4 +132,10 @@ type MusigSession interface {
// shutdown message so it can be used later to generate and verify
// signatures.
InitRemoteNonce(nonce *musig2.Nonces)
+
+ // InvalidateNonce clears the cached local nonce, forcing a fresh
+ // nonce to be generated on the next call to ClosingNonce. This
+ // must be called after each RBF round completes to prevent nonce
+ // reuse across iterations.
+ InvalidateNonce()
}
diff --git a/lnwallet/chancloser/rbf_coop_states.go b/lnwallet/chancloser/rbf_coop_states.go
index 675d0bc..61f7718 100644
--- a/lnwallet/chancloser/rbf_coop_states.go
+++ b/lnwallet/chancloser/rbf_coop_states.go
@@ -792,6 +792,12 @@ type LocalOfferSent struct {
// LocalSig is the signature we sent to the remote party.
LocalSig lnwire.Sig
+
+ // LocalMusigSig is the full musig partial signature from when we
+ // signed as closer. Stored here so LocalOfferSent can combine
+ // signatures without re-signing, which prevents nonce reuse across
+ // RBF iterations. Only set for taproot channels.
+ LocalMusigSig fn.Option[lnwallet.MusigPartialSig]
}
// String returns the name of the state for LocalOfferSent, including proposed.
diff --git a/lnwallet/chancloser/rbf_coop_test.go b/lnwallet/chancloser/rbf_coop_test.go
index 86f93a3..73a19cc 100644
--- a/lnwallet/chancloser/rbf_coop_test.go
+++ b/lnwallet/chancloser/rbf_coop_test.go
@@ -3157,31 +3157,26 @@ func (m *strictNonceMusigSession) InitRemoteNonce(nonce *musig2.Nonces) {
m.remoteNonce = *nonce
}
+func (m *strictNonceMusigSession) InvalidateNonce() {}
+
func (m *strictNonceMusigSession) ClosingNonce() (*musig2.Nonces, error) {
return &musig2.Nonces{
PubNonce: [66]byte{1, 2, 3},
}, nil
}
-// TestLocalOfferSentNonceInitOrder verifies that when processing a
-// LocalSigReceived event in the LocalOfferSent state, the NextCloseeNonce from
-// ClosingSig is properly initialized via initLocalMusigCloseeNonce BEFORE
-// calling ProposalClosingOpts. This is critical for taproot channels because
-// ProposalClosingOpts requires the remote nonce to be set to create a valid
-// MuSig2 session.
-//
-// This test catches the bug where ProposalClosingOpts was called before the
-// nonce was initialized, causing "final signature is invalid" errors during
-// cooperative close.
-func TestLocalOfferSentNonceInitOrder(t *testing.T) {
+// TestLocalOfferSentUsesStoredSig verifies that when processing a
+// LocalSigReceived event in the LocalOfferSent state, the stored
+// LocalMusigSig is used for combining rather than re-signing. This
+// prevents nonce reuse across RBF iterations.
+func TestLocalOfferSentUsesStoredSig(t *testing.T) {
t.Parallel()
- // Create a strict mock that will fail if ProposalClosingOpts is called
- // before InitRemoteNonce.
+ // Create a strict mock that will fail if ProposalClosingOpts is
+ // called — it should NOT be called since we use the stored sig.
strictLocalMusig := newStrictNonceMusigSession()
- // The remote's closee nonce from shutdown - this is what should be used
- // for the current transaction.
+ // The remote's closee nonce from shutdown.
remoteCloseeNonceFromShutdown := lnwire.Musig2Nonce{4, 5, 6}
// Set up the environment with the strict mock.
@@ -3200,16 +3195,17 @@ func TestLocalOfferSentNonceInitOrder(t *testing.T) {
},
}
- // Create the LocalOfferSent state that we'll be testing.
+ // Create the LocalOfferSent state with a stored musig sig,
+ // simulating what LocalCloseStart would have stored.
localOfferSent := &LocalOfferSent{
CloseChannelTerms: closeTerms,
ProposedFee: btcutil.Amount(1000),
ProposedFeeRate: chainfee.FeePerKwFloor.FeePerVByte(),
LocalSig: localSchnorrSig,
+ LocalMusigSig: fn.Some(lnwallet.MusigPartialSig{}),
}
// The environment needs both musig sessions set for taproot path.
- // IsTaproot() returns true when both sessions are non-nil.
env := &Environment{
ChanPoint: randOutPoint(t),
LocalMusigSession: strictLocalMusig,
@@ -3217,7 +3213,6 @@ func TestLocalOfferSentNonceInitOrder(t *testing.T) {
}
// Create a LocalSigReceived event with NextCloseeNonce.
- // This simulates receiving a ClosingSig from the remote party.
nextCloseeNonce := lnwire.Musig2Nonce{10, 11, 12}
localSigEvent := &LocalSigReceived{
SigMsg: lnwire.ClosingSig{
@@ -3238,42 +3233,28 @@ func TestLocalOfferSentNonceInitOrder(t *testing.T) {
},
}
- // Process the event. If the fix is correct, InitRemoteNonce will be
- // called before ProposalClosingOpts.
- //
- // We expect a panic or error from the later code because we don't have
- // a full environment (missing CloseSigner, etc). We use recover to
- // catch any panics and still check our assertions.
+ // Process the event. We expect it to use CombineClosingOpts with the
+ // stored sig, NOT ProposalClosingOpts + CreateCloseProposal.
func() {
defer func() {
- // Recover from any panic - we just want to check that
- // ProposalClosingOpts was called in the right order.
_ = recover()
}()
_, _ = localOfferSent.ProcessEvent(localSigEvent, env)
}()
- // The critical assertion: ProposalClosingOpts should NOT have been
- // called before InitRemoteNonce.
+ // ProposalClosingOpts should NOT have been called — we use the stored
+ // sig directly via CombineClosingOpts.
require.False(
t, strictLocalMusig.proposalOptsCalledBeforeInit,
- "ProposalClosingOpts was called before InitRemoteNonce - "+
- "this would cause 'final signature is invalid' errors",
- )
-
- // Also verify that InitRemoteNonce was actually called.
- require.True(
- t, strictLocalMusig.remoteNonceInited,
- "InitRemoteNonce should have been called",
+ "ProposalClosingOpts should not be called when using "+
+ "stored MusigPartialSig",
)
- // And that it was called with the correct nonce from NonceState
- // (established during shutdown), NOT the NextCloseeNonce from
- // ClosingSig.
+ // Verify that the NextCloseeNonce was stored for the next RBF round.
require.Equal(
- t, musig2.Nonces{PubNonce: remoteCloseeNonceFromShutdown},
- strictLocalMusig.remoteNonce,
+ t, fn.Some(nextCloseeNonce),
+ localOfferSent.NonceState.RemoteCloseeNonce,
"InitRemoteNonce should be called with RemoteCloseeNonce from "+
"NonceState (not NextCloseeNonce from ClosingSig)",
)
diff --git a/lnwallet/chancloser/rbf_coop_transitions.go b/lnwallet/chancloser/rbf_coop_transitions.go
index 25f8e62..9c3788d 100644
--- a/lnwallet/chancloser/rbf_coop_transitions.go
+++ b/lnwallet/chancloser/rbf_coop_transitions.go
@@ -942,6 +942,17 @@ func (c *ClosingNegotiation) updateAndValidateCloseTerms(event ProtocolEvent,
return err
}
+ // For taproot channels, extract the NextCloseeNonce from
+ // ClosingSig if present. This will be used for the next RBF
+ // iteration when we act as closer. This is their new closee
+ // nonce.
+ _, nextCloseeNonce := validateAndExtractSigAndNonce(
+ msg.SigMsg, isTaproot,
+ )
+ nextCloseeNonce.WhenSome(func(nonce lnwire.Musig2Nonce) {
+ c.NonceState.RemoteCloseeNonce = fn.Some(nonce)
+ })
+
return nil
}
@@ -1243,11 +1254,19 @@ func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
"to remote party, fee_sats=%v", env.ChanPoint,
absoluteFee)
+ // For taproot channels, stash the full MusigPartialSig so
+ // LocalOfferSent can combine signatures without re-signing.
+ var localMusigSig fn.Option[lnwallet.MusigPartialSig]
+ if musigPartialSig != nil {
+ localMusigSig = fn.Some(*musigPartialSig)
+ }
+
return &CloseStateTransition{
NextState: &LocalOfferSent{
ProposedFee: absoluteFee,
ProposedFeeRate: msg.TargetFeeRate,
LocalSig: wireSig,
+ LocalMusigSig: localMusigSig,
CloseChannelTerms: l.CloseChannelTerms,
},
NewEvents: fn.Some(RbfEvent{
@@ -1415,70 +1434,62 @@ func extractTaprootPartialSig(sigs lnwire.TaprootPartialSigs) (
func prepareClosingSignatures(env *Environment, l *LocalOfferSent,
msg *LocalSigReceived, sig lnwire.Sig,
closeOpts []lnwallet.ChanCloseOpt,
-) (localSig, remoteSig input.Signature, err error) {
+) (localSig, remoteSig input.Signature,
+ musigOpts []lnwallet.ChanCloseOpt, err error) {
if env.IsTaproot() {
- // For taproot channels, we need to reconstruct the
- // MusigPartialSig from the wire signature. We'll need to create
- // a new CreateCloseProposal to get the proper MusigPartialSig
- // that CompleteCooperativeClose expects.
- rawLocalSig, _, _, err := env.CloseSigner.CreateCloseProposal(
- l.ProposedFee, l.LocalDeliveryScript,
- l.RemoteDeliveryScript, closeOpts...,
+ // Use the stored MusigPartialSig from LocalCloseStart rather
+ // than re-signing. This prevents nonce reuse across the two
+ // state transitions within a closer round.
+ storedSig, err := l.LocalMusigSig.UnwrapOrErr(
+ fmt.Errorf("missing stored musig partial sig " +
+ "for taproot channel"),
)
if err != nil {
- return nil, nil, fmt.Errorf("failed to recreate "+
- "local sig: %w", err)
+ return nil, nil, nil, err
}
- localSig = rawLocalSig
- // Extract the partial sig from the message using our helper
- // function.
+ localPartialSig := storedSig.ToWireSig().PartialSig
+
+ // Extract the remote's partial sig from their ClosingSig.
remotePartialSigOpt := extractTaprootPartialSig(
msg.SigMsg.TaprootPartialSigs,
)
if remotePartialSigOpt.IsNone() {
- return nil, nil, fmt.Errorf("no taproot partial " +
- "sig found in message")
+ return nil, nil, nil, fmt.Errorf("no taproot " +
+ "partial sig found in message")
}
remotePartialSig := remotePartialSigOpt.UnwrapOr(
lnwire.PartialSig{},
)
- // We also need our local partial sig in wire format.
- localMusigSig, ok := rawLocalSig.(*lnwallet.MusigPartialSig)
- if !ok {
- return nil, nil, fmt.Errorf("expected local sig to "+
- "be MusigPartialSig, got %T", rawLocalSig)
- }
- localPartialSig := localMusigSig.ToWireSig().PartialSig
-
- // Use CombineClosingOpts to get the proper signatures.
- // notlint:ll
- localCombined, remoteCombined, _, err := env.LocalMusigSession.CombineClosingOpts(
+ // Combine both partial signatures using the musig session
+ // from the original ProposalClosingOpts call.
+ //nolint:ll
+ localCombined, remoteCombined, combinedOpts, err := env.LocalMusigSession.CombineClosingOpts(
localPartialSig, remotePartialSig,
)
if err != nil {
- return nil, nil, fmt.Errorf("failed to combine "+
- "closing opts: %w", err)
+ return nil, nil, nil, fmt.Errorf("failed to "+
+ "combine closing opts: %w", err)
}
- return localCombined, remoteCombined, nil
+ return localCombined, remoteCombined, combinedOpts, nil
}
// For non-taproot channels, convert wire signatures to regular
// signatures.
remoteSig, err = sig.ToSignature()
if err != nil {
- return nil, nil, err
+ return nil, nil, nil, err
}
localSig, err = l.LocalSig.ToSignature()
if err != nil {
- return nil, nil, err
+ return nil, nil, nil, err
}
- return localSig, remoteSig, nil
+ return localSig, remoteSig, nil, nil
}
// ProcessEvent implements the state transition function for the
@@ -1509,36 +1520,23 @@ func (l *LocalOfferSent) ProcessEvent(event ProtocolEvent, env *Environment,
lnwallet.WithCustomPayer(lntypes.Local),
)
- // For taproot channels, we need to initialize the remote's
- // closee nonce BEFORE calling ProposalClosingOpts. We use the
- // nonce from NonceState (set during shutdown or from prior
- // ClosingSig's NextCloseeNonce).
+ // For taproot channels, update NonceState with the new nonce
+ // from ClosingSig for potential future RBF iterations.
if env.IsTaproot() {
- // Initialize the remote nonce from our stored state.
- // This is the nonce the remote party committed to in
- // their shutdown message (or their previous ClosingSig).
- initLocalMusigCloseeNonce(env, l.NonceState.RemoteCloseeNonce)
-
- // Now that the nonce is initialized, we can safely get
- // the musig closing options.
- musigOpts, err := env.LocalMusigSession.ProposalClosingOpts()
- if err != nil {
- return nil, fmt.Errorf("failed to get musig "+
- "closing opts: %w", err)
- }
- closeOpts = append(closeOpts, musigOpts...)
-
- // Update NonceState with the new nonce from ClosingSig
- // for potential future RBF iterations.
l.NonceState.RemoteCloseeNonce = nextCloseeNonce
}
- localSig, remoteSig, err := prepareClosingSignatures(
+ // Prepare the closing signatures. For taproot, this uses the
+ // stored MusigPartialSig from LocalCloseStart (no re-signing).
+ // The returned musigOpts contain the musig session needed by
+ // CompleteCooperativeClose.
+ localSig, remoteSig, musigOpts, err := prepareClosingSignatures(
env, l, msg, sig, closeOpts,
)
if err != nil {
return nil, err
}
+ closeOpts = append(closeOpts, musigOpts...)
// Now that we have their signature, we'll attempt to validate
// it, then extract a valid closing signature from it.
@@ -1550,6 +1548,13 @@ func (l *LocalOfferSent) ProcessEvent(event ProtocolEvent, env *Environment,
return nil, err
}
+ // Invalidate the closer nonce now that the round is complete.
+ // The next RBF round will generate a fresh nonce in
+ // LocalCloseStart.
+ if env.IsTaproot() {
+ env.LocalMusigSession.InvalidateNonce()
+ }
+
// As we're about to broadcast a new version of the co-op close
// transaction, we'll mark again as broadcast, but with this
// variant of the co-op close tx.
@@ -2014,6 +2019,13 @@ func (l *RemoteCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
lnutils.SpewLogClosure(closeTx),
)
+ // Invalidate the closee nonce that was consumed for signing.
+ // This forces createClosingSigMessage to generate a fresh
+ // nonce for NextCloseeNonce in the next RBF round.
+ if env.IsTaproot() {
+ env.RemoteMusigSession.InvalidateNonce()
+ }
+
closingSigMsg, err := createClosingSigMessage(
env, wireSig, localSig, l.LocalDeliveryScript,
l.RemoteDeliveryScript, msg.SigMsg.FeeSatoshis,
diff --git a/lnwire/closing_sig.go b/lnwire/closing_sig.go
index a6f8583..a14dcb8 100644
--- a/lnwire/closing_sig.go
+++ b/lnwire/closing_sig.go
@@ -9,7 +9,6 @@ import (
"github.com/lightningnetwork/lnd/tlv"
)
-
// TaprootPartialSigs houses the 3 possible taproot partial signatures (without nonces)
// that can be sent in a ClosingSig message. These use just PartialSig since the
// receiver already knows our nonce from the previous ClosingComplete.
@@ -77,19 +76,19 @@ type ClosingSig struct {
// decodeClosingSigSigs decodes the closing sig TLV records from the passed
// ExtraOpaqueData.
-func decodeClosingSigSigs(c *ClosingSigs, tp *TaprootPartialSigs,
- nextNonce *tlv.OptionalRecordT[tlv.TlvType22, Musig2Nonce],
+func decodeClosingSigSigs(c *ClosingSigs, tp *TaprootPartialSigs,
+ nextNonce *tlv.OptionalRecordT[tlv.TlvType22, Musig2Nonce],
tlvRecords ExtraOpaqueData) error {
// Regular signatures
sig1 := c.CloserNoClosee.Zero()
sig2 := c.NoCloserClosee.Zero()
sig3 := c.CloserAndClosee.Zero()
-
+
// Taproot partial signatures (without nonces)
tSig1 := tp.CloserNoClosee.Zero()
tSig2 := tp.NoCloserClosee.Zero()
tSig3 := tp.CloserAndClosee.Zero()
-
+
// Next closee nonce for RBF
nonce := nextNonce.Zero()
@@ -110,7 +109,7 @@ func decodeClosingSigSigs(c *ClosingSigs, tp *TaprootPartialSigs,
if val, ok := typeMap[c.CloserAndClosee.TlvType()]; ok && val == nil {
c.CloserAndClosee = tlv.SomeRecordT(sig3)
}
-
+
// Taproot partial signatures
if val, ok := typeMap[tp.CloserNoClosee.TlvType()]; ok && val == nil {
tp.CloserNoClosee = tlv.SomeRecordT(tSig1)
@@ -121,7 +120,7 @@ func decodeClosingSigSigs(c *ClosingSigs, tp *TaprootPartialSigs,
if val, ok := typeMap[tp.CloserAndClosee.TlvType()]; ok && val == nil {
tp.CloserAndClosee = tlv.SomeRecordT(tSig3)
}
-
+
// Next closee nonce
if val, ok := typeMap[nextNonce.TlvType()]; ok && val == nil {
*nextNonce = tlv.SomeRecordT(nonce)
@@ -173,10 +172,10 @@ func (c *ClosingSig) Decode(r io.Reader, _ uint32) error {
// closingSigSigRecords returns the set of records that encode the closing sigs,
// including both regular and taproot signatures.
-func closingSigSigRecords(c *ClosingSigs, tp *TaprootPartialSigs,
+func closingSigSigRecords(c *ClosingSigs, tp *TaprootPartialSigs,
nextNonce tlv.OptionalRecordT[tlv.TlvType22, Musig2Nonce]) []tlv.RecordProducer {
recordProducers := make([]tlv.RecordProducer, 0, 7)
-
+
// Regular signatures
c.CloserNoClosee.WhenSome(func(sig tlv.RecordT[tlv.TlvType1, Sig]) {
recordProducers = append(recordProducers, &sig)
@@ -187,7 +186,7 @@ func closingSigSigRecords(c *ClosingSigs, tp *TaprootPartialSigs,
c.CloserAndClosee.WhenSome(func(sig tlv.RecordT[tlv.TlvType3, Sig]) {
recordProducers = append(recordProducers, &sig)
})
-
+
// Taproot partial signatures (without nonces)
tp.CloserNoClosee.WhenSome(func(sig tlv.RecordT[tlv.TlvType5, PartialSig]) {
recordProducers = append(recordProducers, &sig)
@@ -198,7 +197,7 @@ func closingSigSigRecords(c *ClosingSigs, tp *TaprootPartialSigs,
tp.CloserAndClosee.WhenSome(func(sig tlv.RecordT[tlv.TlvType7, PartialSig]) {
recordProducers = append(recordProducers, &sig)
})
-
+
// Next closee nonce for RBF
nextNonce.WhenSome(func(nonce tlv.RecordT[tlv.TlvType22, Musig2Nonce]) {
recordProducers = append(recordProducers, &nonce)
diff --git a/lnwire/shutdown.go b/lnwire/shutdown.go
index 9715b90..a7330ca 100644
--- a/lnwire/shutdown.go
+++ b/lnwire/shutdown.go
@@ -24,7 +24,6 @@ func SomeShutdownNonce(nonce Musig2Nonce) ShutdownNonceTLV {
)
}
-
// Shutdown is sent by either side in order to initiate the cooperative closure
// of a channel. This message is sparse as both sides implicitly have the
// information necessary to construct a transaction that will send the settled
diff --git a/peer/musig_chan_closer.go b/peer/musig_chan_closer.go
index 149ebcf..5aa21a5 100644
--- a/peer/musig_chan_closer.go
+++ b/peer/musig_chan_closer.go
@@ -104,13 +104,9 @@ func (m *MusigChanCloser) CombineClosingOpts(localSig,
return localMuSig, remoteMuSig, opts, nil
}
-// ClosingNonce returns the nonce that should be used when generating the our
-// partial signature for the remote party.
+// ClosingNonce generates a fresh nonce for our partial signature. A new nonce
+// is generated on every call to prevent nonce reuse across RBF iterations.
func (m *MusigChanCloser) ClosingNonce() (*musig2.Nonces, error) {
- if m.localNonce != nil {
- return m.localNonce, nil
- }
-
localKey, _ := m.channel.MultiSigKeys()
nonce, err := musig2.GenNonces(
musig2.WithPublicKey(localKey.PubKey),
@@ -130,6 +126,14 @@ func (m *MusigChanCloser) InitRemoteNonce(nonce *musig2.Nonces) {
m.remoteNonce = nonce
}
+// InvalidateNonce clears the cached local nonce, forcing a fresh nonce to be
+// generated on the next call to ClosingNonce. This prevents nonce reuse across
+// RBF iterations.
+func (m *MusigChanCloser) InvalidateNonce() {
+ m.localNonce = nil
+ m.musigSession = nil
+}
+
// A compile-time assertion to ensure MusigChanCloser implements the
// chancloser.MusigSession interface.
var _ chancloser.MusigSession = (*MusigChanCloser)(nil)
Why this scored 82/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.