Merge pull request #8512 from lightningnetwork/rbf-coop-fsm
[3/4] - lnwallet/chancloser: add new protofsm based RBF chan closer
This commit is contained in:
17 files changed
+4159
-102
No files matched your search
@@ -539,8 +539,8 @@ func (c *ChanCloser) AuxOutputs() fn.Option[AuxCloseOutputs] {
|
||||
// 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 {
|
||||
func validateShutdownScript(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
|
||||
@@ -568,12 +568,6 @@ func validateShutdownScript(disconnect func() error, upfrontScript,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -630,7 +624,6 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
|
||||
// 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 {
|
||||
@@ -681,7 +674,6 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
|
||||
// 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 {
|
||||
|
||||
@@ -129,8 +129,8 @@ func TestMaybeMatchScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validateShutdownScript(
|
||||
func() error { return nil }, test.upfrontScript,
|
||||
test.shutdownScript, &chaincfg.SimNetParams,
|
||||
test.upfrontScript, test.shutdownScript,
|
||||
&chaincfg.SimNetParams,
|
||||
)
|
||||
|
||||
if err != test.expectedErr {
|
||||
@@ -189,7 +189,7 @@ func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress {
|
||||
|
||||
func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
|
||||
localScript, remoteScript []byte,
|
||||
_ ...lnwallet.ChanCloseOpt) (input.Signature, *chainhash.Hash,
|
||||
_ ...lnwallet.ChanCloseOpt) (input.Signature, *wire.MsgTx,
|
||||
btcutil.Amount, error) {
|
||||
|
||||
if m.chanType.IsTaproot() {
|
||||
|
||||
@@ -3,7 +3,6 @@ package chancloser
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
@@ -100,7 +99,7 @@ type Channel interface { //nolint:interfacebloat
|
||||
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
||||
closeOpt ...lnwallet.ChanCloseOpt,
|
||||
) (
|
||||
input.Signature, *chainhash.Hash, btcutil.Amount, error)
|
||||
input.Signature, *wire.MsgTx, btcutil.Amount, error)
|
||||
|
||||
// CompleteCooperativeClose persistently "completes" the cooperative
|
||||
// close by producing a fully signed co-op close transaction.
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package chancloser
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type dummyAdapters struct {
|
||||
mock.Mock
|
||||
|
||||
msgSent atomic.Bool
|
||||
|
||||
confChan chan *chainntnfs.TxConfirmation
|
||||
spendChan chan *chainntnfs.SpendDetail
|
||||
}
|
||||
|
||||
func newDaemonAdapters() *dummyAdapters {
|
||||
return &dummyAdapters{
|
||||
confChan: make(chan *chainntnfs.TxConfirmation, 1),
|
||||
spendChan: make(chan *chainntnfs.SpendDetail, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dummyAdapters) SendMessages(pub btcec.PublicKey,
|
||||
msgs []lnwire.Message) error {
|
||||
|
||||
defer d.msgSent.Store(true)
|
||||
|
||||
args := d.Called(pub, msgs)
|
||||
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (d *dummyAdapters) BroadcastTransaction(tx *wire.MsgTx,
|
||||
label string) error {
|
||||
|
||||
args := d.Called(tx, label)
|
||||
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (d *dummyAdapters) DisableChannel(op wire.OutPoint) error {
|
||||
args := d.Called(op)
|
||||
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (d *dummyAdapters) RegisterConfirmationsNtfn(txid *chainhash.Hash,
|
||||
pkScript []byte, numConfs, heightHint uint32,
|
||||
opts ...chainntnfs.NotifierOption,
|
||||
) (*chainntnfs.ConfirmationEvent, error) {
|
||||
|
||||
args := d.Called(txid, pkScript, numConfs)
|
||||
|
||||
err := args.Error(0)
|
||||
|
||||
return &chainntnfs.ConfirmationEvent{
|
||||
Confirmed: d.confChan,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (d *dummyAdapters) RegisterSpendNtfn(outpoint *wire.OutPoint,
|
||||
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
|
||||
|
||||
args := d.Called(outpoint, pkScript, heightHint)
|
||||
|
||||
err := args.Error(0)
|
||||
|
||||
return &chainntnfs.SpendEvent{
|
||||
Spend: d.spendChan,
|
||||
}, err
|
||||
}
|
||||
|
||||
type mockFeeEstimator struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
|
||||
localTxOut, remoteTxOut *wire.TxOut,
|
||||
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
|
||||
|
||||
args := m.Called(chanType, localTxOut, remoteTxOut, idealFeeRate)
|
||||
return args.Get(0).(btcutil.Amount)
|
||||
}
|
||||
|
||||
type mockChanObserver struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockChanObserver) NoDanglingUpdates() bool {
|
||||
args := m.Called()
|
||||
return args.Bool(0)
|
||||
}
|
||||
|
||||
func (m *mockChanObserver) DisableIncomingAdds() error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockChanObserver) DisableOutgoingAdds() error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockChanObserver) MarkCoopBroadcasted(txn *wire.MsgTx,
|
||||
local bool) error {
|
||||
|
||||
args := m.Called(txn, local)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockChanObserver) MarkShutdownSent(deliveryAddr []byte,
|
||||
isInitiator bool) error {
|
||||
|
||||
args := m.Called(deliveryAddr, isInitiator)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockChanObserver) FinalBalances() fn.Option[ShutdownBalances] {
|
||||
args := m.Called()
|
||||
return args.Get(0).(fn.Option[ShutdownBalances])
|
||||
}
|
||||
|
||||
func (m *mockChanObserver) DisableChannel() error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type mockErrorReporter struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockErrorReporter) ReportError(err error) {
|
||||
m.Called(err)
|
||||
}
|
||||
|
||||
type mockCloseSigner struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockCloseSigner) CreateCloseProposal(fee btcutil.Amount,
|
||||
localScript []byte, remoteScript []byte,
|
||||
closeOpt ...lnwallet.ChanCloseOpt) (
|
||||
input.Signature, *wire.MsgTx, btcutil.Amount, error) {
|
||||
|
||||
args := m.Called(fee, localScript, remoteScript, closeOpt)
|
||||
|
||||
return args.Get(0).(input.Signature), args.Get(1).(*wire.MsgTx),
|
||||
args.Get(2).(btcutil.Amount), args.Error(3)
|
||||
}
|
||||
|
||||
func (m *mockCloseSigner) CompleteCooperativeClose(localSig,
|
||||
remoteSig input.Signature,
|
||||
localScript, remoteScript []byte,
|
||||
fee btcutil.Amount, closeOpt ...lnwallet.ChanCloseOpt,
|
||||
) (*wire.MsgTx, btcutil.Amount, error) {
|
||||
|
||||
args := m.Called(
|
||||
localSig, remoteSig, localScript, remoteScript, fee, closeOpt,
|
||||
)
|
||||
|
||||
return args.Get(0).(*wire.MsgTx), args.Get(1).(btcutil.Amount),
|
||||
args.Error(2)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package chancloser
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
// RbfMsgMapper is a struct that implements the MsgMapper interface for the
|
||||
// rbf-coop close state machine. This enables the state machine to be used with
|
||||
// protofsm.
|
||||
type RbfMsgMapper struct {
|
||||
// blockHeight is the height of the block when the co-op close request
|
||||
// was initiated. This is used to validate conditions related to the
|
||||
// thaw height.
|
||||
blockHeight uint32
|
||||
|
||||
// chanID is the channel ID of the channel being closed.
|
||||
chanID lnwire.ChannelID
|
||||
}
|
||||
|
||||
// NewRbfMsgMapper creates a new RbfMsgMapper instance given the current block
|
||||
// height when the co-op close request was initiated.
|
||||
func NewRbfMsgMapper(blockHeight uint32,
|
||||
chanID lnwire.ChannelID) *RbfMsgMapper {
|
||||
|
||||
return &RbfMsgMapper{
|
||||
blockHeight: blockHeight,
|
||||
chanID: chanID,
|
||||
}
|
||||
}
|
||||
|
||||
// someEvent returns the target type as a protocol event option.
|
||||
func someEvent[T ProtocolEvent](m T) fn.Option[ProtocolEvent] {
|
||||
return fn.Some(ProtocolEvent(m))
|
||||
}
|
||||
|
||||
// isExpectedChanID returns true if the channel ID of the message matches the
|
||||
// bound instance.
|
||||
func (r *RbfMsgMapper) isExpectedChanID(chanID lnwire.ChannelID) bool {
|
||||
return r.chanID == chanID
|
||||
}
|
||||
|
||||
// MapMsg maps a wire message into a FSM event. If the message is not mappable,
|
||||
// then an error is returned.
|
||||
func (r *RbfMsgMapper) MapMsg(wireMsg lnwire.Message) fn.Option[ProtocolEvent] {
|
||||
switch msg := wireMsg.(type) {
|
||||
case *lnwire.Shutdown:
|
||||
if !r.isExpectedChanID(msg.ChannelID) {
|
||||
return fn.None[ProtocolEvent]()
|
||||
}
|
||||
|
||||
return someEvent(&ShutdownReceived{
|
||||
BlockHeight: r.blockHeight,
|
||||
ShutdownScript: msg.Address,
|
||||
})
|
||||
|
||||
case *lnwire.ClosingComplete:
|
||||
if !r.isExpectedChanID(msg.ChannelID) {
|
||||
return fn.None[ProtocolEvent]()
|
||||
}
|
||||
|
||||
return someEvent(&OfferReceivedEvent{
|
||||
SigMsg: *msg,
|
||||
})
|
||||
|
||||
case *lnwire.ClosingSig:
|
||||
if !r.isExpectedChanID(msg.ChannelID) {
|
||||
return fn.None[ProtocolEvent]()
|
||||
}
|
||||
|
||||
return someEvent(&LocalSigReceived{
|
||||
SigMsg: *msg,
|
||||
})
|
||||
}
|
||||
|
||||
return fn.None[ProtocolEvent]()
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
package chancloser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/protofsm"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidStateTransition is returned when we receive an unexpected
|
||||
// event for a given state.
|
||||
ErrInvalidStateTransition = fmt.Errorf("invalid state transition")
|
||||
|
||||
// ErrTooManySigs is returned when we receive too many sigs from the
|
||||
// remote party in the ClosingSigs message.
|
||||
ErrTooManySigs = fmt.Errorf("too many sigs received")
|
||||
|
||||
// ErrNoSig is returned when we receive no sig from the remote party.
|
||||
ErrNoSig = fmt.Errorf("no sig received")
|
||||
|
||||
// ErrUnknownFinalBalance is returned if we're unable to determine the
|
||||
// final channel balance after a flush.
|
||||
ErrUnknownFinalBalance = fmt.Errorf("unknown final balance")
|
||||
|
||||
// ErrRemoteCannotPay is returned if the remote party cannot pay the
|
||||
// pay for the fees when it sends a signature.
|
||||
ErrRemoteCannotPay = fmt.Errorf("remote cannot pay fees")
|
||||
|
||||
// ErrNonFinalSequence is returned if we receive a non-final sequence
|
||||
// from the remote party for their signature.
|
||||
ErrNonFinalSequence = fmt.Errorf("received non-final sequence")
|
||||
|
||||
// ErrCloserNoClosee is returned if our balance is dust, but the remote
|
||||
// party includes our output.
|
||||
ErrCloserNoClosee = fmt.Errorf("expected CloserNoClosee sig")
|
||||
|
||||
// ErrCloserAndClosee is returned when we expect a sig covering both
|
||||
// outputs, it isn't present.
|
||||
ErrCloserAndClosee = fmt.Errorf("expected CloserAndClosee sig")
|
||||
)
|
||||
|
||||
// ProtocolEvent is a special interface used to create the equivalent of a
|
||||
// sum-type, but using a "sealed" interface. Protocol events can be used as
|
||||
// input to trigger a state transition, and also as output to trigger a new set
|
||||
// of events into the very same state machine.
|
||||
type ProtocolEvent interface {
|
||||
protocolSealed()
|
||||
}
|
||||
|
||||
// ProtocolEvents is a special type constraint that enumerates all the possible
|
||||
// protocol events. This is used mainly as type-level documentation, and may
|
||||
// also be useful to constraint certain state transition functions.
|
||||
type ProtocolEvents interface {
|
||||
SendShutdown | ShutdownReceived | ShutdownComplete | ChannelFlushed |
|
||||
SendOfferEvent | OfferReceivedEvent | LocalSigReceived |
|
||||
SpendEvent
|
||||
}
|
||||
|
||||
// SpendEvent indicates that a transaction spending the funding outpoint has
|
||||
// been confirmed in the main chain.
|
||||
type SpendEvent struct {
|
||||
// Tx is the spending transaction that has been confirmed.
|
||||
Tx *wire.MsgTx
|
||||
|
||||
// BlockHeight is the height of the block that confirmed the
|
||||
// transaction.
|
||||
BlockHeight uint32
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (s *SpendEvent) protocolSealed() {}
|
||||
|
||||
// SendShutdown indicates that the user wishes to co-op close the channel, so we
|
||||
// should send a new shutdown message to the remote party.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: ChannelActive
|
||||
// - toState: ChannelFlushing
|
||||
type SendShutdown struct {
|
||||
// DeliveryAddr is the address we'd like to receive the funds to. If
|
||||
// None, then a new addr will be generated.
|
||||
DeliveryAddr fn.Option[lnwire.DeliveryAddress]
|
||||
|
||||
// IdealFeeRate is the ideal fee rate we'd like to use for the closing
|
||||
// attempt.
|
||||
IdealFeeRate chainfee.SatPerVByte
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (s *SendShutdown) protocolSealed() {}
|
||||
|
||||
// ShutdownReceived indicates that we received a shutdown event so we need to
|
||||
// enter the flushing state.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: ChannelActive
|
||||
// - toState: ChannelFlushing
|
||||
type ShutdownReceived struct {
|
||||
// ShutdownScript is the script the remote party wants to use to
|
||||
// shutdown.
|
||||
ShutdownScript lnwire.DeliveryAddress
|
||||
|
||||
// BlockHeight is the height at which the shutdown message was
|
||||
// received. This is used for channel leases to determine if a co-op
|
||||
// close can occur.
|
||||
BlockHeight uint32
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (s *ShutdownReceived) protocolSealed() {}
|
||||
|
||||
// ShutdownComplete is an event that indicates the channel has been fully
|
||||
// shutdown. At this point, we'll go to the ChannelFlushing state so we can
|
||||
// wait for all pending updates to be gone from the channel.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: ShutdownPending
|
||||
// - toState: ChannelFlushing
|
||||
type ShutdownComplete struct {
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (s *ShutdownComplete) protocolSealed() {}
|
||||
|
||||
// ShutdownBalances holds the local+remote balance once the channel has been
|
||||
// fully flushed.
|
||||
type ShutdownBalances struct {
|
||||
// LocalBalance is the local balance of the channel.
|
||||
LocalBalance lnwire.MilliSatoshi
|
||||
|
||||
// RemoteBalance is the remote balance of the channel.
|
||||
RemoteBalance lnwire.MilliSatoshi
|
||||
}
|
||||
|
||||
// unknownBalance is a special variable used to denote an unknown channel
|
||||
// balance (channel not fully flushed yet).
|
||||
var unknownBalance = ShutdownBalances{}
|
||||
|
||||
// ChannelFlushed is an event that indicates the channel has been fully flushed
|
||||
// can we can now start closing negotiation.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: ChannelFlushing
|
||||
// - toState: ClosingNegotiation
|
||||
type ChannelFlushed struct {
|
||||
// FreshFlush indicates if this is the first time the channel has been
|
||||
// flushed, or if this is a flush as part of an RBF iteration.
|
||||
FreshFlush bool
|
||||
|
||||
// ShutdownBalances is the balances of the channel once it has been
|
||||
// flushed. We tie this to the ChannelFlushed state as this may not be
|
||||
// the same as the starting value.
|
||||
ShutdownBalances
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (c *ChannelFlushed) protocolSealed() {}
|
||||
|
||||
// SendOfferEvent is a self-triggered event that transitions us from the
|
||||
// LocalCloseStart state to the LocalOfferSent state. This kicks off the new
|
||||
// signing process for the co-op close process.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: LocalCloseStart
|
||||
// - toState: LocalOfferSent
|
||||
type SendOfferEvent struct {
|
||||
// TargetFeeRate is the fee rate we'll use for the closing transaction.
|
||||
TargetFeeRate chainfee.SatPerVByte
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (s *SendOfferEvent) protocolSealed() {}
|
||||
|
||||
// LocalSigReceived is an event that indicates we've received a signature from
|
||||
// the remote party, which signs our the co-op close transaction at our
|
||||
// specified fee rate.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: LocalOfferSent
|
||||
// - toState: ClosePending
|
||||
type LocalSigReceived struct {
|
||||
// SigMsg is the sig message we received from the remote party.
|
||||
SigMsg lnwire.ClosingSig
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (s *LocalSigReceived) protocolSealed() {}
|
||||
|
||||
// OfferReceivedEvent is an event that indicates we've received an offer from
|
||||
// the remote party. This applies to the RemoteCloseStart state.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: RemoteCloseStart
|
||||
// - toState: ClosePending
|
||||
type OfferReceivedEvent struct {
|
||||
// SigMsg is the signature message we received from the remote party.
|
||||
SigMsg lnwire.ClosingComplete
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (s *OfferReceivedEvent) protocolSealed() {}
|
||||
|
||||
// CloseSigner is an interface that abstracts away the details of the signing
|
||||
// new coop close transactions.
|
||||
type CloseSigner interface {
|
||||
// CreateCloseProposal creates a new co-op close proposal in the form
|
||||
// of a valid signature, the chainhash of the final txid, and our final
|
||||
// balance in the created state.
|
||||
CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
||||
closeOpt ...lnwallet.ChanCloseOpt,
|
||||
) (
|
||||
input.Signature, *wire.MsgTx, btcutil.Amount, error)
|
||||
|
||||
// CompleteCooperativeClose persistently "completes" the cooperative
|
||||
// close by producing a fully signed co-op close transaction.
|
||||
CompleteCooperativeClose(localSig, remoteSig input.Signature,
|
||||
localDeliveryScript, remoteDeliveryScript []byte,
|
||||
proposedFee btcutil.Amount, closeOpt ...lnwallet.ChanCloseOpt,
|
||||
) (*wire.MsgTx, btcutil.Amount, error)
|
||||
}
|
||||
|
||||
// ChanStateObserver is an interface used to observe state changes that occur
|
||||
// in a channel. This can be used to figure out if we're able to send a
|
||||
// shutdown message or not.
|
||||
type ChanStateObserver interface {
|
||||
// NoDanglingUpdates returns true if there are no dangling updates in
|
||||
// the channel. In other words, there are no active update messages
|
||||
// that haven't already been covered by a commit sig.
|
||||
NoDanglingUpdates() bool
|
||||
|
||||
// DisableIncomingAdds instructs the channel link to disable process new
|
||||
// incoming add messages.
|
||||
DisableIncomingAdds() error
|
||||
|
||||
// DisableOutgoingAdds instructs the channel link to disable process
|
||||
// new outgoing add messages.
|
||||
DisableOutgoingAdds() error
|
||||
|
||||
// DisableChannel attempts to disable a channel (marking it ineligible
|
||||
// to forward), and also sends out a network update to disable the
|
||||
// channel.
|
||||
DisableChannel() error
|
||||
|
||||
// MarkCoopBroadcasted persistently marks that the channel close
|
||||
// transaction has been broadcast.
|
||||
MarkCoopBroadcasted(*wire.MsgTx, bool) error
|
||||
|
||||
// MarkShutdownSent persists the given ShutdownInfo. The existence of
|
||||
// the ShutdownInfo represents the fact that the Shutdown message has
|
||||
// been sent by us and so should be re-sent on re-establish.
|
||||
MarkShutdownSent(deliveryAddr []byte, isInitiator bool) error
|
||||
|
||||
// FinalBalances is the balances of the channel once it has been
|
||||
// flushed. If Some, then this indicates that the channel is now in a
|
||||
// state where it's always flushed, so we can accelerate the state
|
||||
// transitions.
|
||||
FinalBalances() fn.Option[ShutdownBalances]
|
||||
}
|
||||
|
||||
// Environment is a set of dependencies that a state machine may need to carry
|
||||
// out the logic for a given state transition. All fields are to be considered
|
||||
// immutable, and will be fixed for the lifetime of the state machine.
|
||||
type Environment struct {
|
||||
// ChainParams is the chain parameters for the channel.
|
||||
ChainParams chaincfg.Params
|
||||
|
||||
// ChanPeer is the peer we're attempting to close the channel with.
|
||||
ChanPeer btcec.PublicKey
|
||||
|
||||
// ChanPoint is the channel point of the active channel.
|
||||
ChanPoint wire.OutPoint
|
||||
|
||||
// ChanID is the channel ID of the channel we're attempting to close.
|
||||
ChanID lnwire.ChannelID
|
||||
|
||||
// ShortChanID is the short channel ID of the channel we're attempting
|
||||
// to close.
|
||||
Scid lnwire.ShortChannelID
|
||||
|
||||
// ChanType is the type of channel we're attempting to close.
|
||||
ChanType channeldb.ChannelType
|
||||
|
||||
// BlockHeight is the current block height.
|
||||
BlockHeight uint32
|
||||
|
||||
// DefaultFeeRate is the fee we'll use for the closing transaction if
|
||||
// the user didn't specify an ideal fee rate. This may happen if the
|
||||
// remote party is the one that initiates the co-op close.
|
||||
DefaultFeeRate chainfee.SatPerVByte
|
||||
|
||||
// ThawHeight is the height at which the channel will be thawed. If
|
||||
// this is None, then co-op close can occur at any moment.
|
||||
ThawHeight fn.Option[uint32]
|
||||
|
||||
// RemoteUprontShutdown is the upfront shutdown addr of the remote
|
||||
// party. We'll use this to validate if the remote peer is authorized to
|
||||
// close the channel with the sent addr or not.
|
||||
RemoteUpfrontShutdown fn.Option[lnwire.DeliveryAddress]
|
||||
|
||||
// LocalUprontShutdown is our upfront shutdown address. If Some, then
|
||||
// we'll default to using this.
|
||||
LocalUpfrontShutdown fn.Option[lnwire.DeliveryAddress]
|
||||
|
||||
// NewDeliveryScript is a function that returns a new delivery script.
|
||||
// This is used if we don't have an upfront shutdown addr, and no addr
|
||||
// was specified at closing time.
|
||||
NewDeliveryScript func() (lnwire.DeliveryAddress, error)
|
||||
|
||||
// FeeEstimator is the fee estimator we'll use to determine the fee in
|
||||
// satoshis we'll pay given a local and/or remote output.
|
||||
FeeEstimator CoopFeeEstimator
|
||||
|
||||
// ChanObserver is an interface used to observe state changes to the
|
||||
// channel. We'll use this to figure out when/if we can send certain
|
||||
// messages.
|
||||
ChanObserver ChanStateObserver
|
||||
|
||||
// CloseSigner is the signer we'll use to sign the close transaction.
|
||||
// This is a part of the ChannelFlushed state, as the channel state
|
||||
// we'll be signing can only be determined once the channel has been
|
||||
// flushed.
|
||||
CloseSigner CloseSigner
|
||||
}
|
||||
|
||||
// Name returns the name of the environment. This is used to uniquely identify
|
||||
// the environment of related state machines. For this state machine, the name
|
||||
// is based on the channel ID.
|
||||
func (e *Environment) Name() string {
|
||||
return fmt.Sprintf("rbf_chan_closer(%v)", e.ChanPoint)
|
||||
}
|
||||
|
||||
// CloseStateTransition is the StateTransition type specific to the coop close
|
||||
// state machine.
|
||||
//
|
||||
//nolint:ll
|
||||
type CloseStateTransition = protofsm.StateTransition[ProtocolEvent, *Environment]
|
||||
|
||||
// ProtocolState is our sum-type ish interface that represents the current
|
||||
// protocol state.
|
||||
type ProtocolState interface {
|
||||
// protocolStateSealed is a special method that is used to seal the
|
||||
// interface (only types in this package can implement it).
|
||||
protocolStateSealed()
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
IsTerminal() bool
|
||||
|
||||
// ProcessEvent takes a protocol event, and implements a state
|
||||
// transition for the state.
|
||||
ProcessEvent(ProtocolEvent, *Environment) (*CloseStateTransition, error)
|
||||
}
|
||||
|
||||
// AsymmetricPeerState is an extension of the normal ProtocolState interface
|
||||
// that gives a caller a hit on if the target state should process an incoming
|
||||
// event or not.
|
||||
type AsymmetricPeerState interface {
|
||||
ProtocolState
|
||||
|
||||
// ShouldRouteTo returns true if the target state should process the
|
||||
// target event.
|
||||
ShouldRouteTo(ProtocolEvent) bool
|
||||
}
|
||||
|
||||
// ProtocolStates is a special type constraint that enumerates all the possible
|
||||
// protocol states.
|
||||
type ProtocolStates interface {
|
||||
ChannelActive | ShutdownPending | ChannelFlushing | ClosingNegotiation |
|
||||
LocalCloseStart | LocalOfferSent | RemoteCloseStart |
|
||||
ClosePending | CloseFin
|
||||
}
|
||||
|
||||
// ChannelActive is the base state for the channel closer state machine. In
|
||||
// this state, we haven't begun the shutdown process yet, so the channel is
|
||||
// still active. Receiving the ShutdownSent or ShutdownReceived events will
|
||||
// transition us to the ChannelFushing state.
|
||||
//
|
||||
// When we transition to this state, we emit a DaemonEvent to send the shutdown
|
||||
// message if we received one ourselves. Alternatively, we may send out a new
|
||||
// shutdown if we're initiating it for the very first time.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: None
|
||||
// - toState: ChannelFlushing
|
||||
//
|
||||
// input events:
|
||||
// - SendShutdown
|
||||
// - ShutdownReceived
|
||||
type ChannelActive struct {
|
||||
}
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
func (c *ChannelActive) IsTerminal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (c *ChannelActive) protocolStateSealed() {}
|
||||
|
||||
// ShutdownScripts is a set of scripts that we'll use to co-op close the
|
||||
// channel.
|
||||
type ShutdownScripts struct {
|
||||
// LocalDeliveryScript is the script that we'll send our settled
|
||||
// channel funds to.
|
||||
LocalDeliveryScript lnwire.DeliveryAddress
|
||||
|
||||
// RemoteDeliveryScript is the script that we'll send the remote
|
||||
// party's settled channel funds to.
|
||||
RemoteDeliveryScript lnwire.DeliveryAddress
|
||||
}
|
||||
|
||||
// ShutdownPending is the state we enter into after we've sent or received the
|
||||
// shutdown message. If we sent the shutdown, then we'll wait for the remote
|
||||
// party to send a shutdown. Otherwise, if we received it, then we'll send our
|
||||
// shutdown then go to the next state.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: ChannelActive
|
||||
// - toState: ChannelFlushing
|
||||
//
|
||||
// input events:
|
||||
// - SendShutdown
|
||||
// - ShutdownReceived
|
||||
type ShutdownPending struct {
|
||||
// ShutdownScripts store the set of scripts we'll use to initiate a coop
|
||||
// close.
|
||||
ShutdownScripts
|
||||
|
||||
// IdealFeeRate is the ideal fee rate we'd like to use for the closing
|
||||
// attempt.
|
||||
IdealFeeRate fn.Option[chainfee.SatPerVByte]
|
||||
}
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
func (s *ShutdownPending) IsTerminal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (s *ShutdownPending) protocolStateSealed() {}
|
||||
|
||||
// ChannelFlushing is the state we enter into after we've received or sent a
|
||||
// shutdown message. In this state, we wait the ChannelFlushed event, after
|
||||
// which we'll transition to the CloseReady state.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: ShutdownPending
|
||||
// - toState: ClosingNegotiation
|
||||
//
|
||||
// input events:
|
||||
// - ShutdownComplete
|
||||
// - ShutdownReceived
|
||||
type ChannelFlushing struct {
|
||||
// EarlyRemoteOffer is the offer we received from the remote party
|
||||
// before we obtained the local channel flushed event. We'll stash this
|
||||
// to process later.
|
||||
EarlyRemoteOffer fn.Option[OfferReceivedEvent]
|
||||
|
||||
// ShutdownScripts store the set of scripts we'll use to initiate a coop
|
||||
// close.
|
||||
ShutdownScripts
|
||||
|
||||
// IdealFeeRate is the ideal fee rate we'd like to use for the closing
|
||||
// transaction. Once the channel has been flushed, we'll use this as
|
||||
// our target fee rate.
|
||||
IdealFeeRate fn.Option[chainfee.SatPerVByte]
|
||||
}
|
||||
|
||||
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (c *ChannelFlushing) protocolStateSealed() {}
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
func (c *ChannelFlushing) IsTerminal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ClosingNegotiation is the state we transition to once the channel has been
|
||||
// flushed. This is actually a composite state that contains one for each side
|
||||
// of the channel, as the closing process is asymmetric. Once either of the
|
||||
// peer states reaches the CloseFin state, then the channel is fully closed,
|
||||
// and we'll transition to that terminal state.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: ChannelFlushing
|
||||
// - toState: CloseFin
|
||||
//
|
||||
// input events:
|
||||
// - ChannelFlushed
|
||||
type ClosingNegotiation struct {
|
||||
// PeerStates is a composite state that contains the state for both the
|
||||
// local and remote parties. Our usage of Dual makes this a special
|
||||
// state that allows us to treat two states as a single state. We'll use
|
||||
// the ShouldRouteTo method to determine which state route incoming
|
||||
// events to.
|
||||
PeerState lntypes.Dual[AsymmetricPeerState]
|
||||
}
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
func (c *ClosingNegotiation) IsTerminal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (c *ClosingNegotiation) protocolStateSealed() {}
|
||||
|
||||
// CloseChannelTerms is a set of terms that we'll use to close the channel. This
|
||||
// includes the balances of the channel, and the scripts we'll use to send each
|
||||
// party's funds to.
|
||||
type CloseChannelTerms struct {
|
||||
ShutdownScripts
|
||||
|
||||
ShutdownBalances
|
||||
}
|
||||
|
||||
// DeriveCloseTxOuts takes the close terms, and returns the local and remote tx
|
||||
// out for the close transaction. If an output is dust, then it'll be nil.
|
||||
//
|
||||
// TODO(roasbeef): add func for w/e heuristic to not manifest own output?
|
||||
func (c *CloseChannelTerms) DeriveCloseTxOuts() (*wire.TxOut, *wire.TxOut) {
|
||||
//nolint:ll
|
||||
deriveTxOut := func(balance btcutil.Amount, pkScript []byte) *wire.TxOut {
|
||||
dustLimit := lnwallet.DustLimitForSize(len(pkScript))
|
||||
if balance >= dustLimit {
|
||||
return &wire.TxOut{
|
||||
PkScript: pkScript,
|
||||
Value: int64(balance),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
localTxOut := deriveTxOut(
|
||||
c.LocalBalance.ToSatoshis(),
|
||||
c.LocalDeliveryScript,
|
||||
)
|
||||
remoteTxOut := deriveTxOut(
|
||||
c.RemoteBalance.ToSatoshis(),
|
||||
c.RemoteDeliveryScript,
|
||||
)
|
||||
|
||||
return localTxOut, remoteTxOut
|
||||
}
|
||||
|
||||
// RemoteAmtIsDust returns true if the remote output is dust.
|
||||
func (c *CloseChannelTerms) RemoteAmtIsDust() bool {
|
||||
return c.RemoteBalance.ToSatoshis() < lnwallet.DustLimitForSize(
|
||||
len(c.RemoteDeliveryScript),
|
||||
)
|
||||
}
|
||||
|
||||
// LocalAmtIsDust returns true if the local output is dust.
|
||||
func (c *CloseChannelTerms) LocalAmtIsDust() bool {
|
||||
return c.LocalBalance.ToSatoshis() < lnwallet.DustLimitForSize(
|
||||
len(c.LocalDeliveryScript),
|
||||
)
|
||||
}
|
||||
|
||||
// LocalCanPayFees returns true if the local party can pay the absolute fee
|
||||
// from their local settled balance.
|
||||
func (c *CloseChannelTerms) LocalCanPayFees(absoluteFee btcutil.Amount) bool {
|
||||
return c.LocalBalance.ToSatoshis() >= absoluteFee
|
||||
}
|
||||
|
||||
// RemoteCanPayFees returns true if the remote party can pay the absolute fee
|
||||
// from their remote settled balance.
|
||||
func (c *CloseChannelTerms) RemoteCanPayFees(absoluteFee btcutil.Amount) bool {
|
||||
return c.RemoteBalance.ToSatoshis() >= absoluteFee
|
||||
}
|
||||
|
||||
// LocalCloseStart is the state we enter into after we've received or sent
|
||||
// shutdown, and the channel has been flushed. In this state, we'll emit a new
|
||||
// event to send our offer to drive the rest of the process.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: ChannelFlushing
|
||||
// - toState: LocalOfferSent
|
||||
//
|
||||
// input events:
|
||||
// - SendOfferEvent
|
||||
type LocalCloseStart struct {
|
||||
CloseChannelTerms
|
||||
}
|
||||
|
||||
// ShouldRouteTo returns true if the target state should process the target
|
||||
// event.
|
||||
func (l *LocalCloseStart) ShouldRouteTo(event ProtocolEvent) bool {
|
||||
switch event.(type) {
|
||||
case *SendOfferEvent:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
func (l *LocalCloseStart) IsTerminal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// protocolStateaSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (l *LocalCloseStart) protocolStateSealed() {}
|
||||
|
||||
// LocalOfferSent is the state we transition to after we reveiver the
|
||||
// SendOfferEvent in the LocalCloseStart state. With this state we send our
|
||||
// offer to the remote party, then await a sig from them which concludes the
|
||||
// local cooperative close process.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: LocalCloseStart
|
||||
// - toState: ClosePending
|
||||
//
|
||||
// input events:
|
||||
// - LocalSigReceived
|
||||
type LocalOfferSent struct {
|
||||
CloseChannelTerms
|
||||
|
||||
// ProposedFee is the fee we proposed to the remote party.
|
||||
ProposedFee btcutil.Amount
|
||||
|
||||
// LocalSig is the signature we sent to the remote party.
|
||||
LocalSig lnwire.Sig
|
||||
}
|
||||
|
||||
// ShouldRouteTo returns true if the target state should process the target
|
||||
// event.
|
||||
func (l *LocalOfferSent) ShouldRouteTo(event ProtocolEvent) bool {
|
||||
switch event.(type) {
|
||||
case *LocalSigReceived:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// protocolStateaSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (l *LocalOfferSent) protocolStateSealed() {}
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
func (l *LocalOfferSent) IsTerminal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ClosePending is the state we enter after concluding the negotiation for the
|
||||
// remote or local state. At this point, given a confirmation notification we
|
||||
// can terminate the process. Otherwise, we can receive a fresh CoopCloseReq to
|
||||
// go back to the very start.
|
||||
//
|
||||
// transition:
|
||||
// - fromState: LocalOfferSent || RemoteCloseStart
|
||||
// - toState: CloseFin
|
||||
//
|
||||
// input events:
|
||||
// - LocalSigReceived
|
||||
// - OfferReceivedEvent
|
||||
type ClosePending struct {
|
||||
// CloseTx is the pending close transaction.
|
||||
CloseTx *wire.MsgTx
|
||||
}
|
||||
|
||||
// ShouldRouteTo returns true if the target state should process the target
|
||||
// event.
|
||||
func (c *ClosePending) ShouldRouteTo(event ProtocolEvent) bool {
|
||||
switch event.(type) {
|
||||
case *SpendEvent:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (c *ClosePending) protocolStateSealed() {}
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
func (c *ClosePending) IsTerminal() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// CloseFin is the terminal state for the channel closer state machine. At this
|
||||
// point, the close tx has been confirmed on chain.
|
||||
type CloseFin struct {
|
||||
// ConfirmedTx is the transaction that confirmed the channel close.
|
||||
ConfirmedTx *wire.MsgTx
|
||||
}
|
||||
|
||||
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (c *CloseFin) protocolStateSealed() {}
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
func (c *CloseFin) IsTerminal() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// RemoteCloseStart is similar to the LocalCloseStart, but is used to drive the
|
||||
// process of signing an offer for the remote party
|
||||
//
|
||||
// transition:
|
||||
// - fromState: ChannelFlushing
|
||||
// - toState: ClosePending
|
||||
type RemoteCloseStart struct {
|
||||
CloseChannelTerms
|
||||
}
|
||||
|
||||
// ShouldRouteTo returns true if the target state should process the target
|
||||
// event.
|
||||
func (l *RemoteCloseStart) ShouldRouteTo(event ProtocolEvent) bool {
|
||||
switch event.(type) {
|
||||
case *OfferReceivedEvent:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
||||
func (l *RemoteCloseStart) protocolStateSealed() {}
|
||||
|
||||
// IsTerminal returns true if the target state is a terminal state.
|
||||
func (l *RemoteCloseStart) IsTerminal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RbfChanCloser is a state machine that handles the RBF-enabled cooperative
|
||||
// channel close protocol.
|
||||
type RbfChanCloser = protofsm.StateMachine[ProtocolEvent, *Environment]
|
||||
|
||||
// RbfChanCloserCfg is a configuration struct that is used to initialize a new
|
||||
// RBF chan closer state machine.
|
||||
type RbfChanCloserCfg = protofsm.StateMachineCfg[ProtocolEvent, *Environment]
|
||||
|
||||
// RbfSpendMapper is a type used to map the generic spend event to one specific
|
||||
// to this package.
|
||||
type RbfSpendMapper = protofsm.SpendMapper[ProtocolEvent]
|
||||
|
||||
func SpendMapper(spendEvent *chainntnfs.SpendDetail) ProtocolEvent {
|
||||
return &SpendEvent{
|
||||
Tx: spendEvent.SpendingTx,
|
||||
BlockHeight: uint32(spendEvent.SpendingHeight),
|
||||
}
|
||||
}
|
||||
|
||||
// RbfMsgMapperT is a type used to map incoming wire messages to protocol
|
||||
// events.
|
||||
type RbfMsgMapperT = protofsm.MsgMapper[ProtocolEvent]
|
||||
|
||||
// RbfState is a type alias for the state of the RBF channel closer.
|
||||
type RbfState = protofsm.State[ProtocolEvent, *Environment]
|
||||
|
||||
// RbfEvent is a type alias for the event type of the RBF channel closer.
|
||||
type RbfEvent = protofsm.EmittedEvent[ProtocolEvent]
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+104
-14
@@ -8199,6 +8199,12 @@ type chanCloseOpt struct {
|
||||
// transaction outputs. If this isn't set, then the default BIP-69
|
||||
// sorting is used.
|
||||
customSort CloseSortFunc
|
||||
|
||||
customSequence fn.Option[uint32]
|
||||
|
||||
customLockTime fn.Option[uint32]
|
||||
|
||||
customPayer fn.Option[lntypes.ChannelParty]
|
||||
}
|
||||
|
||||
// ChanCloseOpt is a closure type that cen be used to modify the set of default
|
||||
@@ -8235,6 +8241,31 @@ func WithCustomCoopSort(sorter CloseSortFunc) ChanCloseOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomSequence can be used to specify a custom sequence number for the
|
||||
// co-op close process. Otherwise, a default non-final sequence will be used.
|
||||
func WithCustomSequence(sequence uint32) ChanCloseOpt {
|
||||
return func(opts *chanCloseOpt) {
|
||||
opts.customSequence = fn.Some(sequence)
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomLockTime can be used to specify a custom lock time for the coop
|
||||
// close transaction.
|
||||
func WithCustomLockTime(lockTime uint32) ChanCloseOpt {
|
||||
return func(opts *chanCloseOpt) {
|
||||
opts.customLockTime = fn.Some(lockTime)
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomPayer can be used to specify a custom payer for the closing
|
||||
// transaction. This overrides the default payer, which is the initiator of the
|
||||
// channel.
|
||||
func WithCustomPayer(payer lntypes.ChannelParty) ChanCloseOpt {
|
||||
return func(opts *chanCloseOpt) {
|
||||
opts.customPayer = fn.Some(payer)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCloseProposal is used by both parties in a cooperative channel close
|
||||
// workflow to generate proposed close transactions and signatures. This method
|
||||
// should only be executed once all pending HTLCs (if any) on the channel have
|
||||
@@ -8244,22 +8275,23 @@ func WithCustomCoopSort(sorter CloseSortFunc) ChanCloseOpt {
|
||||
// returned.
|
||||
func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
||||
closeOpts ...ChanCloseOpt) (input.Signature, *chainhash.Hash,
|
||||
closeOpts ...ChanCloseOpt) (input.Signature, *wire.MsgTx,
|
||||
btcutil.Amount, error) {
|
||||
|
||||
lc.Lock()
|
||||
defer lc.Unlock()
|
||||
|
||||
// If we're already closing the channel, then ignore this request.
|
||||
if lc.isClosed {
|
||||
return nil, nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
opts := defaultCloseOpts()
|
||||
for _, optFunc := range closeOpts {
|
||||
optFunc(opts)
|
||||
}
|
||||
|
||||
// Unless there's a custom payer (sign of the RBF flow), if we're
|
||||
// already closing the channel, then ignore this request.
|
||||
if lc.isClosed && opts.customPayer.IsNone() {
|
||||
return nil, nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
// Get the final balances after subtracting the proposed fee, taking
|
||||
// care not to persist the adjusted balance, as the feeRate may change
|
||||
// during the channel closing process.
|
||||
@@ -8269,6 +8301,7 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.CommitFee,
|
||||
opts.customPayer,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
@@ -8294,6 +8327,18 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
)
|
||||
}
|
||||
|
||||
opts.customSequence.WhenSome(func(sequence uint32) {
|
||||
closeTxOpts = append(closeTxOpts, WithCustomTxInSequence(
|
||||
sequence,
|
||||
))
|
||||
})
|
||||
|
||||
opts.customLockTime.WhenSome(func(lockTime uint32) {
|
||||
closeTxOpts = append(closeTxOpts, WithCustomTxLockTime(
|
||||
lockTime,
|
||||
))
|
||||
})
|
||||
|
||||
closeTx, err := CreateCooperativeCloseTx(
|
||||
fundingTxIn(lc.channelState), lc.channelState.LocalChanCfg.DustLimit,
|
||||
lc.channelState.RemoteChanCfg.DustLimit, ourBalance, theirBalance,
|
||||
@@ -8332,8 +8377,8 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
closeTXID := closeTx.TxHash()
|
||||
return sig, &closeTXID, ourBalance, nil
|
||||
return sig, closeTx, ourBalance, nil
|
||||
|
||||
}
|
||||
|
||||
// CompleteCooperativeClose completes the cooperative closure of the target
|
||||
@@ -8352,17 +8397,17 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
||||
lc.Lock()
|
||||
defer lc.Unlock()
|
||||
|
||||
// If the channel is already closing, then ignore this request.
|
||||
if lc.isClosed {
|
||||
// TODO(roasbeef): check to ensure no pending payments
|
||||
return nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
opts := defaultCloseOpts()
|
||||
for _, optFunc := range closeOpts {
|
||||
optFunc(opts)
|
||||
}
|
||||
|
||||
// Unless there's a custom payer (sign of the RBF flow), if we're
|
||||
// already closing the channel, then ignore this request.
|
||||
if lc.isClosed && opts.customPayer.IsNone() {
|
||||
return nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
// Get the final balances after subtracting the proposed fee.
|
||||
ourBalance, theirBalance, err := CoopCloseBalance(
|
||||
lc.channelState.ChanType, lc.channelState.IsInitiator,
|
||||
@@ -8370,6 +8415,7 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.CommitFee,
|
||||
opts.customPayer,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -8395,6 +8441,18 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
||||
)
|
||||
}
|
||||
|
||||
opts.customSequence.WhenSome(func(sequence uint32) {
|
||||
closeTxOpts = append(closeTxOpts, WithCustomTxInSequence(
|
||||
sequence,
|
||||
))
|
||||
})
|
||||
|
||||
opts.customLockTime.WhenSome(func(lockTime uint32) {
|
||||
closeTxOpts = append(closeTxOpts, WithCustomTxLockTime(
|
||||
lockTime,
|
||||
))
|
||||
})
|
||||
|
||||
// Create the transaction used to return the current settled balance
|
||||
// on this active channel back to both parties. In this current model,
|
||||
// the initiator pays full fees for the cooperative close transaction.
|
||||
@@ -9111,6 +9169,13 @@ type closeTxOpts struct {
|
||||
// transaction outputs. If this isn't set, then the default BIP-69
|
||||
// sorting is used.
|
||||
customSort CloseSortFunc
|
||||
|
||||
// customSequence is an optional custom sequence to set on the co-op
|
||||
// close transaction. This gives slightly more control compared to the
|
||||
// enableRBF option.
|
||||
customSequence fn.Option[uint32]
|
||||
|
||||
customLockTime fn.Option[uint32]
|
||||
}
|
||||
|
||||
// defaultCloseTxOpts returns a closeTxOpts struct with default values.
|
||||
@@ -9147,6 +9212,20 @@ func WithCustomTxSort(sorter CloseSortFunc) CloseTxOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomTxInSequence allows a caller to set a custom sequence on the sole
|
||||
// input of the co-op close tx.
|
||||
func WithCustomTxInSequence(sequence uint32) CloseTxOpt {
|
||||
return func(o *closeTxOpts) {
|
||||
o.customSequence = fn.Some(sequence)
|
||||
}
|
||||
}
|
||||
|
||||
func WithCustomTxLockTime(lockTime uint32) CloseTxOpt {
|
||||
return func(o *closeTxOpts) {
|
||||
o.customLockTime = fn.Some(lockTime)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCooperativeCloseTx creates a transaction which if signed by both
|
||||
// parties, then broadcast cooperatively closes an active channel. The creation
|
||||
// of the closure transaction is modified by a boolean indicating if the party
|
||||
@@ -9169,6 +9248,11 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
|
||||
fundingTxIn.Sequence = mempool.MaxRBFSequence
|
||||
}
|
||||
|
||||
// Otherwise, a custom sequence might be specified.
|
||||
opts.customSequence.WhenSome(func(sequence uint32) {
|
||||
fundingTxIn.Sequence = sequence
|
||||
})
|
||||
|
||||
// Construct the transaction to perform a cooperative closure of the
|
||||
// channel. In the event that one side doesn't have any settled funds
|
||||
// within the channel then a refund output for that particular side can
|
||||
@@ -9176,6 +9260,12 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
|
||||
closeTx := wire.NewMsgTx(2)
|
||||
closeTx.AddTxIn(&fundingTxIn)
|
||||
|
||||
opts.customLockTime.WhenSome(func(lockTime uint32) {
|
||||
closeTx.LockTime = lockTime
|
||||
})
|
||||
|
||||
// TODO(roasbeef): needs support for dropping inputs
|
||||
|
||||
// Create both cooperative closure outputs, properly respecting the
|
||||
// dust limits of both parties.
|
||||
var localOutputIdx fn.Option[int]
|
||||
|
||||
+104
-21
@@ -770,29 +770,66 @@ func TestCommitHTLCSigCustomRecordSize(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestCooperativeChannelClosure checks that the coop close process finishes
|
||||
// with an agreement from both parties, and that the final balances of the
|
||||
// close tx check out.
|
||||
// with an agreement from both parties, and that the final balances of the close
|
||||
// tx check out.
|
||||
func TestCooperativeChannelClosure(t *testing.T) {
|
||||
t.Run("tweakless", func(t *testing.T) {
|
||||
testCoopClose(t, &coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit,
|
||||
testCases := []struct {
|
||||
name string
|
||||
closeCase coopCloseTestCase
|
||||
}{
|
||||
{
|
||||
name: "tweakless",
|
||||
closeCase: coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anchors",
|
||||
closeCase: coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anchors local pay",
|
||||
closeCase: coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
customPayer: fn.Some(lntypes.Local),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anchors remote pay",
|
||||
closeCase: coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
customPayer: fn.Some(lntypes.Remote),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
testCoopClose(t, testCase.closeCase)
|
||||
})
|
||||
})
|
||||
t.Run("anchors", func(t *testing.T) {
|
||||
testCoopClose(t, &coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type coopCloseTestCase struct {
|
||||
chanType channeldb.ChannelType
|
||||
anchorAmt btcutil.Amount
|
||||
|
||||
customPayer fn.Option[lntypes.ChannelParty]
|
||||
}
|
||||
|
||||
func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
type closeOpts struct {
|
||||
aliceOpts []ChanCloseOpt
|
||||
bobOpts []ChanCloseOpt
|
||||
}
|
||||
|
||||
func testCoopClose(t *testing.T, testCase coopCloseTestCase) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a test channel which will be used for the duration of this
|
||||
@@ -813,17 +850,38 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
bobChannel.channelState.LocalCommitment.FeePerKw,
|
||||
)
|
||||
|
||||
customPayer := testCase.customPayer
|
||||
|
||||
closeOpts := fn.MapOptionZ(
|
||||
customPayer, func(payer lntypes.ChannelParty) closeOpts {
|
||||
// If the local party is paying then from Alice's PoV,
|
||||
// then local party is paying. From Bob's PoV, the
|
||||
// remote party is paying. If the remote party is, then
|
||||
// the opposite is true.
|
||||
return closeOpts{
|
||||
aliceOpts: []ChanCloseOpt{
|
||||
WithCustomPayer(payer),
|
||||
},
|
||||
bobOpts: []ChanCloseOpt{
|
||||
WithCustomPayer(payer.CounterParty()),
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// We'll start with both Alice and Bob creating a new close proposal
|
||||
// with the same fee.
|
||||
aliceFee := aliceChannel.CalcFee(aliceFeeRate)
|
||||
aliceSig, _, _, err := aliceChannel.CreateCloseProposal(
|
||||
aliceFee, aliceDeliveryScript, bobDeliveryScript,
|
||||
closeOpts.aliceOpts...,
|
||||
)
|
||||
require.NoError(t, err, "unable to create alice coop close proposal")
|
||||
|
||||
bobFee := bobChannel.CalcFee(bobFeeRate)
|
||||
bobSig, _, _, err := bobChannel.CreateCloseProposal(
|
||||
bobFee, bobDeliveryScript, aliceDeliveryScript,
|
||||
closeOpts.bobOpts...,
|
||||
)
|
||||
require.NoError(t, err, "unable to create bob coop close proposal")
|
||||
|
||||
@@ -832,14 +890,14 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
// transaction is well formed, and the signatures verify.
|
||||
aliceCloseTx, bobTxBalance, err := bobChannel.CompleteCooperativeClose(
|
||||
bobSig, aliceSig, bobDeliveryScript, aliceDeliveryScript,
|
||||
bobFee,
|
||||
bobFee, closeOpts.bobOpts...,
|
||||
)
|
||||
require.NoError(t, err, "unable to complete alice cooperative close")
|
||||
bobCloseSha := aliceCloseTx.TxHash()
|
||||
|
||||
bobCloseTx, aliceTxBalance, err := aliceChannel.CompleteCooperativeClose(
|
||||
aliceSig, bobSig, aliceDeliveryScript, bobDeliveryScript,
|
||||
aliceFee,
|
||||
aliceFee, closeOpts.aliceOpts...,
|
||||
)
|
||||
require.NoError(t, err, "unable to complete bob cooperative close")
|
||||
aliceCloseSha := bobCloseTx.TxHash()
|
||||
@@ -848,18 +906,43 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
t.Fatalf("alice and bob close transactions don't match: %v", err)
|
||||
}
|
||||
|
||||
// Finally, make sure the final balances are correct from both's
|
||||
// perspective.
|
||||
type chanFees struct {
|
||||
alice btcutil.Amount
|
||||
bob btcutil.Amount
|
||||
}
|
||||
|
||||
// Compute the closing fees for each party. If not specified, Alice will
|
||||
// always pay the fees. Otherwise, it depends on who the payer is.
|
||||
closeFees := fn.MapOption(func(payer lntypes.ChannelParty) chanFees {
|
||||
var alice, bob btcutil.Amount
|
||||
|
||||
switch payer {
|
||||
case lntypes.Local:
|
||||
alice = bobFee
|
||||
bob = 0
|
||||
case lntypes.Remote:
|
||||
bob = bobFee
|
||||
alice = 0
|
||||
}
|
||||
|
||||
return chanFees{
|
||||
alice: alice,
|
||||
bob: bob,
|
||||
}
|
||||
})(testCase.customPayer).UnwrapOr(chanFees{alice: bobFee})
|
||||
|
||||
// Finally, make sure the final balances are correct from both
|
||||
// perspectives.
|
||||
aliceBalance := aliceChannel.channelState.LocalCommitment.
|
||||
LocalBalance.ToSatoshis()
|
||||
|
||||
// The commit balance have had the initiator's (Alice) commitfee and
|
||||
// The commit balance have had the initiator's (Alice) commit fee and
|
||||
// any anchors subtracted, so add that back to the final expected
|
||||
// balance. Alice also pays the coop close fee, so that must be
|
||||
// subtracted.
|
||||
commitFee := aliceChannel.channelState.LocalCommitment.CommitFee
|
||||
expBalanceAlice := aliceBalance + commitFee +
|
||||
testCase.anchorAmt - bobFee
|
||||
testCase.anchorAmt - closeFees.alice
|
||||
if aliceTxBalance != expBalanceAlice {
|
||||
t.Fatalf("expected balance %v got %v", expBalanceAlice,
|
||||
aliceTxBalance)
|
||||
@@ -868,7 +951,7 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
// Bob is not the initiator, so his final balance should simply be
|
||||
// equal to the latest commitment balance.
|
||||
expBalanceBob := bobChannel.channelState.LocalCommitment.
|
||||
LocalBalance.ToSatoshis()
|
||||
LocalBalance.ToSatoshis() - closeFees.bob
|
||||
if bobTxBalance != expBalanceBob {
|
||||
t.Fatalf("expected bob's balance to be %v got %v",
|
||||
expBalanceBob, bobTxBalance)
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
package lnwallet
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
"pgregory.net/rapid"
|
||||
)
|
||||
|
||||
// genValidAmount generates valid bitcoin amounts (non-negative).
|
||||
func genValidAmount(t *rapid.T, label string) btcutil.Amount {
|
||||
return btcutil.Amount(
|
||||
rapid.Int64Range(
|
||||
100_000, 21_000_000*100_000_000,
|
||||
).Draw(t, label),
|
||||
)
|
||||
}
|
||||
|
||||
// genCoopCloseFee generates a reasonable non-zero cooperative close fee.
|
||||
func genCoopCloseFee(t *rapid.T) btcutil.Amount {
|
||||
// Generate a fee between 250-10000 sats which is a reasonable range for
|
||||
// closing transactions
|
||||
return btcutil.Amount(
|
||||
rapid.Int64Range(250, 10_000).Draw(t, "coop_close_fee"),
|
||||
)
|
||||
}
|
||||
|
||||
// genChannelType generates various channel types, ensuring good coverage of
|
||||
// different channel configurations including anchor outputs and other features.
|
||||
func genChannelType(t *rapid.T) channeldb.ChannelType {
|
||||
var chanType channeldb.ChannelType
|
||||
|
||||
// For each bit, decide randomly if it should be set.
|
||||
bits := []channeldb.ChannelType{
|
||||
channeldb.DualFunderBit,
|
||||
channeldb.SingleFunderTweaklessBit,
|
||||
channeldb.NoFundingTxBit,
|
||||
channeldb.AnchorOutputsBit,
|
||||
channeldb.FrozenBit,
|
||||
channeldb.ZeroHtlcTxFeeBit,
|
||||
channeldb.LeaseExpirationBit,
|
||||
channeldb.ZeroConfBit,
|
||||
channeldb.ScidAliasChanBit,
|
||||
channeldb.ScidAliasFeatureBit,
|
||||
channeldb.SimpleTaprootFeatureBit,
|
||||
channeldb.TapscriptRootBit,
|
||||
}
|
||||
|
||||
// Helper to bias towards setting specific bits more frequently.
|
||||
setBit := func(bit channeldb.ChannelType, probability int) {
|
||||
bitRange := rapid.IntRange(0, 100)
|
||||
label := "bit_" + strconv.FormatUint(uint64(bit), 2)
|
||||
if bitRange.Draw(t, label) < probability {
|
||||
chanType |= bit
|
||||
}
|
||||
}
|
||||
|
||||
// We want to ensure good coverage of anchor outputs since they affect
|
||||
// the balance calculation directly. We'll set the anchor bit with a 50%
|
||||
// chance.
|
||||
setBit(channeldb.AnchorOutputsBit, 50)
|
||||
|
||||
// For other bits, use varying probabilities to ensure good
|
||||
// distribution.
|
||||
for _, bit := range bits {
|
||||
// The anchor bit was already set above so we can skip it here.
|
||||
if bit == channeldb.AnchorOutputsBit {
|
||||
continue
|
||||
}
|
||||
|
||||
// Some bits are related, so we'll make sure we capture that
|
||||
// dep.
|
||||
switch bit {
|
||||
case channeldb.TapscriptRootBit:
|
||||
// If we have TapscriptRootBit, we must have
|
||||
// SimpleTaprootFeatureBit.
|
||||
if chanType&channeldb.SimpleTaprootFeatureBit != 0 {
|
||||
// 70% chance if taproot is enabled.
|
||||
setBit(bit, 70)
|
||||
}
|
||||
|
||||
case channeldb.DualFunderBit:
|
||||
// 40% chance of dual funding.
|
||||
setBit(bit, 40)
|
||||
|
||||
default:
|
||||
// 30% chance for other bits.
|
||||
setBit(bit, 30)
|
||||
}
|
||||
}
|
||||
|
||||
return chanType
|
||||
}
|
||||
|
||||
// genFeePayer generates optional fee payer.
|
||||
func genFeePayer(t *rapid.T) fn.Option[lntypes.ChannelParty] {
|
||||
if !rapid.Bool().Draw(t, "has_fee_payer") {
|
||||
return fn.None[lntypes.ChannelParty]()
|
||||
}
|
||||
|
||||
if rapid.Bool().Draw(t, "is_local") {
|
||||
return fn.Some(lntypes.Local)
|
||||
}
|
||||
|
||||
return fn.Some(lntypes.Remote)
|
||||
}
|
||||
|
||||
// genCommitFee generates a reasonable non-zero commitment fee.
|
||||
func genCommitFee(t *rapid.T) btcutil.Amount {
|
||||
// Generate a reasonable commit fee between 100-5000 sats
|
||||
return btcutil.Amount(
|
||||
rapid.Int64Range(100, 5_000).Draw(t, "commit_fee"),
|
||||
)
|
||||
}
|
||||
|
||||
// TestCoopCloseBalance tests fundamental properties of CoopCloseBalance. This
|
||||
// ensures that the closing fee is always subtracted from the correct balance,
|
||||
// amongst other properties.
|
||||
func TestCoopCloseBalance(tt *testing.T) {
|
||||
tt.Parallel()
|
||||
|
||||
rapid.Check(tt, func(t *rapid.T) {
|
||||
require := require.New(t)
|
||||
|
||||
// Generate test inputs
|
||||
chanType := genChannelType(t)
|
||||
isInitiator := rapid.Bool().Draw(t, "is_initiator")
|
||||
|
||||
// Generate amounts using specific generators
|
||||
coopCloseFee := genCoopCloseFee(t)
|
||||
ourBalance := genValidAmount(t, "local balance")
|
||||
theirBalance := genValidAmount(t, "remote balance")
|
||||
feePayer := genFeePayer(t)
|
||||
commitFee := genCommitFee(t)
|
||||
|
||||
ourFinal, theirFinal, err := CoopCloseBalance(
|
||||
chanType, isInitiator, coopCloseFee,
|
||||
ourBalance, theirBalance, commitFee, feePayer,
|
||||
)
|
||||
|
||||
// Property 1: If inputs are non-negative, we either get valid
|
||||
// outputs or an error.
|
||||
if err != nil {
|
||||
// On error, final balances should be 0
|
||||
require.Zero(
|
||||
ourFinal,
|
||||
"expected zero our_balance on error",
|
||||
)
|
||||
require.Zero(
|
||||
theirFinal,
|
||||
"expected zero their_balance on error",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Property 2: Final balances should be non-negative.
|
||||
require.GreaterOrEqual(
|
||||
ourFinal, btcutil.Amount(0),
|
||||
"our final balance should be non-negative",
|
||||
)
|
||||
require.GreaterOrEqual(
|
||||
theirFinal, btcutil.Amount(0),
|
||||
"their final balance should be non-negative",
|
||||
)
|
||||
|
||||
// Property 3: Total balance should be conserved minus fees.
|
||||
initialTotal := ourBalance + theirBalance
|
||||
initialTotal += commitFee
|
||||
|
||||
if chanType.HasAnchors() {
|
||||
initialTotal += 2 * AnchorSize
|
||||
}
|
||||
|
||||
finalTotal := ourFinal + theirFinal + coopCloseFee
|
||||
require.Equal(
|
||||
initialTotal, finalTotal,
|
||||
"total balance should be conserved",
|
||||
)
|
||||
|
||||
// Property 4: When feePayer is specified, that party's balance
|
||||
// should be reduced by exactly the coopCloseFee.
|
||||
if feePayer.IsSome() {
|
||||
payer := feePayer.UnwrapOrFail(tt)
|
||||
|
||||
if payer == lntypes.Local {
|
||||
require.LessOrEqual(
|
||||
ourBalance-(ourFinal+coopCloseFee),
|
||||
btcutil.Amount(0),
|
||||
"local balance reduced by more than fee", //nolint:ll
|
||||
)
|
||||
} else {
|
||||
require.LessOrEqual(
|
||||
theirBalance-(theirFinal+coopCloseFee),
|
||||
btcutil.Amount(0),
|
||||
"remote balance reduced by more than fee", //nolint:ll
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Property 5: For anchor channels, verify the correct final
|
||||
// balance factors in the anchor amount.
|
||||
if chanType.HasAnchors() {
|
||||
// The initiator delta is the commit fee plus anchor
|
||||
// amount.
|
||||
initiatorDelta := commitFee + 2*AnchorSize
|
||||
|
||||
// Default to initiator paying unless explicitly
|
||||
// specified.
|
||||
isLocalPaying := isInitiator
|
||||
if feePayer.IsSome() {
|
||||
isLocalPaying = feePayer.UnwrapOrFail(tt) ==
|
||||
lntypes.Local
|
||||
}
|
||||
|
||||
if isInitiator {
|
||||
expectedBalance := ourBalance + initiatorDelta
|
||||
if isLocalPaying {
|
||||
expectedBalance -= coopCloseFee
|
||||
}
|
||||
|
||||
require.Equal(expectedBalance, ourFinal,
|
||||
"initiator (local) balance incorrect")
|
||||
} else {
|
||||
// They are the initiator
|
||||
expectedBalance := theirBalance + initiatorDelta
|
||||
if !isLocalPaying {
|
||||
expectedBalance -= coopCloseFee
|
||||
}
|
||||
|
||||
require.Equal(expectedBalance, theirFinal,
|
||||
"initiator (remote) balance incorrect")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+25
-6
@@ -1033,8 +1033,9 @@ func CreateCommitTx(chanType channeldb.ChannelType,
|
||||
// CoopCloseBalance returns the final balances that should be used to create
|
||||
// the cooperative close tx, given the channel type and transaction fee.
|
||||
func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
||||
coopCloseFee, ourBalance, theirBalance,
|
||||
commitFee btcutil.Amount) (btcutil.Amount, btcutil.Amount, error) {
|
||||
coopCloseFee, ourBalance, theirBalance, commitFee btcutil.Amount,
|
||||
feePayer fn.Option[lntypes.ChannelParty],
|
||||
) (btcutil.Amount, btcutil.Amount, error) {
|
||||
|
||||
// We'll make sure we account for the complete balance by adding the
|
||||
// current dangling commitment fee to the balance of the initiator.
|
||||
@@ -1046,16 +1047,34 @@ func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
||||
initiatorDelta += 2 * AnchorSize
|
||||
}
|
||||
|
||||
// The initiator will pay the full coop close fee, subtract that value
|
||||
// from their balance.
|
||||
initiatorDelta -= coopCloseFee
|
||||
|
||||
// To start with, we'll add the anchor and/or commitment fee to the
|
||||
// balance of the initiator.
|
||||
if isInitiator {
|
||||
ourBalance += initiatorDelta
|
||||
} else {
|
||||
theirBalance += initiatorDelta
|
||||
}
|
||||
|
||||
// With the initiator's balance credited, we'll now subtract the closing
|
||||
// fee from the closing party. By default, the initiator pays the full
|
||||
// amount, but this can be overridden by the feePayer option.
|
||||
defaultPayer := func() lntypes.ChannelParty {
|
||||
if isInitiator {
|
||||
return lntypes.Local
|
||||
}
|
||||
|
||||
return lntypes.Remote
|
||||
}()
|
||||
payer := feePayer.UnwrapOr(defaultPayer)
|
||||
|
||||
// Based on the payer computed above, we'll subtract the closing fee.
|
||||
switch payer {
|
||||
case lntypes.Local:
|
||||
ourBalance -= coopCloseFee
|
||||
case lntypes.Remote:
|
||||
theirBalance -= coopCloseFee
|
||||
}
|
||||
|
||||
// During fee negotiation it should always be verified that the
|
||||
// initiator can pay the proposed fee, but we do a sanity check just to
|
||||
// be sure here.
|
||||
|
||||
@@ -34,9 +34,9 @@ type ClosingComplete struct {
|
||||
// channel would like to propose for the close transaction.
|
||||
FeeSatoshis btcutil.Amount
|
||||
|
||||
// Sequence is the sequence number to be used in the input spending the
|
||||
// LockTime is the locktime number to be used in the input spending the
|
||||
// funding transaction.
|
||||
Sequence uint32
|
||||
LockTime uint32
|
||||
|
||||
// ClosingSigs houses the 3 possible signatures that can be sent.
|
||||
ClosingSigs
|
||||
@@ -79,7 +79,7 @@ func decodeClosingSigs(c *ClosingSigs, tlvRecords ExtraOpaqueData) error {
|
||||
// passed io.Reader.
|
||||
func (c *ClosingComplete) Decode(r io.Reader, _ uint32) error {
|
||||
// First, read out all the fields that are hard coded into the message.
|
||||
err := ReadElements(r, &c.ChannelID, &c.FeeSatoshis, &c.Sequence)
|
||||
err := ReadElements(r, &c.ChannelID, &c.FeeSatoshis, &c.LockTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -129,7 +129,7 @@ func (c *ClosingComplete) Encode(w *bytes.Buffer, _ uint32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := WriteUint32(w, c.Sequence); err != nil {
|
||||
if err := WriteUint32(w, c.LockTime); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1352,7 +1352,7 @@ func TestLightningWireProtocol(t *testing.T) {
|
||||
req := ClosingComplete{
|
||||
ChannelID: ChannelID(c),
|
||||
FeeSatoshis: btcutil.Amount(r.Int63()),
|
||||
Sequence: uint32(r.Int63()),
|
||||
LockTime: uint32(r.Int63()),
|
||||
ClosingSigs: ClosingSigs{},
|
||||
}
|
||||
|
||||
|
||||
@@ -2275,6 +2275,13 @@ func messageSummary(msg lnwire.Message) string {
|
||||
return fmt.Sprintf("chan_id=%v, script=%x", msg.ChannelID,
|
||||
msg.Address[:])
|
||||
|
||||
case *lnwire.ClosingComplete:
|
||||
return fmt.Sprintf("chan_id=%v, fee_sat=%v, locktime=%v",
|
||||
msg.ChannelID, msg.FeeSatoshis, msg.LockTime)
|
||||
|
||||
case *lnwire.ClosingSig:
|
||||
return fmt.Sprintf("chan_id=%v", msg.ChannelID)
|
||||
|
||||
case *lnwire.ClosingSigned:
|
||||
return fmt.Sprintf("chan_id=%v, fee_sat=%v", msg.ChannelID,
|
||||
msg.FeeSatoshis)
|
||||
|
||||
+21
-36
@@ -34,11 +34,11 @@ type EmittedEvent[Event any] struct {
|
||||
// InternalEvent is an optional internal event that is to be routed
|
||||
// back to the target state. This enables state to trigger one or many
|
||||
// state transitions without a new external event.
|
||||
InternalEvent fn.Option[[]Event]
|
||||
InternalEvent []Event
|
||||
|
||||
// ExternalEvent is an optional external event that is to be sent to
|
||||
// the daemon for dispatch. Usually, this is some form of I/O.
|
||||
ExternalEvents fn.Option[DaemonEventSet]
|
||||
ExternalEvents DaemonEventSet
|
||||
}
|
||||
|
||||
// StateTransition is a state transition type. It denotes the next state to go
|
||||
@@ -573,46 +573,31 @@ func (s *StateMachine[Event, Env]) applyEvents(currentState State[Event, Env],
|
||||
// With the event processed, we'll process any
|
||||
// new daemon events that were emitted as part
|
||||
// of this new state transition.
|
||||
//
|
||||
//nolint:ll
|
||||
err := fn.MapOptionZ(events.ExternalEvents, func(dEvents DaemonEventSet) error {
|
||||
log.Debugf("FSM(%v): processing "+
|
||||
"daemon %v daemon events",
|
||||
s.cfg.Env.Name(), len(dEvents))
|
||||
|
||||
for _, dEvent := range dEvents {
|
||||
err := s.executeDaemonEvent(
|
||||
dEvent,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, dEvent := range events.ExternalEvents {
|
||||
err := s.executeDaemonEvent(
|
||||
dEvent,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Next, we'll add any new emitted events to
|
||||
// our event queue.
|
||||
// Next, we'll add any new emitted events to our
|
||||
// event queue.
|
||||
//
|
||||
//nolint:ll
|
||||
events.InternalEvent.WhenSome(func(es []Event) {
|
||||
for _, inEvent := range es {
|
||||
log.Debugf("FSM(%v): adding "+
|
||||
"new internal event "+
|
||||
"to queue: %v",
|
||||
s.cfg.Env.Name(),
|
||||
lnutils.SpewLogClosure(
|
||||
inEvent,
|
||||
),
|
||||
)
|
||||
for _, inEvent := range events.InternalEvent {
|
||||
log.Debugf("FSM(%v): adding "+
|
||||
"new internal event "+
|
||||
"to queue: %v",
|
||||
s.cfg.Env.Name(),
|
||||
lnutils.SpewLogClosure(
|
||||
inEvent,
|
||||
),
|
||||
)
|
||||
|
||||
eventQueue.Enqueue(inEvent)
|
||||
}
|
||||
})
|
||||
eventQueue.Enqueue(inEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -80,9 +80,7 @@ func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
|
||||
return &StateTransition[dummyEvents, *dummyEnv]{
|
||||
NextState: &dummyStateStart{},
|
||||
NewEvents: fn.Some(EmittedEvent[dummyEvents]{
|
||||
InternalEvent: fn.Some(
|
||||
[]dummyEvents{&goToFin{}},
|
||||
),
|
||||
InternalEvent: []dummyEvents{&goToFin{}},
|
||||
}),
|
||||
}, nil
|
||||
|
||||
@@ -114,13 +112,13 @@ func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
|
||||
canSend: d.canSend,
|
||||
},
|
||||
NewEvents: fn.Some(EmittedEvent[dummyEvents]{
|
||||
ExternalEvents: fn.Some(DaemonEventSet{
|
||||
ExternalEvents: DaemonEventSet{
|
||||
sendEvent, sendEvent2,
|
||||
&BroadcastTxn{
|
||||
Tx: &wire.MsgTx{},
|
||||
Label: "test",
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user