lnwire: reject messages with both regular and taproot signatures
What changed, and why it matters
This change adds an extra safety check when LND receives certain channel-closing messages. It now rejects messages that contain both old-style ECDSA signatures and new taproot-style signatures at the same time. The developers describe this as 'defense-in-depth,' meaning it is an extra guard rail rather than a fix for a known active attack. It makes the protocol stricter so a malformed or malicious message cannot carry both signature types.
Adopt the patch as a hardening improvement. Operators and downstream integrators should ensure they are running a version that includes this decode-time check if they use taproot-aware channel closes. No immediate emergency response is indicated by the commit content alone, but routine update planning is reasonable.
Security signals we found
Defense-in-depth input validation added at decode time
Rejects mixed signature-type TLV payloads in closing protocol messages
Prevents potential ambiguity in which signature set is authoritative
No CVE, advisory, or researcher attribution present in commit materials
Evidence from the diff
The commit modifies decodeClosingSigs in lnwire/closing_complete.go and decodeClosingSigSigs in lnwire/closing_sig.go. After parsing optional TLV records, it checks whether any of the three closing-signature slots (CloserNoClosee, NoCloserClosee, CloserAndClosee) are populated in both the regular ClosingSigs and the taproot variants. If both are present, decoding returns an error. This prevents a single wire message from carrying both ECDSA and taproot partial signatures, which the higher-level state machine was already expected to reject. The patch is purely additive validation at the deserialization layer.
Changed components
lnwire/closing_complete.golnwire/closing_sig.goClosing protocol message decoding (ClosingComplete, ClosingSig)Inspect captured patch +24 / −0
diff --git a/lnwire/closing_complete.go b/lnwire/closing_complete.go
index fd989be..88fead2 100644
--- a/lnwire/closing_complete.go
+++ b/lnwire/closing_complete.go
@@ -2,6 +2,7 @@ package lnwire
import (
"bytes"
+ "fmt"
"io"
"github.com/btcsuite/btcd/btcutil"
@@ -121,6 +122,17 @@ func decodeClosingSigs(c *ClosingSigs, tc *TaprootClosingSigs, tlvRecords ExtraO
tc.CloserAndClosee = tlv.SomeRecordT(tSig3)
}
+ // Reject messages that contain both regular and taproot signatures.
+ hasRegular := c.CloserNoClosee.IsSome() ||
+ c.NoCloserClosee.IsSome() || c.CloserAndClosee.IsSome()
+ hasTaproot := tc.CloserNoClosee.IsSome() ||
+ tc.NoCloserClosee.IsSome() || tc.CloserAndClosee.IsSome()
+
+ if hasRegular && hasTaproot {
+ return fmt.Errorf("closing_complete contains both " +
+ "regular and taproot signatures")
+ }
+
return nil
}
diff --git a/lnwire/closing_sig.go b/lnwire/closing_sig.go
index 58daff6..a6f8583 100644
--- a/lnwire/closing_sig.go
+++ b/lnwire/closing_sig.go
@@ -2,6 +2,7 @@ package lnwire
import (
"bytes"
+ "fmt"
"io"
"github.com/btcsuite/btcd/btcutil"
@@ -126,6 +127,17 @@ func decodeClosingSigSigs(c *ClosingSigs, tp *TaprootPartialSigs,
*nextNonce = tlv.SomeRecordT(nonce)
}
+ // Reject messages that contain both regular and taproot signatures.
+ hasRegular := c.CloserNoClosee.IsSome() ||
+ c.NoCloserClosee.IsSome() || c.CloserAndClosee.IsSome()
+ hasTaproot := tp.CloserNoClosee.IsSome() ||
+ tp.NoCloserClosee.IsSome() || tp.CloserAndClosee.IsSome()
+
+ if hasRegular && hasTaproot {
+ return fmt.Errorf("closing_sig contains both " +
+ "regular and taproot signatures")
+ }
+
return nil
}
Why this scored 47/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.