mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-20 02:27:21 +01:00
1060 lines
37 KiB
Go
1060 lines
37 KiB
Go
package chancloser
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/fn"
|
|
"github.com/lightningnetwork/lnd/htlcswitch"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/labels"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
)
|
|
|
|
var (
|
|
// ErrChanAlreadyClosing is returned when a channel shutdown is attempted
|
|
// more than once.
|
|
ErrChanAlreadyClosing = fmt.Errorf("channel shutdown already initiated")
|
|
|
|
// ErrChanCloseNotFinished is returned when a caller attempts to access
|
|
// a field or function that is contingent on the channel closure negotiation
|
|
// already being completed.
|
|
ErrChanCloseNotFinished = fmt.Errorf("close negotiation not finished")
|
|
|
|
// ErrInvalidState is returned when the closing state machine receives a
|
|
// message while it is in an unknown state.
|
|
ErrInvalidState = fmt.Errorf("invalid state")
|
|
|
|
// ErrUpfrontShutdownScriptMismatch is returned when a peer or end user
|
|
// provides a cooperative close script which does not match the upfront
|
|
// shutdown script previously set for that party.
|
|
ErrUpfrontShutdownScriptMismatch = fmt.Errorf("shutdown script does not " +
|
|
"match upfront shutdown script")
|
|
|
|
// ErrProposalExceedsMaxFee is returned when as the initiator, the
|
|
// latest fee proposal sent by the responder exceed our max fee.
|
|
// responder.
|
|
ErrProposalExceedsMaxFee = fmt.Errorf("latest fee proposal exceeds " +
|
|
"max fee")
|
|
|
|
// ErrInvalidShutdownScript is returned when we receive an address from
|
|
// a peer that isn't either a p2wsh or p2tr address.
|
|
ErrInvalidShutdownScript = fmt.Errorf("invalid shutdown script")
|
|
)
|
|
|
|
// closeState represents all the possible states the channel closer state
|
|
// machine can be in. Each message will either advance to the next state, or
|
|
// remain at the current state. Once the state machine reaches a state of
|
|
// closeFinished, then negotiation is over.
|
|
type closeState uint8
|
|
|
|
const (
|
|
// closeIdle is the initial starting state. In this state, the state
|
|
// machine has been instantiated, but no state transitions have been
|
|
// attempted. If a state machine receives a message while in this state,
|
|
// then it is the responder to an initiated cooperative channel closure.
|
|
closeIdle closeState = iota
|
|
|
|
// closeShutdownInitiated is the state that's transitioned to once the
|
|
// initiator of a closing workflow sends the shutdown message. At this
|
|
// point, they're waiting for the remote party to respond with their own
|
|
// shutdown message. After which, they'll both enter the fee negotiation
|
|
// phase.
|
|
closeShutdownInitiated
|
|
|
|
// closeAwaitingFlush is the state that's transitioned to once both
|
|
// Shutdown messages have been exchanged but we are waiting for the
|
|
// HTLCs to clear out of the channel.
|
|
closeAwaitingFlush
|
|
|
|
// closeFeeNegotiation is the third, and most persistent state. Both
|
|
// parties enter this state after they've sent and received a shutdown
|
|
// message. During this phase, both sides will send monotonically
|
|
// increasing fee requests until one side accepts the last fee rate offered
|
|
// by the other party. In this case, the party will broadcast the closing
|
|
// transaction, and send the accepted fee to the remote party. This then
|
|
// causes a shift into the closeFinished state.
|
|
closeFeeNegotiation
|
|
|
|
// closeFinished is the final state of the state machine. In this state, a
|
|
// side has accepted a fee offer and has broadcast the valid closing
|
|
// transaction to the network. During this phase, the closing transaction
|
|
// becomes available for examination.
|
|
closeFinished
|
|
)
|
|
|
|
const (
|
|
// defaultMaxFeeMultiplier is a multiplier we'll apply to the ideal fee
|
|
// of the initiator, to decide when the negotiated fee is too high. By
|
|
// default, we want to bail out if we attempt to negotiate a fee that's
|
|
// 3x higher than our max fee.
|
|
defaultMaxFeeMultiplier = 3
|
|
)
|
|
|
|
// ChanCloseCfg holds all the items that a ChanCloser requires to carry out its
|
|
// duties.
|
|
type ChanCloseCfg struct {
|
|
// Channel is the channel that should be closed.
|
|
Channel Channel
|
|
|
|
// MusigSession is used to handle generating musig2 nonces, and also
|
|
// creating the proper set of closing options for taproot channels.
|
|
MusigSession MusigSession
|
|
|
|
// BroadcastTx broadcasts the passed transaction to the network.
|
|
BroadcastTx func(*wire.MsgTx, string) error
|
|
|
|
// DisableChannel disables a channel, resulting in it not being able to
|
|
// forward payments.
|
|
DisableChannel func(wire.OutPoint) error
|
|
|
|
// Disconnect will disconnect from the remote peer in this close.
|
|
Disconnect func() error
|
|
|
|
// MaxFee, is non-zero represents the highest fee that the initiator is
|
|
// willing to pay to close the channel.
|
|
MaxFee chainfee.SatPerKWeight
|
|
|
|
// ChainParams holds the parameters of the chain that we're active on.
|
|
ChainParams *chaincfg.Params
|
|
|
|
// Quit is a channel that should be sent upon in the occasion the state
|
|
// machine should cease all progress and shutdown.
|
|
Quit chan struct{}
|
|
|
|
// FeeEstimator is used to estimate the absolute starting co-op close
|
|
// fee.
|
|
FeeEstimator CoopFeeEstimator
|
|
}
|
|
|
|
// ChanCloser is a state machine that handles the cooperative channel closure
|
|
// procedure. This includes shutting down a channel, marking it ineligible for
|
|
// routing HTLC's, negotiating fees with the remote party, and finally
|
|
// broadcasting the fully signed closure transaction to the network.
|
|
type ChanCloser struct {
|
|
// state is the current state of the state machine.
|
|
state closeState
|
|
|
|
// cfg holds the configuration for this ChanCloser instance.
|
|
cfg ChanCloseCfg
|
|
|
|
// chanPoint is the full channel point of the target channel.
|
|
chanPoint wire.OutPoint
|
|
|
|
// cid is the full channel ID of the target channel.
|
|
cid lnwire.ChannelID
|
|
|
|
// negotiationHeight is the height that the fee negotiation begun at.
|
|
negotiationHeight uint32
|
|
|
|
// closingTx is the final, fully signed closing transaction. This will only
|
|
// be populated once the state machine shifts to the closeFinished state.
|
|
closingTx *wire.MsgTx
|
|
|
|
// idealFeeSat is the ideal fee that the state machine should initially
|
|
// offer when starting negotiation. This will be used as a baseline.
|
|
idealFeeSat btcutil.Amount
|
|
|
|
// maxFee is the highest fee the initiator is willing to pay to close
|
|
// out the channel. This is either a use specified value, or a default
|
|
// multiplier based of the initial starting ideal fee.
|
|
maxFee btcutil.Amount
|
|
|
|
// idealFeeRate is our ideal fee rate.
|
|
idealFeeRate chainfee.SatPerKWeight
|
|
|
|
// lastFeeProposal is the last fee that we proposed to the remote party.
|
|
// We'll use this as a pivot point to ratchet our next offer up, down, or
|
|
// simply accept the remote party's prior offer.
|
|
lastFeeProposal btcutil.Amount
|
|
|
|
// priorFeeOffers is a map that keeps track of all the proposed fees that
|
|
// we've offered during the fee negotiation. We use this map to cut the
|
|
// negotiation early if the remote party ever sends an offer that we've
|
|
// sent in the past. Once negotiation terminates, we can extract the prior
|
|
// signature of our accepted offer from this map.
|
|
//
|
|
// TODO(roasbeef): need to ensure if they broadcast w/ any of our prior
|
|
// sigs, we are aware of
|
|
priorFeeOffers map[btcutil.Amount]*lnwire.ClosingSigned
|
|
|
|
// closeReq is the initial closing request. This will only be populated if
|
|
// we're the initiator of this closing negotiation.
|
|
//
|
|
// TODO(roasbeef): abstract away
|
|
closeReq *htlcswitch.ChanClose
|
|
|
|
// localDeliveryScript is the script that we'll send our settled channel
|
|
// funds to.
|
|
localDeliveryScript []byte
|
|
|
|
// remoteDeliveryScript is the script that we'll send the remote party's
|
|
// settled channel funds to.
|
|
remoteDeliveryScript []byte
|
|
|
|
// locallyInitiated is true if we initiated the channel close.
|
|
locallyInitiated bool
|
|
}
|
|
|
|
// calcCoopCloseFee computes an "ideal" absolute co-op close fee given the
|
|
// delivery scripts of both parties and our ideal fee rate.
|
|
func calcCoopCloseFee(chanType channeldb.ChannelType,
|
|
localOutput, remoteOutput *wire.TxOut,
|
|
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
|
|
|
|
var weightEstimator input.TxWeightEstimator
|
|
|
|
if chanType.IsTaproot() {
|
|
weightEstimator.AddWitnessInput(
|
|
input.TaprootSignatureWitnessSize,
|
|
)
|
|
} else {
|
|
weightEstimator.AddWitnessInput(input.MultiSigWitnessSize)
|
|
}
|
|
|
|
// One of these outputs might be dust, so we'll skip adding it to our
|
|
// mock transaction, so the fees are more accurate.
|
|
if localOutput != nil {
|
|
weightEstimator.AddTxOutput(localOutput)
|
|
}
|
|
if remoteOutput != nil {
|
|
weightEstimator.AddTxOutput(remoteOutput)
|
|
}
|
|
|
|
totalWeight := int64(weightEstimator.Weight())
|
|
|
|
return idealFeeRate.FeeForWeight(totalWeight)
|
|
}
|
|
|
|
// SimpleCoopFeeEstimator is the default co-op close fee estimator. It assumes
|
|
// a normal segwit v0 channel, and that no outputs on the closing transaction
|
|
// are dust.
|
|
type SimpleCoopFeeEstimator struct {
|
|
}
|
|
|
|
// EstimateFee estimates an _absolute_ fee for a co-op close transaction given
|
|
// the local+remote tx outs (for the co-op close transaction), channel type,
|
|
// and ideal fee rate.
|
|
func (d *SimpleCoopFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
|
|
localTxOut, remoteTxOut *wire.TxOut,
|
|
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
|
|
|
|
return calcCoopCloseFee(chanType, localTxOut, remoteTxOut, idealFeeRate)
|
|
}
|
|
|
|
// NewChanCloser creates a new instance of the channel closure given the passed
|
|
// configuration, and delivery+fee preference. The final argument should only
|
|
// be populated iff, we're the initiator of this closing request.
|
|
func NewChanCloser(cfg ChanCloseCfg, deliveryScript []byte,
|
|
idealFeePerKw chainfee.SatPerKWeight, negotiationHeight uint32,
|
|
closeReq *htlcswitch.ChanClose, locallyInitiated bool) *ChanCloser {
|
|
|
|
cid := lnwire.NewChanIDFromOutPoint(cfg.Channel.ChannelPoint())
|
|
return &ChanCloser{
|
|
closeReq: closeReq,
|
|
state: closeIdle,
|
|
chanPoint: *cfg.Channel.ChannelPoint(),
|
|
cid: cid,
|
|
cfg: cfg,
|
|
negotiationHeight: negotiationHeight,
|
|
idealFeeRate: idealFeePerKw,
|
|
localDeliveryScript: deliveryScript,
|
|
priorFeeOffers: make(map[btcutil.Amount]*lnwire.ClosingSigned),
|
|
locallyInitiated: locallyInitiated,
|
|
}
|
|
}
|
|
|
|
// initFeeBaseline computes our ideal fee rate, and also the largest fee we'll
|
|
// accept given information about the delivery script of the remote party.
|
|
func (c *ChanCloser) initFeeBaseline() {
|
|
// Depending on if a balance ends up being dust or not, we'll pass a
|
|
// nil TxOut into the EstimateFee call which can handle it.
|
|
var localTxOut, remoteTxOut *wire.TxOut
|
|
if !c.cfg.Channel.LocalBalanceDust() {
|
|
localTxOut = &wire.TxOut{
|
|
PkScript: c.localDeliveryScript,
|
|
Value: 0,
|
|
}
|
|
}
|
|
if !c.cfg.Channel.RemoteBalanceDust() {
|
|
remoteTxOut = &wire.TxOut{
|
|
PkScript: c.remoteDeliveryScript,
|
|
Value: 0,
|
|
}
|
|
}
|
|
|
|
// Given the target fee-per-kw, we'll compute what our ideal _total_
|
|
// fee will be starting at for this fee negotiation.
|
|
c.idealFeeSat = c.cfg.FeeEstimator.EstimateFee(
|
|
0, localTxOut, remoteTxOut, c.idealFeeRate,
|
|
)
|
|
|
|
// When we're the initiator, we'll want to also factor in the highest
|
|
// fee we want to pay. This'll either be 3x the ideal fee, or the
|
|
// specified explicit max fee.
|
|
c.maxFee = c.idealFeeSat * defaultMaxFeeMultiplier
|
|
if c.cfg.MaxFee > 0 {
|
|
c.maxFee = c.cfg.FeeEstimator.EstimateFee(
|
|
0, localTxOut, remoteTxOut, c.cfg.MaxFee,
|
|
)
|
|
}
|
|
|
|
chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) "+
|
|
"is: %v sat (max_fee=%v sat)", c.cfg.Channel.ChannelPoint(),
|
|
int64(c.idealFeeSat), int64(c.maxFee))
|
|
}
|
|
|
|
// initChanShutdown begins the shutdown process by un-registering the channel,
|
|
// and creating a valid shutdown message to our target delivery address.
|
|
func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) {
|
|
// With both items constructed we'll now send the shutdown message for this
|
|
// particular channel, advertising a shutdown request to our desired
|
|
// closing script.
|
|
shutdown := lnwire.NewShutdown(c.cid, c.localDeliveryScript)
|
|
|
|
// If this is a taproot channel, then we'll need to also generate a
|
|
// nonce that'll be used sign the co-op close transaction offer.
|
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
|
firstClosingNonce, err := c.cfg.MusigSession.ClosingNonce()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
shutdown.ShutdownNonce = (*lnwire.ShutdownNonce)(
|
|
&firstClosingNonce.PubNonce,
|
|
)
|
|
|
|
chancloserLog.Infof("Initiating shutdown w/ nonce: %v",
|
|
spew.Sdump(firstClosingNonce.PubNonce))
|
|
}
|
|
|
|
// Before closing, we'll attempt to send a disable update for the
|
|
// channel. We do so before closing the channel as otherwise the
|
|
// current edge policy won't be retrievable from the graph.
|
|
if err := c.cfg.DisableChannel(c.chanPoint); err != nil {
|
|
chancloserLog.Warnf("Unable to disable channel %v on close: %v",
|
|
c.chanPoint, err)
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): sending shutdown message",
|
|
c.chanPoint)
|
|
|
|
return shutdown, nil
|
|
}
|
|
|
|
// ShutdownChan is the first method that's to be called by the initiator of the
|
|
// cooperative channel closure. This message returns the shutdown message to
|
|
// send to the remote party. Upon completion, we enter the
|
|
// closeShutdownInitiated phase as we await a response.
|
|
func (c *ChanCloser) ShutdownChan() (*lnwire.Shutdown, error) {
|
|
// If we attempt to shutdown the channel for the first time, and we're not
|
|
// in the closeIdle state, then the caller made an error.
|
|
if c.state != closeIdle {
|
|
return nil, ErrChanAlreadyClosing
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): initiating shutdown", c.chanPoint)
|
|
|
|
shutdownMsg, err := c.initChanShutdown()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// With the opening steps complete, we'll transition into the
|
|
// closeShutdownInitiated state. In this state, we'll wait until the other
|
|
// party sends their version of the shutdown message.
|
|
c.state = closeShutdownInitiated
|
|
|
|
// Finally, we'll return the shutdown message to the caller so it can send
|
|
// it to the remote peer.
|
|
return shutdownMsg, nil
|
|
}
|
|
|
|
// ClosingTx returns the fully signed, final closing transaction.
|
|
//
|
|
// NOTE: This transaction is only available if the state machine is in the
|
|
// closeFinished state.
|
|
func (c *ChanCloser) ClosingTx() (*wire.MsgTx, error) {
|
|
// If the state machine hasn't finished closing the channel, then we'll
|
|
// return an error as we haven't yet computed the closing tx.
|
|
if c.state != closeFinished {
|
|
return nil, ErrChanCloseNotFinished
|
|
}
|
|
|
|
return c.closingTx, nil
|
|
}
|
|
|
|
// CloseRequest returns the original close request that prompted the creation
|
|
// of the state machine.
|
|
//
|
|
// NOTE: This will only return a non-nil pointer if we were the initiator of
|
|
// the cooperative closure workflow.
|
|
func (c *ChanCloser) CloseRequest() *htlcswitch.ChanClose {
|
|
return c.closeReq
|
|
}
|
|
|
|
// Channel returns the channel stored in the config as a
|
|
// *lnwallet.LightningChannel.
|
|
//
|
|
// NOTE: This method will PANIC if the underlying channel implementation isn't
|
|
// the desired type.
|
|
func (c *ChanCloser) Channel() *lnwallet.LightningChannel {
|
|
// TODO(roasbeef): remove this
|
|
return c.cfg.Channel.(*lnwallet.LightningChannel)
|
|
}
|
|
|
|
// NegotiationHeight returns the negotiation height.
|
|
func (c *ChanCloser) NegotiationHeight() uint32 {
|
|
return c.negotiationHeight
|
|
}
|
|
|
|
// validateShutdownScript attempts to match and validate the script provided in
|
|
// our peer's shutdown message with the upfront shutdown script we have on
|
|
// record. For any script specified, we also make sure it matches our
|
|
// requirements. If no upfront shutdown script was set, we do not need to
|
|
// enforce option upfront shutdown, so the function returns early. If an
|
|
// upfront script is set, we check whether it matches the script provided by
|
|
// our peer. If they do not match, we use the disconnect function provided to
|
|
// disconnect from the peer.
|
|
func validateShutdownScript(disconnect func() error, upfrontScript,
|
|
peerScript lnwire.DeliveryAddress, netParams *chaincfg.Params) error {
|
|
|
|
// Either way, we'll make sure that the script passed meets our
|
|
// standards. The upfrontScript should have already been checked at an
|
|
// earlier stage, but we'll repeat the check here for defense in depth.
|
|
if len(upfrontScript) != 0 {
|
|
if !lnwallet.ValidateUpfrontShutdown(upfrontScript, netParams) {
|
|
return ErrInvalidShutdownScript
|
|
}
|
|
}
|
|
if len(peerScript) != 0 {
|
|
if !lnwallet.ValidateUpfrontShutdown(peerScript, netParams) {
|
|
return ErrInvalidShutdownScript
|
|
}
|
|
}
|
|
|
|
// If no upfront shutdown script was set, return early because we do
|
|
// not need to enforce closure to a specific script.
|
|
if len(upfrontScript) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// If an upfront shutdown script was provided, disconnect from the peer, as
|
|
// per BOLT 2, and return an error.
|
|
if !bytes.Equal(upfrontScript, peerScript) {
|
|
chancloserLog.Warnf("peer's script: %x does not match upfront "+
|
|
"shutdown script: %x", peerScript, upfrontScript)
|
|
|
|
// Disconnect from the peer because they have violated option upfront
|
|
// shutdown.
|
|
if err := disconnect(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return ErrUpfrontShutdownScriptMismatch
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReceiveShutdown takes a raw Shutdown message and uses it to try and advance
|
|
// the ChanCloser state machine, failing if it is coming in at an invalid time.
|
|
// If appropriate, it will also generate a Shutdown message of its own to send
|
|
// out to the peer. It is possible for this method to return None when no error
|
|
// occurred.
|
|
func (c *ChanCloser) ReceiveShutdown(
|
|
msg lnwire.Shutdown,
|
|
) (fn.Option[lnwire.Shutdown], error) {
|
|
|
|
noShutdown := fn.None[lnwire.Shutdown]()
|
|
|
|
switch c.state {
|
|
// If we're in the close idle state, and we're receiving a channel
|
|
// closure related message, then this indicates that we're on the
|
|
// receiving side of an initiated channel closure.
|
|
case closeIdle:
|
|
// As we're the responder to this shutdown (the other party
|
|
// wants to close), we'll check if this is a frozen channel or
|
|
// not. If the channel is frozen and we were not also the
|
|
// initiator of the channel opening, then we'll deny their close
|
|
// attempt.
|
|
chanInitiator := c.cfg.Channel.IsInitiator()
|
|
if !chanInitiator {
|
|
absoluteThawHeight, err :=
|
|
c.cfg.Channel.AbsoluteThawHeight()
|
|
if err != nil {
|
|
return noShutdown, err
|
|
}
|
|
if c.negotiationHeight < absoluteThawHeight {
|
|
return noShutdown, fmt.Errorf("initiator "+
|
|
"attempting to co-op close frozen "+
|
|
"ChannelPoint(%v) (current_height=%v, "+
|
|
"thaw_height=%v)", c.chanPoint,
|
|
c.negotiationHeight, absoluteThawHeight)
|
|
}
|
|
}
|
|
|
|
// If the remote node opened the channel with option upfront
|
|
// shutdown script, check that the script they provided matches.
|
|
if err := validateShutdownScript(
|
|
c.cfg.Disconnect,
|
|
c.cfg.Channel.RemoteUpfrontShutdownScript(),
|
|
msg.Address, c.cfg.ChainParams,
|
|
); err != nil {
|
|
return noShutdown, err
|
|
}
|
|
|
|
// Once we have checked that the other party has not violated
|
|
// option upfront shutdown we set their preference for delivery
|
|
// address. We'll use this when we craft the closure
|
|
// transaction.
|
|
c.remoteDeliveryScript = msg.Address
|
|
|
|
// We'll generate a shutdown message of our own to send across
|
|
// the wire.
|
|
localShutdown, err := c.initChanShutdown()
|
|
if err != nil {
|
|
return noShutdown, err
|
|
}
|
|
|
|
// If this is a taproot channel, then we'll want to stash the
|
|
// remote nonces so we can properly create a new musig
|
|
// session for signing.
|
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
|
if msg.ShutdownNonce == nil {
|
|
return noShutdown, fmt.Errorf("shutdown " +
|
|
"nonce not populated")
|
|
}
|
|
|
|
c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
|
|
PubNonce: *msg.ShutdownNonce,
|
|
})
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): responding to shutdown",
|
|
c.chanPoint)
|
|
|
|
// After the other party receives this message, we'll actually
|
|
// start the final stage of the closure process: fee
|
|
// negotiation. So we'll update our internal state to reflect
|
|
// this, so we can handle the next message sent.
|
|
c.state = closeAwaitingFlush
|
|
|
|
return fn.Some(*localShutdown), err
|
|
|
|
case closeShutdownInitiated:
|
|
// If the remote node opened the channel with option upfront
|
|
// shutdown script, check that the script they provided matches.
|
|
if err := validateShutdownScript(
|
|
c.cfg.Disconnect,
|
|
c.cfg.Channel.RemoteUpfrontShutdownScript(),
|
|
msg.Address, c.cfg.ChainParams,
|
|
); err != nil {
|
|
return noShutdown, err
|
|
}
|
|
|
|
// Now that we know this is a valid shutdown message and
|
|
// address, we'll record their preferred delivery closing
|
|
// script.
|
|
c.remoteDeliveryScript = msg.Address
|
|
|
|
// At this point, we can now start the fee negotiation state, by
|
|
// constructing and sending our initial signature for what we
|
|
// think the closing transaction should look like.
|
|
c.state = closeAwaitingFlush
|
|
|
|
// If this is a taproot channel, then we'll want to stash the
|
|
// local+remote nonces so we can properly create a new musig
|
|
// session for signing.
|
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
|
if msg.ShutdownNonce == nil {
|
|
return noShutdown, fmt.Errorf("shutdown " +
|
|
"nonce not populated")
|
|
}
|
|
|
|
c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
|
|
PubNonce: *msg.ShutdownNonce,
|
|
})
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): shutdown response "+
|
|
"received, entering fee negotiation", c.chanPoint)
|
|
|
|
return noShutdown, nil
|
|
|
|
default:
|
|
// Otherwise we are not in a state where we can accept this
|
|
// message.
|
|
return noShutdown, ErrInvalidState
|
|
}
|
|
}
|
|
|
|
// BeginNegotiation should be called when we have definitively reached a clean
|
|
// channel state and are ready to cooperatively arrive at a closing transaction.
|
|
// If it is our responsibility to kick off the negotiation, this method will
|
|
// generate a ClosingSigned message. If it is the remote's responsibility, then
|
|
// it will not. In either case it will transition the ChanCloser state machine
|
|
// to the negotiation phase wherein ClosingSigned messages are exchanged until
|
|
// a mutually agreeable result is achieved.
|
|
func (c *ChanCloser) BeginNegotiation() (
|
|
fn.Option[lnwire.ClosingSigned], error,
|
|
) {
|
|
|
|
noClosingSigned := fn.None[lnwire.ClosingSigned]()
|
|
|
|
switch c.state {
|
|
case closeAwaitingFlush:
|
|
// Now that we know their desired delivery script, we can
|
|
// compute what our max/ideal fee will be.
|
|
c.initFeeBaseline()
|
|
|
|
// Before continuing, mark the channel as cooperatively closed
|
|
// with a nil txn. Even though we haven't negotiated the final
|
|
// txn, this guarantees that our listchannels rpc will be
|
|
// externally consistent, and reflect that the channel is being
|
|
// shutdown by the time the closing request returns.
|
|
err := c.cfg.Channel.MarkCoopBroadcasted(
|
|
nil, c.locallyInitiated,
|
|
)
|
|
if err != nil {
|
|
return noClosingSigned, err
|
|
}
|
|
|
|
// At this point, we can now start the fee negotiation state, by
|
|
// constructing and sending our initial signature for what we
|
|
// think the closing transaction should look like.
|
|
c.state = closeFeeNegotiation
|
|
|
|
if !c.cfg.Channel.IsInitiator() {
|
|
return noClosingSigned, nil
|
|
}
|
|
|
|
// We'll craft our initial close proposal in order to keep the
|
|
// negotiation moving, but only if we're the initiator.
|
|
closingSigned, err := c.proposeCloseSigned(c.idealFeeSat)
|
|
if err != nil {
|
|
return noClosingSigned,
|
|
fmt.Errorf("unable to sign new co op "+
|
|
"close offer: %w", err)
|
|
}
|
|
|
|
return fn.Some(*closingSigned), nil
|
|
|
|
default:
|
|
return noClosingSigned, ErrInvalidState
|
|
}
|
|
}
|
|
|
|
// ReceiveClosingSigned is a method that should be called whenever we receive a
|
|
// ClosingSigned message from the wire. It may or may not return a ClosingSigned
|
|
// of our own to send back to the remote.
|
|
func (c *ChanCloser) ReceiveClosingSigned(
|
|
msg lnwire.ClosingSigned,
|
|
) (fn.Option[lnwire.ClosingSigned], error) {
|
|
|
|
noClosing := fn.None[lnwire.ClosingSigned]()
|
|
|
|
switch c.state {
|
|
case closeFeeNegotiation:
|
|
// If this is a taproot channel, then it MUST have a partial
|
|
// signature set at this point.
|
|
isTaproot := c.cfg.Channel.ChanType().IsTaproot()
|
|
if isTaproot && msg.PartialSig == nil {
|
|
return noClosing,
|
|
fmt.Errorf("partial sig not set " +
|
|
"for taproot chan")
|
|
}
|
|
|
|
isInitiator := c.cfg.Channel.IsInitiator()
|
|
|
|
// We'll compare the proposed total fee, to what we've proposed
|
|
// during the negotiations. If it doesn't match any of our
|
|
// prior offers, then we'll attempt to ratchet the fee closer
|
|
// to our ideal fee.
|
|
remoteProposedFee := msg.FeeSatoshis
|
|
|
|
_, feeMatchesOffer := c.priorFeeOffers[remoteProposedFee]
|
|
switch {
|
|
// For taproot channels, since nonces are involved, we can't do
|
|
// the existing co-op close negotiation process without going
|
|
// to a fully round based model. Rather than do this, we'll
|
|
// just accept the very first offer by the initiator.
|
|
case isTaproot && !isInitiator:
|
|
chancloserLog.Infof("ChannelPoint(%v) accepting "+
|
|
"initiator fee of %v", c.chanPoint,
|
|
remoteProposedFee)
|
|
|
|
// To auto-accept the initiators proposal, we'll just
|
|
// send back a signature w/ the same offer. We don't
|
|
// send the message here, as we can drop down and
|
|
// finalize the closure and broadcast, then echo back
|
|
// to Alice the final signature.
|
|
_, err := c.proposeCloseSigned(remoteProposedFee)
|
|
if err != nil {
|
|
return noClosing, fmt.Errorf("unable to sign "+
|
|
"new co op close offer: %w", err)
|
|
}
|
|
|
|
// Otherwise, if we are the initiator, and we just sent a
|
|
// signature for a taproot channel, then we'll ensure that the
|
|
// fee rate matches up exactly.
|
|
case isTaproot && isInitiator && !feeMatchesOffer:
|
|
return noClosing,
|
|
fmt.Errorf("fee rate for "+
|
|
"taproot channels was not accepted: "+
|
|
"sent %v, got %v",
|
|
c.idealFeeSat, remoteProposedFee)
|
|
|
|
// If we're the initiator of the taproot channel, and we had
|
|
// our fee echo'd back, then it's all good, and we can proceed
|
|
// with final broadcast.
|
|
case isTaproot && isInitiator && feeMatchesOffer:
|
|
break
|
|
|
|
// Otherwise, if this is a normal segwit v0 channel, and the
|
|
// fee doesn't match our offer, then we'll try to "negotiate" a
|
|
// new fee.
|
|
case !feeMatchesOffer:
|
|
// We'll now attempt to ratchet towards a fee deemed
|
|
// acceptable by both parties, factoring in our ideal
|
|
// fee rate, and the last proposed fee by both sides.
|
|
proposal := calcCompromiseFee(
|
|
c.chanPoint, c.idealFeeSat, c.lastFeeProposal,
|
|
remoteProposedFee,
|
|
)
|
|
if c.cfg.Channel.IsInitiator() && proposal > c.maxFee {
|
|
return noClosing, fmt.Errorf(
|
|
"%w: %v > %v",
|
|
ErrProposalExceedsMaxFee,
|
|
proposal, c.maxFee)
|
|
}
|
|
|
|
// With our new fee proposal calculated, we'll craft a
|
|
// new close signed signature to send to the other
|
|
// party so we can continue the fee negotiation
|
|
// process.
|
|
closeSigned, err := c.proposeCloseSigned(proposal)
|
|
if err != nil {
|
|
return noClosing, fmt.Errorf("unable to sign "+
|
|
"new co op close offer: %w", err)
|
|
}
|
|
|
|
// If the compromise fee doesn't match what the peer
|
|
// proposed, then we'll return this latest close signed
|
|
// message so we can continue negotiation.
|
|
if proposal != remoteProposedFee {
|
|
chancloserLog.Debugf("ChannelPoint(%v): close "+
|
|
"tx fee disagreement, continuing "+
|
|
"negotiation", c.chanPoint)
|
|
|
|
return fn.Some(*closeSigned), nil
|
|
}
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v) fee of %v accepted, "+
|
|
"ending negotiation", c.chanPoint, remoteProposedFee)
|
|
|
|
// Otherwise, we've agreed on a fee for the closing
|
|
// transaction! We'll craft the final closing transaction so we
|
|
// can broadcast it to the network.
|
|
var (
|
|
localSig, remoteSig input.Signature
|
|
closeOpts []lnwallet.ChanCloseOpt
|
|
err error
|
|
)
|
|
matchingSig := c.priorFeeOffers[remoteProposedFee]
|
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
|
muSession := c.cfg.MusigSession
|
|
localSig, remoteSig, closeOpts, err =
|
|
muSession.CombineClosingOpts(
|
|
*matchingSig.PartialSig,
|
|
*msg.PartialSig,
|
|
)
|
|
if err != nil {
|
|
return noClosing, err
|
|
}
|
|
} else {
|
|
localSig, err = matchingSig.Signature.ToSignature()
|
|
if err != nil {
|
|
return noClosing, err
|
|
}
|
|
remoteSig, err = msg.Signature.ToSignature()
|
|
if err != nil {
|
|
return noClosing, err
|
|
}
|
|
}
|
|
|
|
closeTx, _, err := c.cfg.Channel.CompleteCooperativeClose(
|
|
localSig, remoteSig, c.localDeliveryScript,
|
|
c.remoteDeliveryScript, remoteProposedFee, closeOpts...,
|
|
)
|
|
if err != nil {
|
|
return noClosing, err
|
|
}
|
|
c.closingTx = closeTx
|
|
|
|
// Before publishing the closing tx, we persist it to the
|
|
// database, such that it can be republished if something goes
|
|
// wrong.
|
|
err = c.cfg.Channel.MarkCoopBroadcasted(
|
|
closeTx, c.locallyInitiated,
|
|
)
|
|
if err != nil {
|
|
return noClosing, err
|
|
}
|
|
|
|
// With the closing transaction crafted, we'll now broadcast it
|
|
// to the network.
|
|
chancloserLog.Infof("Broadcasting cooperative close tx: %v",
|
|
newLogClosure(func() string {
|
|
return spew.Sdump(closeTx)
|
|
}),
|
|
)
|
|
|
|
// Create a close channel label.
|
|
chanID := c.cfg.Channel.ShortChanID()
|
|
closeLabel := labels.MakeLabel(
|
|
labels.LabelTypeChannelClose, &chanID,
|
|
)
|
|
|
|
if err := c.cfg.BroadcastTx(closeTx, closeLabel); err != nil {
|
|
return noClosing, err
|
|
}
|
|
|
|
// Finally, we'll transition to the closeFinished state, and
|
|
// also return the final close signed message we sent.
|
|
// Additionally, we return true for the second argument to
|
|
// indicate we're finished with the channel closing
|
|
// negotiation.
|
|
c.state = closeFinished
|
|
matchingOffer := c.priorFeeOffers[remoteProposedFee]
|
|
|
|
return fn.Some(*matchingOffer), nil
|
|
|
|
// If we received a message while in the closeFinished state, then this
|
|
// should only be the remote party echoing the last ClosingSigned
|
|
// message that we agreed on.
|
|
case closeFinished:
|
|
|
|
// There's no more to do as both sides should have already
|
|
// broadcast the closing transaction at this state.
|
|
return noClosing, nil
|
|
|
|
default:
|
|
return noClosing, ErrInvalidState
|
|
}
|
|
}
|
|
|
|
// ProcessCloseMsg attempts to process the next message in the closing series.
|
|
// This method will update the state accordingly and return two primary values:
|
|
// the next set of messages to be sent, and a bool indicating if the fee
|
|
// negotiation process has completed. If the second value is true, then this
|
|
// means the ChanCloser can be garbage collected.
|
|
func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|
bool, error) {
|
|
|
|
switch c.state {
|
|
default:
|
|
return nil, false, ErrInvalidState
|
|
}
|
|
}
|
|
|
|
// proposeCloseSigned attempts to propose a new signature for the closing
|
|
// transaction for a channel based on the prior fee negotiations and our current
|
|
// compromise fee.
|
|
func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSigned, error) {
|
|
var (
|
|
closeOpts []lnwallet.ChanCloseOpt
|
|
err error
|
|
)
|
|
|
|
// If this is a taproot channel, then we'll include the musig session
|
|
// generated for the next co-op close negotiation round.
|
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
|
closeOpts, err = c.cfg.MusigSession.ProposalClosingOpts()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
rawSig, _, _, err := c.cfg.Channel.CreateCloseProposal(
|
|
fee, c.localDeliveryScript, c.remoteDeliveryScript,
|
|
closeOpts...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We'll note our last signature and proposed fee so when the remote
|
|
// party responds we'll be able to decide if we've agreed on fees or
|
|
// not.
|
|
var (
|
|
parsedSig lnwire.Sig
|
|
partialSig *lnwire.PartialSigWithNonce
|
|
)
|
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
|
musig, ok := rawSig.(*lnwallet.MusigPartialSig)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected MusigPartialSig, "+
|
|
"got %T", rawSig)
|
|
}
|
|
|
|
partialSig = musig.ToWireSig()
|
|
} else {
|
|
parsedSig, err = lnwire.NewSigFromSignature(rawSig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
c.lastFeeProposal = fee
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): proposing fee of %v sat to "+
|
|
"close chan", c.chanPoint, int64(fee))
|
|
|
|
// We'll assemble a ClosingSigned message using this information and
|
|
// return it to the caller so we can kick off the final stage of the
|
|
// channel closure process.
|
|
closeSignedMsg := lnwire.NewClosingSigned(c.cid, fee, parsedSig)
|
|
|
|
// For musig2 channels, the main sig is blank, and instead we'll send
|
|
// over a partial signature which'll be combined once our offer is
|
|
// accepted.
|
|
if partialSig != nil {
|
|
closeSignedMsg.PartialSig = &partialSig.PartialSig
|
|
}
|
|
|
|
// We'll also save this close signed, in the case that the remote party
|
|
// accepts our offer. This way, we don't have to re-sign.
|
|
c.priorFeeOffers[fee] = closeSignedMsg
|
|
|
|
return closeSignedMsg, nil
|
|
}
|
|
|
|
// feeInAcceptableRange returns true if the passed remote fee is deemed to be
|
|
// in an "acceptable" range to our local fee. This is an attempt at a
|
|
// compromise and to ensure that the fee negotiation has a stopping point. We
|
|
// consider their fee acceptable if it's within 30% of our fee.
|
|
func feeInAcceptableRange(localFee, remoteFee btcutil.Amount) bool {
|
|
// If our offer is lower than theirs, then we'll accept their offer if it's
|
|
// no more than 30% *greater* than our current offer.
|
|
if localFee < remoteFee {
|
|
acceptableRange := localFee + ((localFee * 3) / 10)
|
|
return remoteFee <= acceptableRange
|
|
}
|
|
|
|
// If our offer is greater than theirs, then we'll accept their offer if
|
|
// it's no more than 30% *less* than our current offer.
|
|
acceptableRange := localFee - ((localFee * 3) / 10)
|
|
return remoteFee >= acceptableRange
|
|
}
|
|
|
|
// ratchetFee is our step function used to inch our fee closer to something
|
|
// that both sides can agree on. If up is true, then we'll attempt to increase
|
|
// our offered fee. Otherwise, if up is false, then we'll attempt to decrease
|
|
// our offered fee.
|
|
func ratchetFee(fee btcutil.Amount, up bool) btcutil.Amount {
|
|
// If we need to ratchet up, then we'll increase our fee by 10%.
|
|
if up {
|
|
return fee + ((fee * 1) / 10)
|
|
}
|
|
|
|
// Otherwise, we'll *decrease* our fee by 10%.
|
|
return fee - ((fee * 1) / 10)
|
|
}
|
|
|
|
// calcCompromiseFee performs the current fee negotiation algorithm, taking
|
|
// into consideration our ideal fee based on current fee environment, the fee
|
|
// we last proposed (if any), and the fee proposed by the peer.
|
|
func calcCompromiseFee(chanPoint wire.OutPoint, ourIdealFee, lastSentFee,
|
|
remoteFee btcutil.Amount) btcutil.Amount {
|
|
|
|
// TODO(roasbeef): take in number of rounds as well?
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): computing fee compromise, ideal="+
|
|
"%v, last_sent=%v, remote_offer=%v", chanPoint, int64(ourIdealFee),
|
|
int64(lastSentFee), int64(remoteFee))
|
|
|
|
// Otherwise, we'll need to attempt to make a fee compromise if this is the
|
|
// second round, and neither side has agreed on fees.
|
|
switch {
|
|
// If their proposed fee is identical to our ideal fee, then we'll go with
|
|
// that as we can short circuit the fee negotiation. Similarly, if we
|
|
// haven't sent an offer yet, we'll default to our ideal fee.
|
|
case ourIdealFee == remoteFee || lastSentFee == 0:
|
|
return ourIdealFee
|
|
|
|
// If the last fee we sent, is equal to the fee the remote party is
|
|
// offering, then we can simply return this fee as the negotiation is over.
|
|
case remoteFee == lastSentFee:
|
|
return lastSentFee
|
|
|
|
// If the fee the remote party is offering is less than the last one we
|
|
// sent, then we'll need to ratchet down in order to move our offer closer
|
|
// to theirs.
|
|
case remoteFee < lastSentFee:
|
|
// If the fee is lower, but still acceptable, then we'll just return
|
|
// this fee and end the negotiation.
|
|
if feeInAcceptableRange(lastSentFee, remoteFee) {
|
|
chancloserLog.Infof("ChannelPoint(%v): proposed remote fee is "+
|
|
"close enough, capitulating", chanPoint)
|
|
return remoteFee
|
|
}
|
|
|
|
// Otherwise, we'll ratchet the fee *down* using our current algorithm.
|
|
return ratchetFee(lastSentFee, false)
|
|
|
|
// If the fee the remote party is offering is greater than the last one we
|
|
// sent, then we'll ratchet up in order to ensure we terminate eventually.
|
|
case remoteFee > lastSentFee:
|
|
// If the fee is greater, but still acceptable, then we'll just return
|
|
// this fee in order to put an end to the negotiation.
|
|
if feeInAcceptableRange(lastSentFee, remoteFee) {
|
|
chancloserLog.Infof("ChannelPoint(%v): proposed remote fee is "+
|
|
"close enough, capitulating", chanPoint)
|
|
return remoteFee
|
|
}
|
|
|
|
// Otherwise, we'll ratchet the fee up using our current algorithm.
|
|
return ratchetFee(lastSentFee, true)
|
|
|
|
default:
|
|
// TODO(roasbeef): fail if their fee isn't in expected range
|
|
return remoteFee
|
|
}
|
|
}
|
|
|
|
// ParseUpfrontShutdownAddress attempts to parse an upfront shutdown address.
|
|
// If the address is empty, it returns nil. If it successfully decoded the
|
|
// address, it returns a script that pays out to the address.
|
|
func ParseUpfrontShutdownAddress(address string,
|
|
params *chaincfg.Params) (lnwire.DeliveryAddress, error) {
|
|
|
|
if len(address) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
addr, err := btcutil.DecodeAddress(
|
|
address, params,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid address: %v", err)
|
|
}
|
|
|
|
if !addr.IsForNet(params) {
|
|
return nil, fmt.Errorf("invalid address: %v is not a %s "+
|
|
"address", addr, params.Name)
|
|
}
|
|
|
|
return txscript.PayToAddrScript(addr)
|
|
}
|