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
|
// 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
|
// our peer. If they do not match, we use the disconnect function provided to
|
||||||
// disconnect from the peer.
|
// disconnect from the peer.
|
||||||
func validateShutdownScript(disconnect func() error, upfrontScript,
|
func validateShutdownScript(upfrontScript, peerScript lnwire.DeliveryAddress,
|
||||||
peerScript lnwire.DeliveryAddress, netParams *chaincfg.Params) error {
|
netParams *chaincfg.Params) error {
|
||||||
|
|
||||||
// Either way, we'll make sure that the script passed meets our
|
// Either way, we'll make sure that the script passed meets our
|
||||||
// standards. The upfrontScript should have already been checked at an
|
// 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 "+
|
chancloserLog.Warnf("peer's script: %x does not match upfront "+
|
||||||
"shutdown script: %x", peerScript, upfrontScript)
|
"shutdown script: %x", peerScript, upfrontScript)
|
||||||
|
|
||||||
// Disconnect from the peer because they have violated option upfront
|
|
||||||
// shutdown.
|
|
||||||
if err := disconnect(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrUpfrontShutdownScriptMismatch
|
return ErrUpfrontShutdownScriptMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,7 +624,6 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
|
|||||||
// If the remote node opened the channel with option upfront
|
// If the remote node opened the channel with option upfront
|
||||||
// shutdown script, check that the script they provided matches.
|
// shutdown script, check that the script they provided matches.
|
||||||
if err := validateShutdownScript(
|
if err := validateShutdownScript(
|
||||||
c.cfg.Disconnect,
|
|
||||||
c.cfg.Channel.RemoteUpfrontShutdownScript(),
|
c.cfg.Channel.RemoteUpfrontShutdownScript(),
|
||||||
msg.Address, c.cfg.ChainParams,
|
msg.Address, c.cfg.ChainParams,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -681,7 +674,6 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
|
|||||||
// If the remote node opened the channel with option upfront
|
// If the remote node opened the channel with option upfront
|
||||||
// shutdown script, check that the script they provided matches.
|
// shutdown script, check that the script they provided matches.
|
||||||
if err := validateShutdownScript(
|
if err := validateShutdownScript(
|
||||||
c.cfg.Disconnect,
|
|
||||||
c.cfg.Channel.RemoteUpfrontShutdownScript(),
|
c.cfg.Channel.RemoteUpfrontShutdownScript(),
|
||||||
msg.Address, c.cfg.ChainParams,
|
msg.Address, c.cfg.ChainParams,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ func TestMaybeMatchScript(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
err := validateShutdownScript(
|
err := validateShutdownScript(
|
||||||
func() error { return nil }, test.upfrontScript,
|
test.upfrontScript, test.shutdownScript,
|
||||||
test.shutdownScript, &chaincfg.SimNetParams,
|
&chaincfg.SimNetParams,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != test.expectedErr {
|
if err != test.expectedErr {
|
||||||
@@ -189,7 +189,7 @@ func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress {
|
|||||||
|
|
||||||
func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
|
func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
|
||||||
localScript, remoteScript []byte,
|
localScript, remoteScript []byte,
|
||||||
_ ...lnwallet.ChanCloseOpt) (input.Signature, *chainhash.Hash,
|
_ ...lnwallet.ChanCloseOpt) (input.Signature, *wire.MsgTx,
|
||||||
btcutil.Amount, error) {
|
btcutil.Amount, error) {
|
||||||
|
|
||||||
if m.chanType.IsTaproot() {
|
if m.chanType.IsTaproot() {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package chancloser
|
|||||||
import (
|
import (
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/fn/v2"
|
"github.com/lightningnetwork/lnd/fn/v2"
|
||||||
@@ -100,7 +99,7 @@ type Channel interface { //nolint:interfacebloat
|
|||||||
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
||||||
closeOpt ...lnwallet.ChanCloseOpt,
|
closeOpt ...lnwallet.ChanCloseOpt,
|
||||||
) (
|
) (
|
||||||
input.Signature, *chainhash.Hash, btcutil.Amount, error)
|
input.Signature, *wire.MsgTx, btcutil.Amount, error)
|
||||||
|
|
||||||
// CompleteCooperativeClose persistently "completes" the cooperative
|
// CompleteCooperativeClose persistently "completes" the cooperative
|
||||||
// close by producing a fully signed co-op close transaction.
|
// 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
|
// transaction outputs. If this isn't set, then the default BIP-69
|
||||||
// sorting is used.
|
// sorting is used.
|
||||||
customSort CloseSortFunc
|
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
|
// 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
|
// CreateCloseProposal is used by both parties in a cooperative channel close
|
||||||
// workflow to generate proposed close transactions and signatures. This method
|
// workflow to generate proposed close transactions and signatures. This method
|
||||||
// should only be executed once all pending HTLCs (if any) on the channel have
|
// should only be executed once all pending HTLCs (if any) on the channel have
|
||||||
@@ -8244,22 +8275,23 @@ func WithCustomCoopSort(sorter CloseSortFunc) ChanCloseOpt {
|
|||||||
// returned.
|
// returned.
|
||||||
func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||||
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
||||||
closeOpts ...ChanCloseOpt) (input.Signature, *chainhash.Hash,
|
closeOpts ...ChanCloseOpt) (input.Signature, *wire.MsgTx,
|
||||||
btcutil.Amount, error) {
|
btcutil.Amount, error) {
|
||||||
|
|
||||||
lc.Lock()
|
lc.Lock()
|
||||||
defer lc.Unlock()
|
defer lc.Unlock()
|
||||||
|
|
||||||
// If we're already closing the channel, then ignore this request.
|
|
||||||
if lc.isClosed {
|
|
||||||
return nil, nil, 0, ErrChanClosing
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := defaultCloseOpts()
|
opts := defaultCloseOpts()
|
||||||
for _, optFunc := range closeOpts {
|
for _, optFunc := range closeOpts {
|
||||||
optFunc(opts)
|
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
|
// Get the final balances after subtracting the proposed fee, taking
|
||||||
// care not to persist the adjusted balance, as the feeRate may change
|
// care not to persist the adjusted balance, as the feeRate may change
|
||||||
// during the channel closing process.
|
// during the channel closing process.
|
||||||
@@ -8269,6 +8301,7 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
|||||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||||
lc.channelState.LocalCommitment.CommitFee,
|
lc.channelState.LocalCommitment.CommitFee,
|
||||||
|
opts.customPayer,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, 0, err
|
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(
|
closeTx, err := CreateCooperativeCloseTx(
|
||||||
fundingTxIn(lc.channelState), lc.channelState.LocalChanCfg.DustLimit,
|
fundingTxIn(lc.channelState), lc.channelState.LocalChanCfg.DustLimit,
|
||||||
lc.channelState.RemoteChanCfg.DustLimit, ourBalance, theirBalance,
|
lc.channelState.RemoteChanCfg.DustLimit, ourBalance, theirBalance,
|
||||||
@@ -8332,8 +8377,8 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeTXID := closeTx.TxHash()
|
return sig, closeTx, ourBalance, nil
|
||||||
return sig, &closeTXID, ourBalance, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompleteCooperativeClose completes the cooperative closure of the target
|
// CompleteCooperativeClose completes the cooperative closure of the target
|
||||||
@@ -8352,17 +8397,17 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
|||||||
lc.Lock()
|
lc.Lock()
|
||||||
defer lc.Unlock()
|
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()
|
opts := defaultCloseOpts()
|
||||||
for _, optFunc := range closeOpts {
|
for _, optFunc := range closeOpts {
|
||||||
optFunc(opts)
|
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.
|
// Get the final balances after subtracting the proposed fee.
|
||||||
ourBalance, theirBalance, err := CoopCloseBalance(
|
ourBalance, theirBalance, err := CoopCloseBalance(
|
||||||
lc.channelState.ChanType, lc.channelState.IsInitiator,
|
lc.channelState.ChanType, lc.channelState.IsInitiator,
|
||||||
@@ -8370,6 +8415,7 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
|||||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||||
lc.channelState.LocalCommitment.CommitFee,
|
lc.channelState.LocalCommitment.CommitFee,
|
||||||
|
opts.customPayer,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
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
|
// Create the transaction used to return the current settled balance
|
||||||
// on this active channel back to both parties. In this current model,
|
// on this active channel back to both parties. In this current model,
|
||||||
// the initiator pays full fees for the cooperative close transaction.
|
// 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
|
// transaction outputs. If this isn't set, then the default BIP-69
|
||||||
// sorting is used.
|
// sorting is used.
|
||||||
customSort CloseSortFunc
|
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.
|
// 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
|
// CreateCooperativeCloseTx creates a transaction which if signed by both
|
||||||
// parties, then broadcast cooperatively closes an active channel. The creation
|
// parties, then broadcast cooperatively closes an active channel. The creation
|
||||||
// of the closure transaction is modified by a boolean indicating if the party
|
// 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
|
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
|
// Construct the transaction to perform a cooperative closure of the
|
||||||
// channel. In the event that one side doesn't have any settled funds
|
// 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
|
// 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 := wire.NewMsgTx(2)
|
||||||
closeTx.AddTxIn(&fundingTxIn)
|
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
|
// Create both cooperative closure outputs, properly respecting the
|
||||||
// dust limits of both parties.
|
// dust limits of both parties.
|
||||||
var localOutputIdx fn.Option[int]
|
var localOutputIdx fn.Option[int]
|
||||||
|
|||||||
+104
-21
@@ -770,29 +770,66 @@ func TestCommitHTLCSigCustomRecordSize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestCooperativeChannelClosure checks that the coop close process finishes
|
// TestCooperativeChannelClosure checks that the coop close process finishes
|
||||||
// with an agreement from both parties, and that the final balances of the
|
// with an agreement from both parties, and that the final balances of the close
|
||||||
// close tx check out.
|
// tx check out.
|
||||||
func TestCooperativeChannelClosure(t *testing.T) {
|
func TestCooperativeChannelClosure(t *testing.T) {
|
||||||
t.Run("tweakless", func(t *testing.T) {
|
testCases := []struct {
|
||||||
testCoopClose(t, &coopCloseTestCase{
|
name string
|
||||||
chanType: channeldb.SingleFunderTweaklessBit,
|
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 {
|
type coopCloseTestCase struct {
|
||||||
chanType channeldb.ChannelType
|
chanType channeldb.ChannelType
|
||||||
anchorAmt btcutil.Amount
|
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()
|
t.Parallel()
|
||||||
|
|
||||||
// Create a test channel which will be used for the duration of this
|
// 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,
|
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
|
// We'll start with both Alice and Bob creating a new close proposal
|
||||||
// with the same fee.
|
// with the same fee.
|
||||||
aliceFee := aliceChannel.CalcFee(aliceFeeRate)
|
aliceFee := aliceChannel.CalcFee(aliceFeeRate)
|
||||||
aliceSig, _, _, err := aliceChannel.CreateCloseProposal(
|
aliceSig, _, _, err := aliceChannel.CreateCloseProposal(
|
||||||
aliceFee, aliceDeliveryScript, bobDeliveryScript,
|
aliceFee, aliceDeliveryScript, bobDeliveryScript,
|
||||||
|
closeOpts.aliceOpts...,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "unable to create alice coop close proposal")
|
require.NoError(t, err, "unable to create alice coop close proposal")
|
||||||
|
|
||||||
bobFee := bobChannel.CalcFee(bobFeeRate)
|
bobFee := bobChannel.CalcFee(bobFeeRate)
|
||||||
bobSig, _, _, err := bobChannel.CreateCloseProposal(
|
bobSig, _, _, err := bobChannel.CreateCloseProposal(
|
||||||
bobFee, bobDeliveryScript, aliceDeliveryScript,
|
bobFee, bobDeliveryScript, aliceDeliveryScript,
|
||||||
|
closeOpts.bobOpts...,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "unable to create bob coop close proposal")
|
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.
|
// transaction is well formed, and the signatures verify.
|
||||||
aliceCloseTx, bobTxBalance, err := bobChannel.CompleteCooperativeClose(
|
aliceCloseTx, bobTxBalance, err := bobChannel.CompleteCooperativeClose(
|
||||||
bobSig, aliceSig, bobDeliveryScript, aliceDeliveryScript,
|
bobSig, aliceSig, bobDeliveryScript, aliceDeliveryScript,
|
||||||
bobFee,
|
bobFee, closeOpts.bobOpts...,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "unable to complete alice cooperative close")
|
require.NoError(t, err, "unable to complete alice cooperative close")
|
||||||
bobCloseSha := aliceCloseTx.TxHash()
|
bobCloseSha := aliceCloseTx.TxHash()
|
||||||
|
|
||||||
bobCloseTx, aliceTxBalance, err := aliceChannel.CompleteCooperativeClose(
|
bobCloseTx, aliceTxBalance, err := aliceChannel.CompleteCooperativeClose(
|
||||||
aliceSig, bobSig, aliceDeliveryScript, bobDeliveryScript,
|
aliceSig, bobSig, aliceDeliveryScript, bobDeliveryScript,
|
||||||
aliceFee,
|
aliceFee, closeOpts.aliceOpts...,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "unable to complete bob cooperative close")
|
require.NoError(t, err, "unable to complete bob cooperative close")
|
||||||
aliceCloseSha := bobCloseTx.TxHash()
|
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)
|
t.Fatalf("alice and bob close transactions don't match: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, make sure the final balances are correct from both's
|
type chanFees struct {
|
||||||
// perspective.
|
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.
|
aliceBalance := aliceChannel.channelState.LocalCommitment.
|
||||||
LocalBalance.ToSatoshis()
|
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
|
// any anchors subtracted, so add that back to the final expected
|
||||||
// balance. Alice also pays the coop close fee, so that must be
|
// balance. Alice also pays the coop close fee, so that must be
|
||||||
// subtracted.
|
// subtracted.
|
||||||
commitFee := aliceChannel.channelState.LocalCommitment.CommitFee
|
commitFee := aliceChannel.channelState.LocalCommitment.CommitFee
|
||||||
expBalanceAlice := aliceBalance + commitFee +
|
expBalanceAlice := aliceBalance + commitFee +
|
||||||
testCase.anchorAmt - bobFee
|
testCase.anchorAmt - closeFees.alice
|
||||||
if aliceTxBalance != expBalanceAlice {
|
if aliceTxBalance != expBalanceAlice {
|
||||||
t.Fatalf("expected balance %v got %v", expBalanceAlice,
|
t.Fatalf("expected balance %v got %v", expBalanceAlice,
|
||||||
aliceTxBalance)
|
aliceTxBalance)
|
||||||
@@ -868,7 +951,7 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
|||||||
// Bob is not the initiator, so his final balance should simply be
|
// Bob is not the initiator, so his final balance should simply be
|
||||||
// equal to the latest commitment balance.
|
// equal to the latest commitment balance.
|
||||||
expBalanceBob := bobChannel.channelState.LocalCommitment.
|
expBalanceBob := bobChannel.channelState.LocalCommitment.
|
||||||
LocalBalance.ToSatoshis()
|
LocalBalance.ToSatoshis() - closeFees.bob
|
||||||
if bobTxBalance != expBalanceBob {
|
if bobTxBalance != expBalanceBob {
|
||||||
t.Fatalf("expected bob's balance to be %v got %v",
|
t.Fatalf("expected bob's balance to be %v got %v",
|
||||||
expBalanceBob, bobTxBalance)
|
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
|
// CoopCloseBalance returns the final balances that should be used to create
|
||||||
// the cooperative close tx, given the channel type and transaction fee.
|
// the cooperative close tx, given the channel type and transaction fee.
|
||||||
func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
||||||
coopCloseFee, ourBalance, theirBalance,
|
coopCloseFee, ourBalance, theirBalance, commitFee btcutil.Amount,
|
||||||
commitFee btcutil.Amount) (btcutil.Amount, btcutil.Amount, error) {
|
feePayer fn.Option[lntypes.ChannelParty],
|
||||||
|
) (btcutil.Amount, btcutil.Amount, error) {
|
||||||
|
|
||||||
// We'll make sure we account for the complete balance by adding the
|
// We'll make sure we account for the complete balance by adding the
|
||||||
// current dangling commitment fee to the balance of the initiator.
|
// current dangling commitment fee to the balance of the initiator.
|
||||||
@@ -1046,16 +1047,34 @@ func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
|||||||
initiatorDelta += 2 * AnchorSize
|
initiatorDelta += 2 * AnchorSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// The initiator will pay the full coop close fee, subtract that value
|
// To start with, we'll add the anchor and/or commitment fee to the
|
||||||
// from their balance.
|
// balance of the initiator.
|
||||||
initiatorDelta -= coopCloseFee
|
|
||||||
|
|
||||||
if isInitiator {
|
if isInitiator {
|
||||||
ourBalance += initiatorDelta
|
ourBalance += initiatorDelta
|
||||||
} else {
|
} else {
|
||||||
theirBalance += initiatorDelta
|
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
|
// During fee negotiation it should always be verified that the
|
||||||
// initiator can pay the proposed fee, but we do a sanity check just to
|
// initiator can pay the proposed fee, but we do a sanity check just to
|
||||||
// be sure here.
|
// be sure here.
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ type ClosingComplete struct {
|
|||||||
// channel would like to propose for the close transaction.
|
// channel would like to propose for the close transaction.
|
||||||
FeeSatoshis btcutil.Amount
|
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.
|
// funding transaction.
|
||||||
Sequence uint32
|
LockTime uint32
|
||||||
|
|
||||||
// ClosingSigs houses the 3 possible signatures that can be sent.
|
// ClosingSigs houses the 3 possible signatures that can be sent.
|
||||||
ClosingSigs
|
ClosingSigs
|
||||||
@@ -79,7 +79,7 @@ func decodeClosingSigs(c *ClosingSigs, tlvRecords ExtraOpaqueData) error {
|
|||||||
// passed io.Reader.
|
// passed io.Reader.
|
||||||
func (c *ClosingComplete) Decode(r io.Reader, _ uint32) error {
|
func (c *ClosingComplete) Decode(r io.Reader, _ uint32) error {
|
||||||
// First, read out all the fields that are hard coded into the message.
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ func (c *ClosingComplete) Encode(w *bytes.Buffer, _ uint32) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := WriteUint32(w, c.Sequence); err != nil {
|
if err := WriteUint32(w, c.LockTime); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1352,7 +1352,7 @@ func TestLightningWireProtocol(t *testing.T) {
|
|||||||
req := ClosingComplete{
|
req := ClosingComplete{
|
||||||
ChannelID: ChannelID(c),
|
ChannelID: ChannelID(c),
|
||||||
FeeSatoshis: btcutil.Amount(r.Int63()),
|
FeeSatoshis: btcutil.Amount(r.Int63()),
|
||||||
Sequence: uint32(r.Int63()),
|
LockTime: uint32(r.Int63()),
|
||||||
ClosingSigs: ClosingSigs{},
|
ClosingSigs: ClosingSigs{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2275,6 +2275,13 @@ func messageSummary(msg lnwire.Message) string {
|
|||||||
return fmt.Sprintf("chan_id=%v, script=%x", msg.ChannelID,
|
return fmt.Sprintf("chan_id=%v, script=%x", msg.ChannelID,
|
||||||
msg.Address[:])
|
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:
|
case *lnwire.ClosingSigned:
|
||||||
return fmt.Sprintf("chan_id=%v, fee_sat=%v", msg.ChannelID,
|
return fmt.Sprintf("chan_id=%v, fee_sat=%v", msg.ChannelID,
|
||||||
msg.FeeSatoshis)
|
msg.FeeSatoshis)
|
||||||
|
|||||||
+21
-36
@@ -34,11 +34,11 @@ type EmittedEvent[Event any] struct {
|
|||||||
// InternalEvent is an optional internal event that is to be routed
|
// InternalEvent is an optional internal event that is to be routed
|
||||||
// back to the target state. This enables state to trigger one or many
|
// back to the target state. This enables state to trigger one or many
|
||||||
// state transitions without a new external event.
|
// 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
|
// ExternalEvent is an optional external event that is to be sent to
|
||||||
// the daemon for dispatch. Usually, this is some form of I/O.
|
// 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
|
// 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
|
// With the event processed, we'll process any
|
||||||
// new daemon events that were emitted as part
|
// new daemon events that were emitted as part
|
||||||
// of this new state transition.
|
// of this new state transition.
|
||||||
//
|
for _, dEvent := range events.ExternalEvents {
|
||||||
//nolint:ll
|
err := s.executeDaemonEvent(
|
||||||
err := fn.MapOptionZ(events.ExternalEvents, func(dEvents DaemonEventSet) error {
|
dEvent,
|
||||||
log.Debugf("FSM(%v): processing "+
|
)
|
||||||
"daemon %v daemon events",
|
if err != nil {
|
||||||
s.cfg.Env.Name(), len(dEvents))
|
return err
|
||||||
|
|
||||||
for _, dEvent := range dEvents {
|
|
||||||
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
|
// Next, we'll add any new emitted events to our
|
||||||
// our event queue.
|
// event queue.
|
||||||
//
|
//
|
||||||
//nolint:ll
|
//nolint:ll
|
||||||
events.InternalEvent.WhenSome(func(es []Event) {
|
for _, inEvent := range events.InternalEvent {
|
||||||
for _, inEvent := range es {
|
log.Debugf("FSM(%v): adding "+
|
||||||
log.Debugf("FSM(%v): adding "+
|
"new internal event "+
|
||||||
"new internal event "+
|
"to queue: %v",
|
||||||
"to queue: %v",
|
s.cfg.Env.Name(),
|
||||||
s.cfg.Env.Name(),
|
lnutils.SpewLogClosure(
|
||||||
lnutils.SpewLogClosure(
|
inEvent,
|
||||||
inEvent,
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
eventQueue.Enqueue(inEvent)
|
eventQueue.Enqueue(inEvent)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -80,9 +80,7 @@ func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
|
|||||||
return &StateTransition[dummyEvents, *dummyEnv]{
|
return &StateTransition[dummyEvents, *dummyEnv]{
|
||||||
NextState: &dummyStateStart{},
|
NextState: &dummyStateStart{},
|
||||||
NewEvents: fn.Some(EmittedEvent[dummyEvents]{
|
NewEvents: fn.Some(EmittedEvent[dummyEvents]{
|
||||||
InternalEvent: fn.Some(
|
InternalEvent: []dummyEvents{&goToFin{}},
|
||||||
[]dummyEvents{&goToFin{}},
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
@@ -114,13 +112,13 @@ func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
|
|||||||
canSend: d.canSend,
|
canSend: d.canSend,
|
||||||
},
|
},
|
||||||
NewEvents: fn.Some(EmittedEvent[dummyEvents]{
|
NewEvents: fn.Some(EmittedEvent[dummyEvents]{
|
||||||
ExternalEvents: fn.Some(DaemonEventSet{
|
ExternalEvents: DaemonEventSet{
|
||||||
sendEvent, sendEvent2,
|
sendEvent, sendEvent2,
|
||||||
&BroadcastTxn{
|
&BroadcastTxn{
|
||||||
Tx: &wire.MsgTx{},
|
Tx: &wire.MsgTx{},
|
||||||
Label: "test",
|
Label: "test",
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
}),
|
}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user