mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-04 17:55:36 +01:00
In this commit, we enable a custom payer for the rbf coop close. This allows us to ensure that the party that started one side of the close flow pays the fees.
1045 lines
34 KiB
Go
1045 lines
34 KiB
Go
package chancloser
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/mempool"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lightningnetwork/lnd/fn/v2"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/labels"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnutils"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/protofsm"
|
|
"github.com/lightningnetwork/lnd/tlv"
|
|
)
|
|
|
|
// sendShutdownEvents is a helper function that returns a set of daemon events
|
|
// we need to emit when we decide that we should send a shutdown message. We'll
|
|
// also mark the channel as borked as well, as at this point, we no longer want
|
|
// to continue with normal operation.
|
|
func sendShutdownEvents(chanID lnwire.ChannelID, chanPoint wire.OutPoint,
|
|
deliveryAddr lnwire.DeliveryAddress, peerPub btcec.PublicKey,
|
|
postSendEvent fn.Option[ProtocolEvent],
|
|
chanState ChanStateObserver) (protofsm.DaemonEventSet, error) {
|
|
|
|
// We'll emit a daemon event that instructs the daemon to send out a
|
|
// new shutdown message to the remote peer.
|
|
msgsToSend := &protofsm.SendMsgEvent[ProtocolEvent]{
|
|
TargetPeer: peerPub,
|
|
Msgs: []lnwire.Message{&lnwire.Shutdown{
|
|
ChannelID: chanID,
|
|
Address: deliveryAddr,
|
|
}},
|
|
SendWhen: fn.Some(func() bool {
|
|
ok := chanState.NoDanglingUpdates()
|
|
if ok {
|
|
chancloserLog.Infof("ChannelPoint(%v): no "+
|
|
"dangling updates sending shutdown "+
|
|
"message", chanPoint)
|
|
}
|
|
|
|
return ok
|
|
}),
|
|
PostSendEvent: postSendEvent,
|
|
}
|
|
|
|
// If a close is already in process (we're in the RBF loop), then we
|
|
// can skip everything below, and just send out the shutdown message.
|
|
if chanState.FinalBalances().IsSome() {
|
|
return protofsm.DaemonEventSet{msgsToSend}, nil
|
|
}
|
|
|
|
// 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 := chanState.DisableChannel(); err != nil {
|
|
return nil, fmt.Errorf("unable to disable channel: %w", err)
|
|
}
|
|
|
|
// If we have a post-send event, then this means that we're the
|
|
// responder. We'll use this fact below to update state in the DB.
|
|
isInitiator := postSendEvent.IsNone()
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): disabling outgoing adds",
|
|
chanPoint)
|
|
|
|
// As we're about to send a shutdown, we'll disable adds in the
|
|
// outgoing direction.
|
|
if err := chanState.DisableOutgoingAdds(); err != nil {
|
|
return nil, fmt.Errorf("unable to disable outgoing "+
|
|
"adds: %w", err)
|
|
}
|
|
|
|
// To be able to survive a restart, we'll also write to disk
|
|
// information about the shutdown we're about to send out.
|
|
err := chanState.MarkShutdownSent(deliveryAddr, isInitiator)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to mark shutdown sent: %w", err)
|
|
}
|
|
|
|
chancloserLog.Debugf("ChannelPoint(%v): marking channel as borked",
|
|
chanPoint)
|
|
|
|
return protofsm.DaemonEventSet{msgsToSend}, nil
|
|
}
|
|
|
|
// validateShutdown is a helper function that validates that the shutdown has a
|
|
// proper delivery script, and can be sent based on the current thaw height of
|
|
// the channel.
|
|
func validateShutdown(chanThawHeight fn.Option[uint32],
|
|
upfrontAddr fn.Option[lnwire.DeliveryAddress],
|
|
msg *ShutdownReceived, chanPoint wire.OutPoint,
|
|
chainParams chaincfg.Params) error {
|
|
|
|
// If we've received a shutdown message, and we have a thaw height,
|
|
// then we need to make sure that the channel can now be co-op closed.
|
|
err := fn.MapOptionZ(chanThawHeight, func(thawHeight uint32) error {
|
|
// If the current height is below the thaw height, then we'll
|
|
// reject the shutdown message as we can't yet co-op close the
|
|
// channel.
|
|
if msg.BlockHeight < thawHeight {
|
|
return fmt.Errorf("initiator attempting to "+
|
|
"co-op close frozen ChannelPoint(%v) "+
|
|
"(current_height=%v, thaw_height=%v)",
|
|
chanPoint, msg.BlockHeight,
|
|
thawHeight)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Next, we'll verify that the remote party is sending the expected
|
|
// shutdown script.
|
|
return fn.MapOption(func(addr lnwire.DeliveryAddress) error {
|
|
return validateShutdownScript(
|
|
addr, msg.ShutdownScript, &chainParams,
|
|
)
|
|
})(upfrontAddr).UnwrapOr(nil)
|
|
}
|
|
|
|
// ProcessEvent takes a protocol event, and implements a state transition for
|
|
// the state. From this state, we can receive two possible incoming events:
|
|
// SendShutdown and ShutdownReceived. Both of these will transition us to the
|
|
// ChannelFlushing state.
|
|
func (c *ChannelActive) ProcessEvent(event ProtocolEvent, env *Environment,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
switch msg := event.(type) {
|
|
// If we get a confirmation, then a prior transaction we broadcasted
|
|
// has confirmed, so we can move to our terminal state early.
|
|
case *SpendEvent:
|
|
return &CloseStateTransition{
|
|
NextState: &CloseFin{
|
|
ConfirmedTx: msg.Tx,
|
|
},
|
|
}, nil
|
|
|
|
// If we receive the SendShutdown event, then we'll send our shutdown
|
|
// with a special SendPredicate, then go to the ShutdownPending where
|
|
// we'll wait for the remote to send their shutdown.
|
|
case *SendShutdown:
|
|
// If we have an upfront shutdown addr or a delivery addr then
|
|
// we'll use that. Otherwise, we'll generate a new delivery
|
|
// addr.
|
|
shutdownScript, err := env.LocalUpfrontShutdown.Alt(
|
|
msg.DeliveryAddr,
|
|
).UnwrapOrFuncErr(env.NewDeliveryScript)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We'll emit some daemon events to send the shutdown message
|
|
// and disable the channel on the network level. In this case,
|
|
// we don't need a post send event as receive their shutdown is
|
|
// what'll move us beyond the ShutdownPending state.
|
|
daemonEvents, err := sendShutdownEvents(
|
|
env.ChanID, env.ChanPoint, shutdownScript,
|
|
env.ChanPeer, fn.None[ProtocolEvent](),
|
|
env.ChanObserver,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): sending shutdown msg, "+
|
|
"delivery_script=%v", env.ChanPoint, shutdownScript)
|
|
|
|
// From here, we'll transition to the shutdown pending state. In
|
|
// this state we await their shutdown message (self loop), then
|
|
// also the flushing event.
|
|
return &CloseStateTransition{
|
|
NextState: &ShutdownPending{
|
|
IdealFeeRate: fn.Some(msg.IdealFeeRate),
|
|
ShutdownScripts: ShutdownScripts{
|
|
LocalDeliveryScript: shutdownScript,
|
|
},
|
|
},
|
|
NewEvents: fn.Some(RbfEvent{
|
|
ExternalEvents: daemonEvents,
|
|
}),
|
|
}, nil
|
|
|
|
// When we receive a shutdown from the remote party, we'll validate the
|
|
// shutdown message, then transition to the ShutdownPending state. We'll
|
|
// also emit similar events like the above to send out shutdown, and
|
|
// also disable the channel.
|
|
case *ShutdownReceived:
|
|
chancloserLog.Infof("ChannelPoint(%v): received shutdown msg")
|
|
|
|
// Validate that they can send the message now, and also that
|
|
// they haven't violated their commitment to a prior upfront
|
|
// shutdown addr.
|
|
err := validateShutdown(
|
|
env.ThawHeight, env.RemoteUpfrontShutdown, msg,
|
|
env.ChanPoint, env.ChainParams,
|
|
)
|
|
if err != nil {
|
|
chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
|
|
"shutdown attempt: %v", err)
|
|
|
|
return nil, err
|
|
}
|
|
|
|
// If we have an upfront shutdown addr we'll use that,
|
|
// otherwise, we'll generate a new delivery script.
|
|
shutdownAddr, err := env.LocalUpfrontShutdown.UnwrapOrFuncErr(
|
|
env.NewDeliveryScript,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): sending shutdown msg "+
|
|
"at next clean commit state", env.ChanPoint)
|
|
|
|
// Now that we know the shutdown message is valid, we'll obtain
|
|
// the set of daemon events we need to emit. We'll also specify
|
|
// that once the message has actually been sent, that we
|
|
// generate receive an input event of a ShutdownComplete.
|
|
daemonEvents, err := sendShutdownEvents(
|
|
env.ChanID, env.ChanPoint, shutdownAddr,
|
|
env.ChanPeer,
|
|
fn.Some[ProtocolEvent](&ShutdownComplete{}),
|
|
env.ChanObserver,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): disabling incoming adds",
|
|
env.ChanPoint)
|
|
|
|
// We just received a shutdown, so we'll disable the adds in
|
|
// the outgoing direction.
|
|
if err := env.ChanObserver.DisableIncomingAdds(); err != nil {
|
|
return nil, fmt.Errorf("unable to disable incoming "+
|
|
"adds: %w", err)
|
|
}
|
|
|
|
remoteAddr := msg.ShutdownScript
|
|
|
|
return &CloseStateTransition{
|
|
NextState: &ShutdownPending{
|
|
ShutdownScripts: ShutdownScripts{
|
|
LocalDeliveryScript: shutdownAddr,
|
|
RemoteDeliveryScript: remoteAddr,
|
|
},
|
|
},
|
|
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
|
ExternalEvents: daemonEvents,
|
|
}),
|
|
}, nil
|
|
|
|
// Any other messages in this state will result in an error, as this is
|
|
// an undefined state transition.
|
|
default:
|
|
return nil, fmt.Errorf("%w: received %T while in ChannelActive",
|
|
ErrInvalidStateTransition, msg)
|
|
}
|
|
}
|
|
|
|
// ProcessEvent takes a protocol event, and implements a state transition for
|
|
// the state. Our path to this state will determine the set of valid events. If
|
|
// we were the one that sent the shutdown, then we'll just wait on the
|
|
// ShutdownReceived event. Otherwise, we received the shutdown, and can move
|
|
// forward once we receive the ShutdownComplete event. Receiving
|
|
// ShutdownComplete means that we've sent our shutdown, as this was specified
|
|
// as a post send event.
|
|
func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
switch msg := event.(type) {
|
|
// If we get a confirmation, then a prior transaction we broadcasted
|
|
// has confirmed, so we can move to our terminal state early.
|
|
case *SpendEvent:
|
|
return &CloseStateTransition{
|
|
NextState: &CloseFin{
|
|
ConfirmedTx: msg.Tx,
|
|
},
|
|
}, nil
|
|
|
|
// When we receive a shutdown from the remote party, we'll validate the
|
|
// shutdown message, then transition to the ChannelFlushing state.
|
|
case *ShutdownReceived:
|
|
chancloserLog.Infof("ChannelPoint(%v): received shutdown msg",
|
|
env.ChanPoint)
|
|
|
|
// Validate that they can send the message now, and also that
|
|
// they haven't violated their commitment to a prior upfront
|
|
// shutdown addr.
|
|
err := validateShutdown(
|
|
env.ThawHeight, env.RemoteUpfrontShutdown, msg,
|
|
env.ChanPoint, env.ChainParams,
|
|
)
|
|
if err != nil {
|
|
chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
|
|
"shutdown attempt: %v", err)
|
|
|
|
return nil, err
|
|
}
|
|
|
|
// If the channel is *already* flushed, and the close is
|
|
// go straight into negotiation, as this is the RBF loop.
|
|
// already in progress, then we can skip the flushing state and
|
|
var eventsToEmit fn.Option[protofsm.EmittedEvent[ProtocolEvent]]
|
|
finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
|
|
unknownBalance,
|
|
)
|
|
if finalBalances != unknownBalance {
|
|
channelFlushed := ProtocolEvent(&ChannelFlushed{
|
|
ShutdownBalances: finalBalances,
|
|
})
|
|
eventsToEmit = fn.Some(RbfEvent{
|
|
InternalEvent: []ProtocolEvent{
|
|
channelFlushed,
|
|
},
|
|
})
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): disabling incoming adds",
|
|
env.ChanPoint)
|
|
|
|
// We just received a shutdown, so we'll disable the adds in
|
|
// the outgoing direction.
|
|
if err := env.ChanObserver.DisableIncomingAdds(); err != nil {
|
|
return nil, fmt.Errorf("unable to disable incoming "+
|
|
"adds: %w", err)
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
|
|
"be flushed...", env.ChanPoint)
|
|
|
|
// We transition to the ChannelFlushing state, where we await
|
|
// the ChannelFlushed event.
|
|
return &CloseStateTransition{
|
|
NextState: &ChannelFlushing{
|
|
IdealFeeRate: s.IdealFeeRate,
|
|
ShutdownScripts: ShutdownScripts{
|
|
LocalDeliveryScript: s.LocalDeliveryScript, //nolint:ll
|
|
RemoteDeliveryScript: msg.ShutdownScript, //nolint:ll
|
|
},
|
|
},
|
|
NewEvents: eventsToEmit,
|
|
}, nil
|
|
|
|
// If we get this message, then this means that we were finally able to
|
|
// send out shutdown after receiving it from the remote party. We'll
|
|
// now transition directly to the ChannelFlushing state.
|
|
case *ShutdownComplete:
|
|
chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
|
|
"be flushed...", env.ChanPoint)
|
|
|
|
// If the channel is *already* flushed, and the close is
|
|
// already in progress, then we can skip the flushing state and
|
|
// go straight into negotiation, as this is the RBF loop.
|
|
var eventsToEmit fn.Option[protofsm.EmittedEvent[ProtocolEvent]]
|
|
finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
|
|
unknownBalance,
|
|
)
|
|
if finalBalances != unknownBalance {
|
|
channelFlushed := ProtocolEvent(&ChannelFlushed{
|
|
ShutdownBalances: finalBalances,
|
|
})
|
|
eventsToEmit = fn.Some(RbfEvent{
|
|
InternalEvent: []ProtocolEvent{
|
|
channelFlushed,
|
|
},
|
|
})
|
|
}
|
|
|
|
// From here, we'll transition to the channel flushing state.
|
|
// We'll stay here until we receive the ChannelFlushed event.
|
|
return &CloseStateTransition{
|
|
NextState: &ChannelFlushing{
|
|
IdealFeeRate: s.IdealFeeRate,
|
|
ShutdownScripts: s.ShutdownScripts,
|
|
},
|
|
NewEvents: eventsToEmit,
|
|
}, nil
|
|
|
|
// Any other messages in this state will result in an error, as this is
|
|
// an undefined state transition.
|
|
default:
|
|
return nil, fmt.Errorf("%w: received %T while in "+
|
|
"ShutdownPending", ErrInvalidStateTransition, msg)
|
|
}
|
|
}
|
|
|
|
// ProcessEvent takes a new protocol event, and figures out if we can
|
|
// transition to the next state, or just loop back upon ourself. If we receive
|
|
// a ShutdownReceived event, then we'll stay in the ChannelFlushing state, as
|
|
// we haven't yet fully cleared the channel. Otherwise, we can move to the
|
|
// CloseReady state which'll being the channel closing process.
|
|
func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent, env *Environment,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
switch msg := event.(type) {
|
|
// If we get a confirmation, then a prior transaction we broadcasted
|
|
// has confirmed, so we can move to our terminal state early.
|
|
case *SpendEvent:
|
|
return &CloseStateTransition{
|
|
NextState: &CloseFin{
|
|
ConfirmedTx: msg.Tx,
|
|
},
|
|
}, nil
|
|
|
|
// If we get an OfferReceived event, then the channel is flushed from
|
|
// the PoV of the remote party. However, due to propagation delay or
|
|
// concurrency, we may not have received the ChannelFlushed event yet.
|
|
// In this case, we'll stash the event and wait for the ChannelFlushed
|
|
// event.
|
|
case *OfferReceivedEvent:
|
|
chancloserLog.Infof("ChannelPoint(%v): received remote offer "+
|
|
"early, stashing...", env.ChanPoint)
|
|
|
|
c.EarlyRemoteOffer = fn.Some(*msg)
|
|
|
|
// TODO(roasbeef): unit test!
|
|
// * actually do this ^
|
|
|
|
// We'll perform a noop update so we can wait for the actual
|
|
// channel flushed event.
|
|
return &CloseStateTransition{
|
|
NextState: c,
|
|
}, nil
|
|
|
|
// If we receive the ChannelFlushed event, then the coast is clear so
|
|
// we'll now morph into the dual peer state so we can handle any
|
|
// messages needed to drive forward the close process.
|
|
case *ChannelFlushed:
|
|
// Both the local and remote losing negotiation needs the terms
|
|
// we'll be using to close the channel, so we'll create them
|
|
// here.
|
|
closeTerms := CloseChannelTerms{
|
|
ShutdownScripts: c.ShutdownScripts,
|
|
ShutdownBalances: msg.ShutdownBalances,
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): channel flushed! "+
|
|
"proceeding with co-op close", env.ChanPoint)
|
|
|
|
// Now that the channel has been flushed, we'll mark on disk
|
|
// that we're approaching the point of no return where we'll
|
|
// send a new signature to the remote party.
|
|
//
|
|
// TODO(roasbeef): doesn't actually matter if initiator here?
|
|
if msg.FreshFlush {
|
|
err := env.ChanObserver.MarkCoopBroadcasted(nil, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If an ideal fee rate was specified, then we'll use that,
|
|
// otherwise we'll fall back to the default value given in the
|
|
// env.
|
|
idealFeeRate := c.IdealFeeRate.UnwrapOr(env.DefaultFeeRate)
|
|
|
|
// We'll then use that fee rate to determine the absolute fee
|
|
// we'd propose.
|
|
//
|
|
// TODO(roasbeef): need to sign the 3 diff versions of this?
|
|
localTxOut, remoteTxOut := closeTerms.DeriveCloseTxOuts()
|
|
absoluteFee := env.FeeEstimator.EstimateFee(
|
|
env.ChanType, localTxOut, remoteTxOut,
|
|
idealFeeRate.FeePerKWeight(),
|
|
)
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): using ideal_fee=%v, "+
|
|
"absolute_fee=%v", env.ChanPoint, idealFeeRate,
|
|
absoluteFee)
|
|
|
|
var (
|
|
internalEvents []ProtocolEvent
|
|
newEvents fn.Option[RbfEvent]
|
|
)
|
|
|
|
// If we received a remote offer early from the remote party,
|
|
// then we'll add that to the set of internal events to emit.
|
|
c.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
|
|
internalEvents = append(internalEvents, &offer)
|
|
})
|
|
|
|
// Only if we have enough funds to pay for the fees do we need
|
|
// to emit a localOfferSign event.
|
|
//
|
|
// TODO(roasbeef): also only proceed if was higher than fee in
|
|
// last round?
|
|
if closeTerms.LocalCanPayFees(absoluteFee) {
|
|
// Each time we go into this negotiation flow, we'll
|
|
// kick off our local state with a new close attempt.
|
|
// So we'll emit a internal event to drive forward that
|
|
// part of the state.
|
|
localOfferSign := ProtocolEvent(&SendOfferEvent{
|
|
TargetFeeRate: idealFeeRate,
|
|
})
|
|
internalEvents = append(internalEvents, localOfferSign)
|
|
} else {
|
|
chancloserLog.Infof("ChannelPoint(%v): unable to pay "+
|
|
"fees with local balance, skipping "+
|
|
"closing_complete", env.ChanPoint)
|
|
}
|
|
|
|
if len(internalEvents) > 0 {
|
|
newEvents = fn.Some(RbfEvent{
|
|
InternalEvent: internalEvents,
|
|
})
|
|
}
|
|
|
|
return &CloseStateTransition{
|
|
NextState: &ClosingNegotiation{
|
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
|
Local: &LocalCloseStart{
|
|
CloseChannelTerms: closeTerms,
|
|
},
|
|
Remote: &RemoteCloseStart{
|
|
CloseChannelTerms: closeTerms,
|
|
},
|
|
},
|
|
},
|
|
NewEvents: newEvents,
|
|
}, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("%w: received %T while in "+
|
|
"ChannelFlushing", ErrInvalidStateTransition, msg)
|
|
}
|
|
}
|
|
|
|
// processNegotiateEvent is a helper function that processes a new event to
|
|
// local channel state once we're in the ClosingNegotiation state.
|
|
func processNegotiateEvent(c *ClosingNegotiation, event ProtocolEvent,
|
|
env *Environment, chanPeer lntypes.ChannelParty,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
targetPeerState := c.PeerState.GetForParty(chanPeer)
|
|
|
|
// Drive forward the remote state based on the next event.
|
|
transition, err := targetPeerState.ProcessEvent(
|
|
event, env,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nextPeerState, ok := transition.NextState.(AsymmetricPeerState) //nolint:ll
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected %T to be "+
|
|
"AsymmetricPeerState", transition.NextState)
|
|
}
|
|
|
|
// Make a copy of the input state, then update the peer state of the
|
|
// proper party.
|
|
newPeerState := *c
|
|
newPeerState.PeerState.SetForParty(chanPeer, nextPeerState)
|
|
|
|
return &CloseStateTransition{
|
|
NextState: &newPeerState,
|
|
NewEvents: transition.NewEvents,
|
|
}, nil
|
|
}
|
|
|
|
// ProcessEvent drives forward the composite states for the local and remote
|
|
// party in response to new events. From this state, we'll continue to drive
|
|
// forward the local and remote states until we arrive at the StateFin stage,
|
|
// or we loop back up to the ShutdownPending state.
|
|
func (c *ClosingNegotiation) ProcessEvent(event ProtocolEvent, env *Environment,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
// There're two classes of events that can break us out of this state:
|
|
// we receive a confirmation event, or we receive a signal to restart
|
|
// the co-op close process.
|
|
switch msg := event.(type) {
|
|
// If we get a confirmation, then the spend request we issued when we
|
|
// were leaving the ChannelFlushing state has been confirmed. We'll
|
|
// now transition to the StateFin state.
|
|
case *SpendEvent:
|
|
return &CloseStateTransition{
|
|
NextState: &CloseFin{
|
|
ConfirmedTx: msg.Tx,
|
|
},
|
|
}, nil
|
|
|
|
// Otherwise, if we receive a shutdown, or receive an event to send a
|
|
// shutdown, then we'll go back up to the ChannelActive state, and have
|
|
// it handle this event by emitting an internal event.
|
|
//
|
|
// TODO(roasbeef): both will have fee rate specified, so ok?
|
|
case *ShutdownReceived, *SendShutdown:
|
|
chancloserLog.Infof("ChannelPoint(%v): RBF case triggered, "+
|
|
"restarting negotiation", env.ChanPoint)
|
|
|
|
return &CloseStateTransition{
|
|
NextState: &ChannelActive{},
|
|
NewEvents: fn.Some(RbfEvent{
|
|
InternalEvent: []ProtocolEvent{event},
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
// If we get to this point, then we have an event that'll drive forward
|
|
// the negotiation process. Based on the event, we'll figure out which
|
|
// state we'll be modifying.
|
|
switch {
|
|
case c.PeerState.GetForParty(lntypes.Local).ShouldRouteTo(event):
|
|
chancloserLog.Infof("ChannelPoint(%v): routing %T to local "+
|
|
"chan state", env.ChanPoint, event)
|
|
|
|
// Drive forward the local state based on the next event.
|
|
return processNegotiateEvent(c, event, env, lntypes.Local)
|
|
|
|
case c.PeerState.GetForParty(lntypes.Remote).ShouldRouteTo(event):
|
|
chancloserLog.Infof("ChannelPoint(%v): routing %T to remote "+
|
|
"chan state", env.ChanPoint, event)
|
|
|
|
// Drive forward the remote state based on the next event.
|
|
return processNegotiateEvent(c, event, env, lntypes.Remote)
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: received %T while in ClosingNegotiation",
|
|
ErrInvalidStateTransition, event)
|
|
}
|
|
|
|
// newSigTlv is a helper function that returns a new optional TLV sig field for
|
|
// the parametrized tlv.TlvType value.
|
|
func newSigTlv[T tlv.TlvType](s lnwire.Sig) tlv.OptionalRecordT[T, lnwire.Sig] {
|
|
return tlv.SomeRecordT(tlv.NewRecordT[T](s))
|
|
}
|
|
|
|
// ProcessEvent implements the event processing to kick off the process of
|
|
// obtaining a new (possibly RBF'd) signature for our commitment transaction.
|
|
func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
switch msg := event.(type) { //nolint:gocritic
|
|
// If we receive a SendOfferEvent, then we'll use the specified fee
|
|
// rate to generate for the closing transaction with our ideal fee
|
|
// rate.
|
|
case *SendOfferEvent:
|
|
// First, we'll figure out the absolute fee rate we should pay
|
|
// given the state of the local/remote outputs.
|
|
localTxOut, remoteTxOut := l.DeriveCloseTxOuts()
|
|
absoluteFee := env.FeeEstimator.EstimateFee(
|
|
env.ChanType, localTxOut, remoteTxOut,
|
|
msg.TargetFeeRate.FeePerKWeight(),
|
|
)
|
|
|
|
// Now that we know what fee we want to pay, we'll create a new
|
|
// signature over our co-op close transaction. For our
|
|
// proposals, we'll just always use the known RBF sequence
|
|
// value.
|
|
localScript := l.LocalDeliveryScript
|
|
rawSig, closeTx, closeBalance, err := env.CloseSigner.CreateCloseProposal( //nolint:ll
|
|
absoluteFee, localScript, l.RemoteDeliveryScript,
|
|
lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
|
|
lnwallet.WithCustomPayer(lntypes.Local),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wireSig, err := lnwire.NewSigFromSignature(rawSig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chancloserLog.Infof("closing w/ local_addr=%x, "+
|
|
"remote_addr=%x, fee=%v", localScript[:],
|
|
l.RemoteDeliveryScript[:], absoluteFee)
|
|
|
|
chancloserLog.Infof("proposing closing_tx=%v",
|
|
spew.Sdump(closeTx))
|
|
|
|
// Now that we have our signature, we'll set the proper
|
|
// closingSigs field based on if the remote party's output is
|
|
// dust or not.
|
|
var closingSigs lnwire.ClosingSigs
|
|
switch {
|
|
// If the remote party's output is dust, then we'll set the
|
|
// CloserNoClosee field.
|
|
case remoteTxOut == nil:
|
|
closingSigs.CloserNoClosee = newSigTlv[tlv.TlvType1](
|
|
wireSig,
|
|
)
|
|
|
|
// If after paying for fees, our balance is below dust, then
|
|
// we'll set the NoCloserClosee field.
|
|
case closeBalance < lnwallet.DustLimitForSize(len(localScript)):
|
|
closingSigs.NoCloserClosee = newSigTlv[tlv.TlvType2](
|
|
wireSig,
|
|
)
|
|
|
|
// Otherwise, we'll set the CloserAndClosee field.
|
|
//
|
|
// TODO(roasbeef): should actually set both??
|
|
default:
|
|
closingSigs.CloserAndClosee = newSigTlv[tlv.TlvType3](
|
|
wireSig,
|
|
)
|
|
}
|
|
|
|
// Now that we have our sig, we'll emit a daemon event to send
|
|
// it to the remote party, then transition to the
|
|
// LocalOfferSent state.
|
|
//
|
|
// TODO(roasbeef): type alias for protocol event
|
|
sendEvent := protofsm.DaemonEventSet{&protofsm.SendMsgEvent[ProtocolEvent]{ //nolint:ll
|
|
TargetPeer: env.ChanPeer,
|
|
// TODO(roasbeef): mew new func
|
|
Msgs: []lnwire.Message{&lnwire.ClosingComplete{
|
|
ChannelID: env.ChanID,
|
|
FeeSatoshis: absoluteFee,
|
|
LockTime: env.BlockHeight,
|
|
ClosingSigs: closingSigs,
|
|
}},
|
|
}}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): sending closing sig "+
|
|
"to remote party, fee_sats=%v", env.ChanPoint,
|
|
absoluteFee)
|
|
|
|
return &CloseStateTransition{
|
|
NextState: &LocalOfferSent{
|
|
ProposedFee: absoluteFee,
|
|
LocalSig: wireSig,
|
|
CloseChannelTerms: l.CloseChannelTerms,
|
|
},
|
|
NewEvents: fn.Some(RbfEvent{
|
|
ExternalEvents: sendEvent,
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: received %T while in LocalCloseStart",
|
|
ErrInvalidStateTransition, event)
|
|
}
|
|
|
|
// extractSig extracts the expected signature from the closing sig message.
|
|
// Only one of them should actually be populated as the closing sig message is
|
|
// sent in response to a ClosingComplete message, it should only sign the same
|
|
// version of the co-op close tx as the sender did.
|
|
func extractSig(msg lnwire.ClosingSig) fn.Result[lnwire.Sig] {
|
|
// First, we'll validate that only one signature is included in their
|
|
// response to our initial offer. If not, then we'll exit here, and
|
|
// trigger a recycle of the connection.
|
|
sigInts := []bool{
|
|
msg.CloserNoClosee.IsSome(), msg.NoCloserClosee.IsSome(),
|
|
msg.CloserAndClosee.IsSome(),
|
|
}
|
|
numSigs := fn.Foldl(0, sigInts, func(acc int, sigInt bool) int {
|
|
if sigInt {
|
|
return acc + 1
|
|
}
|
|
|
|
return acc
|
|
})
|
|
if numSigs != 1 {
|
|
return fn.Errf[lnwire.Sig]("%w: only one sig should be set, "+
|
|
"got %v", ErrTooManySigs, numSigs)
|
|
}
|
|
|
|
// The final sig is the one that's actually set.
|
|
sig := msg.CloserAndClosee.ValOpt().Alt(
|
|
msg.NoCloserClosee.ValOpt(),
|
|
).Alt(
|
|
msg.CloserNoClosee.ValOpt(),
|
|
)
|
|
|
|
return fn.NewResult(sig.UnwrapOrErr(ErrNoSig))
|
|
}
|
|
|
|
// ProcessEvent implements the state transition function for the
|
|
// LocalOfferSent state. In this state, we'll wait for the remote party to
|
|
// send a close_signed message which gives us the ability to broadcast a new
|
|
// co-op close transaction.
|
|
func (l *LocalOfferSent) ProcessEvent(event ProtocolEvent, env *Environment,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
switch msg := event.(type) { //nolint:gocritic
|
|
// If we receive a LocalSigReceived event, then we'll attempt to
|
|
// validate the signature from the remote party. If valid, then we can
|
|
// broadcast the transaction, and transition to the ClosePending state.
|
|
case *LocalSigReceived:
|
|
// Extract and validate that only one sig field is set.
|
|
sig, err := extractSig(msg.SigMsg).Unpack()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
remoteSig, err := sig.ToSignature()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
localSig, err := l.LocalSig.ToSignature()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Now that we have their signature, we'll attempt to validate
|
|
// it, then extract a valid closing signature from it.
|
|
closeTx, _, err := env.CloseSigner.CompleteCooperativeClose(
|
|
localSig, remoteSig, l.LocalDeliveryScript,
|
|
l.RemoteDeliveryScript, l.ProposedFee,
|
|
lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
|
|
lnwallet.WithCustomPayer(lntypes.Local),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// As we're about to broadcast a new version of the co-op close
|
|
// transaction, we'll mark again as broadcast, but with this
|
|
// variant of the co-op close tx.
|
|
err = env.ChanObserver.MarkCoopBroadcasted(closeTx, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
broadcastEvent := protofsm.DaemonEventSet{&protofsm.BroadcastTxn{ //nolint:ll
|
|
Tx: closeTx,
|
|
Label: labels.MakeLabel(
|
|
labels.LabelTypeChannelClose, &env.Scid,
|
|
),
|
|
}}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): received sig from "+
|
|
"remote party, broadcasting: tx=%v", env.ChanPoint,
|
|
lnutils.SpewLogClosure(closeTx),
|
|
)
|
|
|
|
return &CloseStateTransition{
|
|
NextState: &ClosePending{
|
|
CloseTx: closeTx,
|
|
},
|
|
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
|
ExternalEvents: broadcastEvent,
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: received %T while in LocalOfferSent",
|
|
ErrInvalidStateTransition, event)
|
|
}
|
|
|
|
// ProcessEvent implements the state transition function for the
|
|
// RemoteCloseStart. In this state, we'll wait for the remote party to send a
|
|
// closing_complete message. Assuming they can pay for the fees, we'll sign it
|
|
// ourselves, then transition to the next state of ClosePending.
|
|
func (l *RemoteCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
switch msg := event.(type) { //nolint:gocritic
|
|
// If we receive a OfferReceived event, we'll make sure they can
|
|
// actually pay for the fee. If so, then we'll counter sign and
|
|
// transition to a terminal state.
|
|
case *OfferReceivedEvent:
|
|
// To start, we'll perform some basic validation of the sig
|
|
// message they've sent. We'll validate that the remote party
|
|
// actually has enough fees to pay the closing fees.
|
|
if !l.RemoteCanPayFees(msg.SigMsg.FeeSatoshis) {
|
|
return nil, fmt.Errorf("%w: %v vs %v",
|
|
ErrRemoteCannotPay,
|
|
msg.SigMsg.FeeSatoshis,
|
|
l.RemoteBalance.ToSatoshis())
|
|
}
|
|
|
|
// With the basic sanity checks out of the way, we'll now
|
|
// figure out which signature that we'll attempt to sign
|
|
// against.
|
|
var (
|
|
remoteSig input.Signature
|
|
noClosee bool
|
|
)
|
|
switch {
|
|
// If our balance is dust, then we expect the CloserNoClosee
|
|
// sig to be set.
|
|
case l.LocalAmtIsDust():
|
|
if msg.SigMsg.CloserNoClosee.IsNone() {
|
|
return nil, ErrCloserNoClosee
|
|
}
|
|
msg.SigMsg.CloserNoClosee.WhenSomeV(func(s lnwire.Sig) {
|
|
remoteSig, _ = s.ToSignature()
|
|
noClosee = true
|
|
})
|
|
|
|
// Otherwise, we'll assume that CloseAndClosee is set.
|
|
//
|
|
// TODO(roasbeef): NoCloserClosee, but makes no sense?
|
|
default:
|
|
if msg.SigMsg.CloserAndClosee.IsNone() {
|
|
return nil, ErrCloserAndClosee
|
|
}
|
|
msg.SigMsg.CloserAndClosee.WhenSomeV(func(s lnwire.Sig) { //nolint:ll
|
|
remoteSig, _ = s.ToSignature()
|
|
})
|
|
}
|
|
|
|
chanOpts := []lnwallet.ChanCloseOpt{
|
|
lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
|
|
lnwallet.WithCustomLockTime(msg.SigMsg.LockTime),
|
|
lnwallet.WithCustomPayer(lntypes.Remote),
|
|
}
|
|
|
|
chancloserLog.Infof("responding to close w/ local_addr=%x, "+
|
|
"remote_addr=%x, fee=%v",
|
|
l.LocalDeliveryScript[:], l.RemoteDeliveryScript[:],
|
|
msg.SigMsg.FeeSatoshis)
|
|
|
|
// Now that we have the remote sig, we'll sign the version they
|
|
// signed, then attempt to complete the cooperative close
|
|
// process.
|
|
//
|
|
// TODO(roasbeef): need to be able to omit an output when
|
|
// signing based on the above, as closing opt
|
|
rawSig, _, _, err := env.CloseSigner.CreateCloseProposal(
|
|
msg.SigMsg.FeeSatoshis, l.LocalDeliveryScript,
|
|
l.RemoteDeliveryScript, chanOpts...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wireSig, err := lnwire.NewSigFromSignature(rawSig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
localSig, err := wireSig.ToSignature()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// With our signature created, we'll now attempt to finalize the
|
|
// close process.
|
|
closeTx, _, err := env.CloseSigner.CompleteCooperativeClose(
|
|
localSig, remoteSig, l.LocalDeliveryScript,
|
|
l.RemoteDeliveryScript, msg.SigMsg.FeeSatoshis,
|
|
chanOpts...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chancloserLog.Infof("ChannelPoint(%v): received sig (fee=%v "+
|
|
"sats) from remote party, signing new tx=%v",
|
|
env.ChanPoint, msg.SigMsg.FeeSatoshis,
|
|
lnutils.SpewLogClosure(closeTx),
|
|
)
|
|
|
|
var closingSigs lnwire.ClosingSigs
|
|
if noClosee {
|
|
closingSigs.CloserNoClosee = newSigTlv[tlv.TlvType1](
|
|
wireSig,
|
|
)
|
|
} else {
|
|
closingSigs.CloserAndClosee = newSigTlv[tlv.TlvType3](
|
|
wireSig,
|
|
)
|
|
}
|
|
|
|
// As we're about to broadcast a new version of the co-op close
|
|
// transaction, we'll mark again as broadcast, but with this
|
|
// variant of the co-op close tx.
|
|
//
|
|
// TODO(roasbeef): db will only store one instance, store both?
|
|
err = env.ChanObserver.MarkCoopBroadcasted(closeTx, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// As we transition, we'll omit two events: one to broadcast
|
|
// the transaction, and the other to send our ClosingSig
|
|
// message to the remote party.
|
|
sendEvent := &protofsm.SendMsgEvent[ProtocolEvent]{
|
|
TargetPeer: env.ChanPeer,
|
|
Msgs: []lnwire.Message{&lnwire.ClosingSig{
|
|
ChannelID: env.ChanID,
|
|
ClosingSigs: closingSigs,
|
|
}},
|
|
}
|
|
broadcastEvent := &protofsm.BroadcastTxn{
|
|
Tx: closeTx,
|
|
Label: labels.MakeLabel(
|
|
labels.LabelTypeChannelClose, &env.Scid,
|
|
),
|
|
}
|
|
daemonEvents := protofsm.DaemonEventSet{
|
|
sendEvent, broadcastEvent,
|
|
}
|
|
|
|
// Now that we've extracted the signature, we'll transition to
|
|
// the next state where we'll sign+broadcast the sig.
|
|
return &CloseStateTransition{
|
|
NextState: &ClosePending{
|
|
CloseTx: closeTx,
|
|
},
|
|
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
|
ExternalEvents: daemonEvents,
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: received %T while in RemoteCloseStart",
|
|
ErrInvalidStateTransition, event)
|
|
}
|
|
|
|
// ProcessEvent is a semi-terminal state in the rbf-coop close state machine.
|
|
// In this state, we're waiting for either a confirmation, or for either side
|
|
// to attempt to create a new RBF'd co-op close transaction.
|
|
func (c *ClosePending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
switch msg := event.(type) {
|
|
// If we can a spend while waiting for the close, then we'll go to our
|
|
// terminal state.
|
|
case *SpendEvent:
|
|
return &CloseStateTransition{
|
|
NextState: &CloseFin{
|
|
ConfirmedTx: msg.Tx,
|
|
},
|
|
}, nil
|
|
|
|
default:
|
|
|
|
return &CloseStateTransition{
|
|
NextState: c,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// ProcessEvent is the event processing for out terminal state. In this state,
|
|
// we just keep looping back on ourselves.
|
|
func (c *CloseFin) ProcessEvent(event ProtocolEvent, env *Environment,
|
|
) (*CloseStateTransition, error) {
|
|
|
|
return &CloseStateTransition{
|
|
NextState: c,
|
|
}, nil
|
|
}
|