diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 05eb46a68be..2e5f54258ae 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -230,6 +230,10 @@ type ChainArbitratorConfig struct { // AuxResolver is an optional interface that can be used to modify the // way contracts are resolved. AuxResolver fn.Option[lnwallet.AuxContractResolver] + + // AuxCloser is an optional interface that can be used to finalize + // cooperative channel closes. + AuxCloser fn.Option[AuxChanCloser] } // ChainArbitrator is a sub-system that oversees the on-chain resolution of all @@ -1138,6 +1142,7 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error extractStateNumHint: lnwallet.GetStateNumHint, auxLeafStore: c.cfg.AuxLeafStore, auxResolver: c.cfg.AuxResolver, + auxCloser: c.cfg.AuxCloser, }, ) if err != nil { @@ -1315,6 +1320,7 @@ func (c *ChainArbitrator) loadOpenChannels() error { extractStateNumHint: lnwallet.GetStateNumHint, auxLeafStore: c.cfg.AuxLeafStore, auxResolver: c.cfg.AuxResolver, + auxCloser: c.cfg.AuxCloser, }, ) if err != nil { diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 9c566fd6b51..847c29dda54 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -24,6 +24,7 @@ import ( "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/types" "github.com/lightningnetwork/lnd/lnwire" ) @@ -37,6 +38,14 @@ const ( maxCommitPointPollTimeout = 10 * time.Minute ) +// AuxChanCloser is used to allow an external caller to finalize a cooperative +// channel close. +type AuxChanCloser interface { + // FinalizeClose is called after the close transaction has been agreed + // upon and confirmed. + FinalizeClose(desc types.AuxCloseDesc, closeTx *wire.MsgTx) error +} + // LocalUnilateralCloseInfo encapsulates all the information we need to act on // a local force close that gets confirmed. type LocalUnilateralCloseInfo struct { @@ -229,6 +238,9 @@ type chainWatcherConfig struct { // auxResolver is used to supplement contract resolution. auxResolver fn.Option[lnwallet.AuxContractResolver] + + // auxCloser is used to finalize cooperative closes. + auxCloser fn.Option[AuxChanCloser] } // chainWatcher is a system that's assigned to every active channel. The duty @@ -986,6 +998,74 @@ func (c *chainWatcher) toSelfAmount(tx *wire.MsgTx) btcutil.Amount { return btcutil.Amount(fn.Sum(vals)) } +// finalizeCoopClose calls the aux closer to finalize a cooperative close +// transaction that has been confirmed on-chain. +func (c *chainWatcher) finalizeCoopClose(aux AuxChanCloser, + closeTx *wire.MsgTx) error { + + chanState := c.cfg.chanState + + // Get the shutdown info to extract the local delivery script. + shutdown, err := chanState.ShutdownInfo() + if err != nil { + return fmt.Errorf("get shutdown info: %w", err) + } + + // Build the AuxShutdownReq. + req := types.AuxShutdownReq{ + ChanPoint: chanState.FundingOutpoint, + ShortChanID: chanState.ShortChanID(), + Initiator: chanState.IsInitiator, + CommitBlob: chanState.LocalCommitment.CustomBlob, + FundingBlob: chanState.CustomBlob, + } + + // Shutdown info must be present in order to continue. + if shutdown.IsNone() { + return fmt.Errorf("failed to finalize coop close, shutdown " + + "info missing") + } + + // Extract close outputs from the transaction. We need to identify + // which outputs belong to local vs remote parties. + var localCloseOutput, remoteCloseOutput fn.Option[types.CloseOutput] + + // Get the delivery scripts for the local party. + var localDeliveryScript lnwire.DeliveryAddress + shutdown.WhenSome(func(s channeldb.ShutdownInfo) { + localDeliveryScript = s.DeliveryScript.Val + }) + + // Scan through the close transaction outputs to identify local and + // remote outputs. + for _, out := range closeTx.TxOut { + if len(localDeliveryScript) > 0 && + slices.Equal(out.PkScript, localDeliveryScript) { + + localCloseOutput = fn.Some(types.CloseOutput{ + Amt: btcutil.Amount(out.Value), + PkScript: out.PkScript, + DustLimit: chanState.LocalChanCfg.DustLimit, + }) + } else { + // This must be the remote output. + remoteCloseOutput = fn.Some(types.CloseOutput{ + Amt: btcutil.Amount(out.Value), + PkScript: out.PkScript, + DustLimit: chanState.RemoteChanCfg.DustLimit, + }) + } + } + + desc := types.AuxCloseDesc{ + AuxShutdownReq: req, + LocalCloseOutput: localCloseOutput, + RemoteCloseOutput: remoteCloseOutput, + } + + return aux.FinalizeClose(desc, closeTx) +} + // dispatchCooperativeClose processed a detect cooperative channel closure. // We'll use the spending transaction to locate our output within the // transaction, then clean up the database state. We'll also dispatch a @@ -1036,6 +1116,17 @@ func (c *chainWatcher) dispatchCooperativeClose(commitSpend *chainntnfs.SpendDet ChannelCloseSummary: closeSummary, } + // If we have an aux closer, finalize the cooperative close now that + // it's confirmed. + err = fn.MapOptionZ( + c.cfg.auxCloser, func(aux AuxChanCloser) error { + return c.finalizeCoopClose(aux, broadcastTx) + }, + ) + if err != nil { + return fmt.Errorf("finalize coop close: %w", err) + } + // With the event processed, we'll now notify all subscribers of the // event. c.Lock() diff --git a/lnwallet/chancloser/aux_closer.go b/lnwallet/chancloser/aux_closer.go index 62f475dd434..e810b360c67 100644 --- a/lnwallet/chancloser/aux_closer.go +++ b/lnwallet/chancloser/aux_closer.go @@ -1,78 +1,13 @@ package chancloser import ( - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/types" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/tlv" ) -// CloseOutput represents an output that should be included in the close -// transaction. -type CloseOutput struct { - // Amt is the amount of the output. - Amt btcutil.Amount - - // DustLimit is the dust limit for the local node. - DustLimit btcutil.Amount - - // PkScript is the script that should be used to pay to the output. - PkScript []byte - - // ShutdownRecords is the set of custom records that may result in - // extra close outputs being added. - ShutdownRecords lnwire.CustomRecords -} - -// AuxShutdownReq is used to request a set of extra custom records to include -// in the shutdown message. -type AuxShutdownReq struct { - // ChanPoint is the channel point of the channel that is being shut - // down. - ChanPoint wire.OutPoint - - // ShortChanID is the short channel ID of the channel that is being - // closed. - ShortChanID lnwire.ShortChannelID - - // Initiator is true if the local node is the initiator of the channel. - Initiator bool - - // InternalKey is the internal key for the shutdown addr. This will - // only be set for taproot shutdown addrs. - InternalKey fn.Option[btcec.PublicKey] - - // CommitBlob is the blob that was included in the last commitment. - CommitBlob fn.Option[tlv.Blob] - - // FundingBlob is the blob that was included in the funding state. - FundingBlob fn.Option[tlv.Blob] -} - -// AuxCloseDesc is used to describe the channel close that is being performed. -type AuxCloseDesc struct { - AuxShutdownReq - - // CloseFee is the closing fee to be paid for this state. - CloseFee btcutil.Amount - - // CommitFee is the fee that was paid for the last commitment. - CommitFee btcutil.Amount - - // LocalCloseOutput is the output that the local node should be paid - // to. This is None if the local party will not have an output on the - // co-op close transaction. - LocalCloseOutput fn.Option[CloseOutput] - - // RemoteCloseOutput is the output that the remote node should be paid - // to. This will be None if the remote party will not have an output on - // the co-op close transaction. - RemoteCloseOutput fn.Option[CloseOutput] -} - // AuxCloseOutputs is used to specify extra outputs that should be used when // constructing the co-op close transaction. type AuxCloseOutputs struct { @@ -91,14 +26,15 @@ type AuxCloseOutputs struct { type AuxChanCloser interface { // ShutdownBlob returns the set of custom records that should be // included in the shutdown message. - ShutdownBlob(req AuxShutdownReq) (fn.Option[lnwire.CustomRecords], + ShutdownBlob(req types.AuxShutdownReq) (fn.Option[lnwire.CustomRecords], error) // AuxCloseOutputs returns the set of custom outputs that should be used // to construct the co-op close transaction. - AuxCloseOutputs(desc AuxCloseDesc) (fn.Option[AuxCloseOutputs], error) + AuxCloseOutputs(desc types.AuxCloseDesc) (fn.Option[AuxCloseOutputs], + error) // FinalizeClose is called after the close transaction has been agreed // upon. - FinalizeClose(desc AuxCloseDesc, closeTx *wire.MsgTx) error + FinalizeClose(desc types.AuxCloseDesc, closeTx *wire.MsgTx) error } diff --git a/lnwallet/chancloser/chancloser.go b/lnwallet/chancloser/chancloser.go index cc6ccffa8cd..6e5eaf39356 100644 --- a/lnwallet/chancloser/chancloser.go +++ b/lnwallet/chancloser/chancloser.go @@ -19,6 +19,7 @@ import ( "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwallet/types" "github.com/lightningnetwork/lnd/lnwire" ) @@ -239,12 +240,12 @@ type ChanCloser struct { // localCloseOutput is the local output on the closing transaction that // the local party should be paid to. This will only be populated if the // local balance isn't dust. - localCloseOutput fn.Option[CloseOutput] + localCloseOutput fn.Option[types.CloseOutput] // remoteCloseOutput is the remote output on the closing transaction // that the remote party should be paid to. This will only be populated // if the remote balance isn't dust. - remoteCloseOutput fn.Option[CloseOutput] + remoteCloseOutput fn.Option[types.CloseOutput] // auxOutputs are the optional additional outputs that might be added to // the closing transaction. @@ -378,14 +379,17 @@ func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) { // At this point, we'll check to see if we have any custom records to // add to the shutdown message. err := fn.MapOptionZ(c.cfg.AuxCloser, func(a AuxChanCloser) error { - shutdownCustomRecords, err := a.ShutdownBlob(AuxShutdownReq{ - ChanPoint: c.chanPoint, - ShortChanID: c.cfg.Channel.ShortChanID(), - Initiator: c.cfg.Channel.IsInitiator(), - InternalKey: c.localInternalKey, - CommitBlob: c.cfg.Channel.LocalCommitmentBlob(), - FundingBlob: c.cfg.Channel.FundingBlob(), - }) + channel := c.cfg.Channel + shutdownCustomRecords, err := a.ShutdownBlob( + types.AuxShutdownReq{ + ChanPoint: c.chanPoint, + ShortChanID: channel.ShortChanID(), + Initiator: channel.IsInitiator(), + InternalKey: c.localInternalKey, + CommitBlob: channel.LocalCommitmentBlob(), + FundingBlob: channel.FundingBlob(), + }, + ) if err != nil { return err } @@ -442,7 +446,7 @@ func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) { // it might still carry value in custom channel terms. _, dustAmt := c.cfg.Channel.LocalBalanceDust() localBalance, _ := c.cfg.Channel.CommitBalances() - c.localCloseOutput = fn.Some(CloseOutput{ + c.localCloseOutput = fn.Some(types.CloseOutput{ Amt: localBalance, DustLimit: dustAmt, PkScript: c.localDeliveryScript, @@ -519,12 +523,12 @@ func (c *ChanCloser) NegotiationHeight() uint32 { } // LocalCloseOutput returns the local close output. -func (c *ChanCloser) LocalCloseOutput() fn.Option[CloseOutput] { +func (c *ChanCloser) LocalCloseOutput() fn.Option[types.CloseOutput] { return c.localCloseOutput } // RemoteCloseOutput returns the remote close output. -func (c *ChanCloser) RemoteCloseOutput() fn.Option[CloseOutput] { +func (c *ChanCloser) RemoteCloseOutput() fn.Option[types.CloseOutput] { return c.remoteCloseOutput } @@ -590,7 +594,7 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) ( // terms, it might still carry value in custom channel terms. _, dustAmt := c.cfg.Channel.RemoteBalanceDust() _, remoteBalance := c.cfg.Channel.CommitBalances() - c.remoteCloseOutput = fn.Some(CloseOutput{ + c.remoteCloseOutput = fn.Some(types.CloseOutput{ Amt: remoteBalance, DustLimit: dustAmt, PkScript: msg.Address, @@ -970,33 +974,6 @@ func (c *ChanCloser) ReceiveClosingSigned( //nolint:funlen } c.closingTx = closeTx - // If there's an aux chan closer, then we'll finalize with it - // before we write to disk. - err = fn.MapOptionZ( - c.cfg.AuxCloser, func(aux AuxChanCloser) error { - channel := c.cfg.Channel - //nolint:ll - req := AuxShutdownReq{ - ChanPoint: c.chanPoint, - ShortChanID: c.cfg.Channel.ShortChanID(), - InternalKey: c.localInternalKey, - Initiator: channel.IsInitiator(), - CommitBlob: channel.LocalCommitmentBlob(), - FundingBlob: channel.FundingBlob(), - } - desc := AuxCloseDesc{ - AuxShutdownReq: req, - LocalCloseOutput: c.localCloseOutput, - RemoteCloseOutput: c.remoteCloseOutput, - } - - return aux.FinalizeClose(desc, closeTx) - }, - ) - if err != nil { - return noClosing, err - } - // Before publishing the closing tx, we persist it to the // database, such that it can be republished if something goes // wrong. @@ -1053,7 +1030,7 @@ func (c *ChanCloser) auxCloseOutputs( var closeOuts fn.Option[AuxCloseOutputs] err := fn.MapOptionZ(c.cfg.AuxCloser, func(aux AuxChanCloser) error { - req := AuxShutdownReq{ + req := types.AuxShutdownReq{ ChanPoint: c.chanPoint, ShortChanID: c.cfg.Channel.ShortChanID(), InternalKey: c.localInternalKey, @@ -1061,7 +1038,7 @@ func (c *ChanCloser) auxCloseOutputs( CommitBlob: c.cfg.Channel.LocalCommitmentBlob(), FundingBlob: c.cfg.Channel.FundingBlob(), } - outs, err := aux.AuxCloseOutputs(AuxCloseDesc{ + outs, err := aux.AuxCloseOutputs(types.AuxCloseDesc{ AuxShutdownReq: req, CloseFee: closeFee, CommitFee: c.cfg.Channel.CommitFee(), diff --git a/lnwallet/types/close_types.go b/lnwallet/types/close_types.go new file mode 100644 index 00000000000..490b08f249e --- /dev/null +++ b/lnwallet/types/close_types.go @@ -0,0 +1,73 @@ +package types + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +// CloseOutput represents an output that should be included in the close +// transaction. +type CloseOutput struct { + // Amt is the amount of the output. + Amt btcutil.Amount + + // DustLimit is the dust limit for the local node. + DustLimit btcutil.Amount + + // PkScript is the script that should be used to pay to the output. + PkScript []byte + + // ShutdownRecords is the set of custom records that may result in + // extra close outputs being added. + ShutdownRecords lnwire.CustomRecords +} + +// AuxShutdownReq is used to request a set of extra custom records to include +// in the shutdown message. +type AuxShutdownReq struct { + // ChanPoint is the channel point of the channel that is being shut + // down. + ChanPoint wire.OutPoint + + // ShortChanID is the short channel ID of the channel that is being + // closed. + ShortChanID lnwire.ShortChannelID + + // Initiator is true if the local node is the initiator of the channel. + Initiator bool + + // InternalKey is the internal key for the shutdown addr. This will + // only be set for taproot shutdown addrs. + InternalKey fn.Option[btcec.PublicKey] + + // CommitBlob is the blob that was included in the last commitment. + CommitBlob fn.Option[tlv.Blob] + + // FundingBlob is the blob that was included in the funding state. + FundingBlob fn.Option[tlv.Blob] +} + +// AuxCloseDesc is used to describe the channel close that is being performed. +type AuxCloseDesc struct { + AuxShutdownReq + + // CloseFee is the closing fee to be paid for this state. + CloseFee btcutil.Amount + + // CommitFee is the fee that was paid for the last commitment. + CommitFee btcutil.Amount + + // LocalCloseOutput is the output that the local node should be paid + // to. This is None if the local party will not have an output on the + // co-op close transaction. + LocalCloseOutput fn.Option[CloseOutput] + + // RemoteCloseOutput is the output that the remote node should be paid + // to. This will be None if the remote party will not have an output on + // the co-op close transaction. + RemoteCloseOutput fn.Option[CloseOutput] +} diff --git a/peer/brontide.go b/peer/brontide.go index 9191cbb2eeb..7376a59033a 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -44,6 +44,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chancloser" + "github.com/lightningnetwork/lnd/lnwallet/types" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/msgmux" "github.com/lightningnetwork/lnd/netann" @@ -168,12 +169,12 @@ type ChannelCloseUpdate struct { // LocalCloseOutput is an optional, additional output on the closing // transaction that the local party should be paid to. This will only be // populated if the local balance isn't dust. - LocalCloseOutput fn.Option[chancloser.CloseOutput] + LocalCloseOutput fn.Option[types.CloseOutput] // RemoteCloseOutput is an optional, additional output on the closing // transaction that the remote party should be paid to. This will only // be populated if the remote balance isn't dust. - RemoteCloseOutput fn.Option[chancloser.CloseOutput] + RemoteCloseOutput fn.Option[types.CloseOutput] // AuxOutputs is an optional set of additional outputs that might be // included in the closing transaction. These are used for custom diff --git a/rpcserver.go b/rpcserver.go index d3d3c518014..44458792e03 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -68,6 +68,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chancloser" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" + "github.com/lightningnetwork/lnd/lnwallet/types" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/macaroons" paymentsdb "github.com/lightningnetwork/lnd/payments/db" @@ -3050,7 +3051,7 @@ func createRPCCloseUpdate( err := fn.MapOptionZ( u.LocalCloseOutput, - func(closeOut chancloser.CloseOutput) error { + func(closeOut types.CloseOutput) error { cr, err := closeOut.ShutdownRecords.Serialize() if err != nil { return fmt.Errorf("error serializing "+ @@ -3075,7 +3076,7 @@ func createRPCCloseUpdate( err = fn.MapOptionZ( u.RemoteCloseOutput, - func(closeOut chancloser.CloseOutput) error { + func(closeOut types.CloseOutput) error { cr, err := closeOut.ShutdownRecords.Serialize() if err != nil { return fmt.Errorf("error serializing "+ diff --git a/server.go b/server.go index a2d36eb8653..6361e743408 100644 --- a/server.go +++ b/server.go @@ -60,6 +60,7 @@ import ( "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + chcl "github.com/lightningnetwork/lnd/lnwallet/chancloser" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwallet/rpcwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -1375,6 +1376,11 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, AuxLeafStore: implCfg.AuxLeafStore, AuxSigner: implCfg.AuxSigner, AuxResolver: implCfg.AuxContractResolver, + AuxCloser: fn.MapOption( + func(c chcl.AuxChanCloser) contractcourt.AuxChanCloser { + return c + }, + )(implCfg.AuxChanCloser), }, dbs.ChanStateDB) // Select the configuration and funding parameters for Bitcoin.