multi: wire taproot RBF support throughout the stack
What changed, and why it matters
This commit enables a new, more flexible way for taproot Lightning channels to close cooperatively. Previously, taproot channels had to fall back to an older close path even when the new RBF-based cooperative close feature was enabled. The change removes that restriction and adds the necessary nonce-exchange logic so taproot channels can use the modern close flow. It is a feature-completion patch rather than a clear-cut security fix, but because it touches cryptographic state setup for channel closing, it could affect safety if the new path has latent bugs.
Treat this as a feature-enablement change with security implications. Review the related MuSig2 nonce rotation and session management code for correctness, run integration tests covering taproot cooperative close and restart scenarios, and monitor for any follow-up fixes that may indicate latent issues in the new taproot RBF close path.
Security signals we found
Feature completion for taproot RBF cooperative close
MuSig2 session setup added for taproot channels during RBF close initialization
Removes taproot exclusion from new close path
Touches cryptographic nonce exchange path in peer connection handling
Evidence from the diff
The patch wires taproot channel support into LND’s RBF cooperative close flow. It removes several isTaprootChan checks that forced taproot channels onto the legacy closer, updates the protocol option description to no longer say taproot channels are unsupported, and in initRbfChanCloser attaches a MusigChanCloser to both LocalMusigSession and RemoteMusigSession when the channel is taproot. This completes the integration of MuSig2-based nonce handling for taproot RBF closes.
Changed components
lncfg/protocol.gopeer/brontide.goRBF cooperative close state machineTaproot channel close pathMuSig2 nonce/session handlingInspect captured patch +18 / −20
diff --git a/lncfg/protocol.go b/lncfg/protocol.go
index 73fc7da..a300b70 100644
--- a/lncfg/protocol.go
+++ b/lncfg/protocol.go
@@ -37,7 +37,7 @@ type ProtocolOptions struct {
// RbfCoopClose should be set if we want to signal that we support for
// the new experimental RBF coop close feature.
- RbfCoopClose bool `long:"rbf-coop-close" description:"if set, then lnd will signal that it supports the new RBF based coop close protocol, taproot channels are not supported"`
+ RbfCoopClose bool `long:"rbf-coop-close" description:"if set, then lnd will signal that it supports the new RBF based coop close protocol"`
// NoAnchors should be set if we don't want to support opening or accepting
// channels having the anchor commitment type.
diff --git a/peer/brontide.go b/peer/brontide.go
index 336b88e..e93a8d6 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -1308,7 +1308,6 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
return nil, err
}
- isTaprootChan := lnChan.ChanType().IsTaproot()
var (
shutdownMsg fn.Option[lnwire.Shutdown]
@@ -1316,9 +1315,8 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
)
shutdownInfo.WhenSome(func(info channeldb.ShutdownInfo) {
// If we can use the new RBF close feature, we don't
- // need to create the legacy closer. However for taproot
- // channels, we'll continue to use the legacy closer.
- if p.rbfCoopCloseAllowed() && !isTaprootChan {
+ // need to create the legacy closer.
+ if p.rbfCoopCloseAllowed() {
return
}
@@ -1395,10 +1393,8 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
p.activeChannels.Store(chanID, lnChan)
// We're using the old co-op close, so we don't need to init
- // the new RBF chan closer. If we have a taproot chan, then
- // we'll also use the legacy type, so we don't need to make the
- // new closer.
- if !p.rbfCoopCloseAllowed() || isTaprootChan {
+ // the new RBF chan closer.
+ if !p.rbfCoopCloseAllowed() {
continue
}
@@ -3429,7 +3425,6 @@ func chooseDeliveryScript(upfront, requested lnwire.DeliveryAddress,
func (p *Brontide) restartCoopClose(lnChan *lnwallet.LightningChannel) (
*lnwire.Shutdown, error) {
- isTaprootChan := lnChan.ChanType().IsTaproot()
// If this channel has status ChanStatusCoopBroadcasted and does not
// have a closing transaction, then the cooperative close process was
@@ -3483,8 +3478,8 @@ func (p *Brontide) restartCoopClose(lnChan *lnwallet.LightningChannel) (
// If the new RBF co-op close is negotiated, then we'll init and start
// that state machine, skipping the steps for the negotiate machine
- // below. We don't support this close type for taproot channels though.
- if p.rbfCoopCloseAllowed() && !isTaprootChan {
+ // below.
+ if p.rbfCoopCloseAllowed() {
_, err := p.initRbfChanCloser(lnChan)
if err != nil {
return nil, fmt.Errorf("unable to init rbf chan "+
@@ -4027,6 +4022,14 @@ func (p *Brontide) initRbfChanCloser(
),
}
+ // For taproot channels, we need to set both LocalMusigSession and
+ // RemoteMusigSession to handle nonce exchange during RBF cooperative close.
+ if channel.ChanType().IsTaproot() {
+ musigCloser := NewMusigChanCloser(channel)
+ env.LocalMusigSession = musigCloser
+ env.RemoteMusigSession = musigCloser
+ }
+
spendEvent := protofsm.RegisterSpend[chancloser.ProtocolEvent]{
OutPoint: channel.ChannelPoint(),
PkScript: channel.FundingTxOut().PkScript,
@@ -4339,7 +4342,6 @@ func (p *Brontide) handleLocalCloseReq(req *htlcswitch.ChanClose) {
return
}
- isTaprootChan := channel.ChanType().IsTaproot()
switch req.CloseType {
// A type of CloseRegular indicates that the user has opted to close
@@ -4353,9 +4355,7 @@ func (p *Brontide) handleLocalCloseReq(req *htlcswitch.ChanClose) {
// iteration, in which case we'll be obtaining a new
// transaction w/ a higher fee rate.
//
- // We don't support this close type for taproot channels yet
- // however.
- case !isTaprootChan && p.rbfCoopCloseAllowed():
+ case p.rbfCoopCloseAllowed():
err = p.startRbfChanCloser(
newRPCShutdownInit(req), channel.ChannelPoint(),
)
@@ -5373,12 +5373,10 @@ func (p *Brontide) addActiveChannel(c *lnpeer.NewChannel) error {
"peer", chanPoint)
}
- isTaprootChan := c.ChanType.IsTaproot()
// We're using the old co-op close, so we don't need to init the new RBF
- // chan closer. If this is a taproot channel, then we'll also fall
- // through, as we don't support this type yet w/ rbf close.
- if !p.rbfCoopCloseAllowed() || isTaprootChan {
+ // chan closer.
+ if !p.rbfCoopCloseAllowed() {
return nil
}
Why this scored 34/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.