lnwallet/chancloser: address PR review comments
What changed, and why it matters
This commit is a small code cleanup in LND's cooperative channel-closing logic. The only behavior change that matters for security is replacing a risky type assertion with a safe one, so the program returns an error instead of crashing if an unexpected signature type is passed. The rest of the changes are typo fixes, comment moves, and minor simplifications. There is no evidence this fixes an actively exploitable vulnerability.
Treat as a routine hardening/cleanup commit. Include in normal review and testing; no urgent security response is warranted based on the diff alone.
Security signals we found
Unsafe type assertion replaced with safe type assertion to prevent panic
Defensive error handling added in taproot signature path
No memory corruption, authentication bypass, or protocol weakness visible in diff
Evidence from the diff
The patch modifies lnwallet/chancloser/rbf_coop_transitions.go and its test file. The functional change is in createClosingSigMessage: the unsafe type assertion localSig.(*lnwallet.MusigPartialSig) is replaced with a safe type assertion that returns a formatted error if the dynamic type does not match. Other changes: a nested if for taproot nonce generation is flattened, a typo in a comment is fixed, an unnecessary generic type argument is removed, and a doc comment is moved to the correct exported test function. No cryptographic or protocol flaw is evident from the diff.
Changed components
lnwallet/chancloser/rbf_coop_transitions.golnwallet/chancloser/rbf_coop_test.goInspect captured patch +34 / −31
diff --git a/lnwallet/chancloser/rbf_coop_test.go b/lnwallet/chancloser/rbf_coop_test.go
index 563f407..c02adec 100644
--- a/lnwallet/chancloser/rbf_coop_test.go
+++ b/lnwallet/chancloser/rbf_coop_test.go
@@ -1245,8 +1245,8 @@ func testRemoteInitiatedCloseOkNonTap(t *testing.T, ctx context.Context) {
// We assert our shutdown events, and also that we eventually
// send a shutdown to the remote party. We'll hold back the
- // send in this case though, as we should only send once the no
- // updates are dangling.
+ // send in this case though, as we should only send once there
+ // are no updates dangling.
closeHarness.expectShutdownEvents(shutdownExpect{
isInitiator: false,
allowSend: false,
@@ -1303,8 +1303,8 @@ func testRemoteInitiatedCloseOkTaproot(t *testing.T, ctx context.Context) {
// We assert our shutdown events, and also that we eventually
// send a shutdown to the remote party. We'll hold back the
- // send in this case though, as we should only send once the no
- // updates are dangling.
+ // send in this case though, as we should only send once there
+ // are no updates dangling.
closeHarness.expectShutdownEvents(shutdownExpect{
isInitiator: false,
allowSend: false,
@@ -2147,10 +2147,6 @@ func testRecvOfferRbfLoopIterations(t *testing.T, closeTerms *CloseChannelTerms,
})
}
-// TestRbfCloseClosingNegotiationLocal tests the local portion of the primary
-// RBF close loop. We should be able to transition to a close state, get a sig,
-// then restart all over again to re-request a signature of at new higher fee
-// rate.
// testSendOfferIterationNoDust is a helper function that tests the send offer
// iteration scenario for both taproot and non-taproot channels.
func testSendOfferIterationNoDust(t *testing.T, startingState *ClosingNegotiation,
@@ -2221,6 +2217,10 @@ func testSendOfferIterationNoDust(t *testing.T, startingState *ClosingNegotiatio
})
}
+// TestRbfCloseClosingNegotiationLocal tests the local portion of the primary
+// RBF close loop. We should be able to transition to a close state, get a sig,
+// then restart all over again to re-request a signature at a new higher fee
+// rate.
func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
t.Parallel()
ctx := context.Background()
diff --git a/lnwallet/chancloser/rbf_coop_transitions.go b/lnwallet/chancloser/rbf_coop_transitions.go
index bbbe8f0..50732d3 100644
--- a/lnwallet/chancloser/rbf_coop_transitions.go
+++ b/lnwallet/chancloser/rbf_coop_transitions.go
@@ -52,26 +52,24 @@ func sendShutdownEvents(chanID lnwire.ChannelID, chanPoint wire.OutPoint,
// For taproot channels using modern RBF flow, auto-generate closee
// nonce if not provided. The shutdown message only contains our closee
// nonce - the nonce the remote party will use when they act as closer.
- if env.IsTaproot() {
- // If closee nonce not provided, generate one now. Note how we
- // generate it using the RemoteMusigSession, as that'll set our
- // localNonce, we'll receive their remoteNonce for this session
- // once we get their ClosingComplete message.
- if localCloseeNonce.IsNone() {
- remoteMusig := env.RemoteMusigSession
- if remoteMusig != nil {
- closeeNonces, err := remoteMusig.ClosingNonce()
- if err != nil {
- return nil, none, fmt.Errorf("unable "+
- "to generate closee "+
- "nonce: %w", err)
- }
- localCloseeNonce = fn.Some(
- lnwire.Musig2Nonce(
- closeeNonces.PubNonce,
- ),
- )
+ if env.IsTaproot() && localCloseeNonce.IsNone() {
+ // Generate closee nonce now. Note how we generate it using the
+ // RemoteMusigSession, as that'll set our localNonce, we'll
+ // receive their remoteNonce for this session once we get their
+ // ClosingComplete message.
+ remoteMusig := env.RemoteMusigSession
+ if remoteMusig != nil {
+ closeeNonces, err := remoteMusig.ClosingNonce()
+ if err != nil {
+ return nil, none, fmt.Errorf("unable "+
+ "to generate closee "+
+ "nonce: %w", err)
}
+ localCloseeNonce = fn.Some(
+ lnwire.Musig2Nonce(
+ closeeNonces.PubNonce,
+ ),
+ )
}
}
@@ -853,7 +851,7 @@ func extractSigAndNonceFromClosingSig(msg lnwire.ClosingSig,
"sigs present"), fn.None[lnwire.Musig2Nonce]()
}
- // If it's a taprotot sig, then we may need to also extract the nonce.
+ // If it's a taproot sig, then we may need to also extract the nonce.
if hasTaprootSigs {
return extractTaprootSigAndNonce(msg)
}
@@ -1316,10 +1314,15 @@ func createClosingSigMessage(env *Environment, wireSig lnwire.Sig,
)
// For taproot channels, use PartialSig (no nonce) since receiver knows
- // our nonce
+ // our nonce.
if env.IsTaproot() {
// We already have the MusigPartialSig from earlier.
- musigSig := localSig.(*lnwallet.MusigPartialSig)
+ musigSig, ok := localSig.(*lnwallet.MusigPartialSig)
+ if !ok {
+ return nil, fmt.Errorf("expected "+
+ "MusigPartialSig for taproot channel, "+
+ "got %T", localSig)
+ }
wireSigWithNonce := musigSig.ToWireSig()
partialSig := wireSigWithNonce.PartialSig
@@ -1677,7 +1680,7 @@ func NewRegularSigType(sig lnwire.Sig) SigType {
// NewTaprootSigType creates a SigType for a taproot signature with nonce.
func NewTaprootSigType(ps lnwire.PartialSigWithNonce) SigType {
- return fn.NewRight[lnwire.Sig, lnwire.PartialSigWithNonce](ps)
+ return fn.NewRight[lnwire.Sig](ps)
}
// SigFieldSet represents which signature fields are present in a
Why this scored 24/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.