From 23a9a761e354635e566c8dce7af0a98b7489fc21 Mon Sep 17 00:00:00 2001
From: Olaoluwa Osuntokun <laolu32@gmail.com>
Date: Wed, 31 Jan 2024 19:27:03 -0800
Subject: [PATCH] peer: add initial awareness of new rbf coop closer

In this commit, we use the interfaces we created in the prior commit to
make a new method capable of spinning up the new rbf coop closer.
---
 lnwallet/channel.go |  13 ++++
 peer/brontide.go    | 158 +++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 170 insertions(+), 1 deletion(-)

diff --git a/lnwallet/channel.go b/lnwallet/channel.go
index d7489f942..ab064fbd4 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -9935,3 +9935,16 @@ func (lc *LightningChannel) FundingBlob() fn.Option[tlv.Blob] {
 		return newBlob
 	})(lc.channelState.CustomBlob)
 }
+
+// ZeroConfRealScid returns an optional real scid for the channel. If this
+// returns None, then this isn't a zero conf channel. Otherwise, the real scid
+// value will be returned.
+//
+//nolint:ll
+func (lc *LightningChannel) ZeroConfRealScid() fn.Option[lnwire.ShortChannelID] {
+	if lc.channelState.IsZeroConf() {
+		return fn.Some(lc.channelState.ZeroConfRealScid())
+	}
+
+	return fn.None[lnwire.ShortChannelID]()
+}
diff --git a/peer/brontide.go b/peer/brontide.go
index 08fe9a27d..5214120a0 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -3,6 +3,7 @@ package peer
 import (
 	"bytes"
 	"container/list"
+	"context"
 	"errors"
 	"fmt"
 	"math/rand"
@@ -46,6 +47,7 @@ import (
 	"github.com/lightningnetwork/lnd/msgmux"
 	"github.com/lightningnetwork/lnd/netann"
 	"github.com/lightningnetwork/lnd/pool"
+	"github.com/lightningnetwork/lnd/protofsm"
 	"github.com/lightningnetwork/lnd/queue"
 	"github.com/lightningnetwork/lnd/subscribe"
 	"github.com/lightningnetwork/lnd/ticker"
@@ -913,6 +915,16 @@ func (p *Brontide) taprootShutdownAllowed() bool {
 		p.LocalFeatures().HasFeature(lnwire.ShutdownAnySegwitOptional)
 }
 
+// rbfCoopCloseAllowed returns true if both parties have negotiated the new RBF
+// coop close feature.
+func (p *Brontide) rbfCoopCloseAllowed() bool {
+	return p.RemoteFeatures().HasFeature(
+		lnwire.RbfCoopCloseOptionalStaging,
+	) && p.LocalFeatures().HasFeature(
+		lnwire.RbfCoopCloseOptionalStaging,
+	)
+}
+
 // QuitSignal is a method that should return a channel which will be sent upon
 // or closed once the backing peer exits. This allows callers using the
 // interface to cancel any processing in the event the backing implementation
@@ -3314,7 +3326,8 @@ func (p *Brontide) createChanCloser(channel *lnwallet.LightningChannel,
 }
 
 // initNegotiateChanCloser initializes the channel closer for a channel that is
-// using the original "negotiation" based protocol.
+// using the original "negotiation" based protocol. This path is used when
+// we're the one initiating the channel close.
 //
 // TODO(roasbeef): can make a MsgEndpoint for existing handling logic to
 // further abstract.
@@ -3392,6 +3405,149 @@ func (p *Brontide) initNegotiateChanCloser(req *htlcswitch.ChanClose,
 	return nil
 }
 
+func chooseAddr(addr lnwire.DeliveryAddress) fn.Option[lnwire.DeliveryAddress] {
+	if len(addr) == 0 {
+		return fn.None[lnwire.DeliveryAddress]()
+	}
+
+	return fn.Some(addr)
+}
+
+// initRbfChanCloser initializes the channel closer for a channel that
+// is using the new RBF based co-op close protocol. This only creates the chan
+// closer, but doesn't attempt to trigger any manual state transitions.
+func (p *Brontide) initRbfChanCloser(
+	channel *lnwallet.LightningChannel) (*chancloser.RbfChanCloser, error) {
+
+	chanID := lnwire.NewChanIDFromOutPoint(channel.ChannelPoint())
+
+	link := p.fetchLinkFromKeyAndCid(chanID)
+
+	_, startingHeight, err := p.cfg.ChainIO.GetBestBlock()
+	if err != nil {
+		return nil, fmt.Errorf("cannot obtain best block: %w", err)
+	}
+
+	defaultFeePerKw, err := p.cfg.FeeEstimator.EstimateFeePerKW(
+		p.cfg.CoopCloseTargetConfs,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("unable to estimate fee: %w", err)
+	}
+
+	thawHeight, err := channel.AbsoluteThawHeight()
+	if err != nil {
+		return nil, fmt.Errorf("unable to get thaw height: %w", err)
+	}
+
+	peerPub := *p.IdentityKey()
+
+	msgMapper := chancloser.NewRbfMsgMapper(uint32(startingHeight), chanID, peerPub)
+
+	initialState := chancloser.ChannelActive{}
+
+	scid := channel.ZeroConfRealScid().UnwrapOr(
+		channel.ShortChanID(),
+	)
+
+	env := chancloser.Environment{
+		ChainParams:    p.cfg.Wallet.Cfg.NetParams,
+		ChanPeer:       peerPub,
+		ChanPoint:      channel.ChannelPoint(),
+		ChanID:         chanID,
+		Scid:           scid,
+		ChanType:       channel.ChanType(),
+		DefaultFeeRate: defaultFeePerKw.FeePerVByte(),
+		ThawHeight:     fn.Some(thawHeight),
+		RemoteUpfrontShutdown: chooseAddr(
+			channel.RemoteUpfrontShutdownScript(),
+		),
+		LocalUpfrontShutdown: chooseAddr(
+			channel.LocalUpfrontShutdownScript(),
+		),
+		NewDeliveryScript: func() (lnwire.DeliveryAddress, error) {
+			return p.genDeliveryScript()
+		},
+		FeeEstimator: &chancloser.SimpleCoopFeeEstimator{},
+		ChanObserver: newChanObserver(
+			channel, link, p.cfg.ChanStatusMgr,
+		),
+	}
+
+	spendEvent := protofsm.RegisterSpend[chancloser.ProtocolEvent]{
+		OutPoint:   channel.ChannelPoint(),
+		PkScript:   channel.FundingTxOut().PkScript,
+		HeightHint: scid.BlockHeight,
+		PostSpendEvent: fn.Some[chancloser.RbfSpendMapper](
+			chancloser.SpendMapper,
+		),
+	}
+
+	// TODO(roasbeef): edge case here to re-enable a channel before both
+	// shutdown sent?
+
+	daemonAdapters := NewLndDaemonAdapters(LndAdapterCfg{
+		MsgSender:     newPeerMsgSender(peerPub, p),
+		TxBroadcaster: p.cfg.Wallet,
+		ChainNotifier: p.cfg.ChainNotifier,
+	})
+
+	protoCfg := chancloser.RbfChanCloserCfg{
+		Daemon:       daemonAdapters,
+		InitialState: &initialState,
+		Env:          &env,
+		InitEvent:    fn.Some[protofsm.DaemonEvent](&spendEvent),
+		MsgMapper: fn.Some[protofsm.MsgMapper[chancloser.ProtocolEvent]]( //nolint:ll
+			msgMapper,
+		),
+	}
+
+	chanCloser := protofsm.NewStateMachine(protoCfg)
+
+	// Now that we've created the channel state machine, we'll register for
+	// a hook to be sent once the channel has been flushed.
+	link.OnFlushedOnce(func() {
+		commitState := channel.StateSnapshot()
+
+		ctx := context.Background()
+		chanCloser.SendEvent(ctx, &chancloser.ChannelFlushed{
+			ShutdownBalances: chancloser.ShutdownBalances{
+				LocalBalance:  commitState.LocalBalance,
+				RemoteBalance: commitState.RemoteBalance,
+			},
+		})
+	})
+
+	return &chanCloser, nil
+}
+
+// initAndStartRbfChanCloser initializes the channel closer for a channel that
+// is using the new RBF based co-op close protocol. This is called when we're
+// the one that's initiating the cooperative channel close.
+func (p *Brontide) initAndStartRbfChanCloser(req *htlcswitch.ChanClose,
+	channel *lnwallet.LightningChannel) error {
+
+	// TODO(roasbeef): either kick off sent shutdown or shutdown recv'd
+	//  * can also send the NoDangling in as new event?
+
+	// First, we'll create the channel closer for this channel.
+	chanCloser, err := p.initRbfChanCloser(channel)
+	if err != nil {
+		return err
+	}
+
+	// With the chan closer created, we'll now kick off the co-op close
+	// process by instructing it to send a shutdown message to the remote
+	// party.
+	ctx := context.Background()
+	chanCloser.SendEvent(ctx, &chancloser.SendShutdown{
+		IdealFeeRate: req.TargetFeePerKw.FeePerVByte(),
+		DeliveryAddr: chooseAddr(req.DeliveryScript),
+	})
+
+	return nil
+}
+
 // handleLocalCloseReq kicks-off the workflow to execute a cooperative or
 // forced unilateral closure of the channel initiated by a local subsystem.
 func (p *Brontide) handleLocalCloseReq(req *htlcswitch.ChanClose) {