diff --git a/config.go b/config.go index 6d5bc546a8..e9ce410354 100644 --- a/config.go +++ b/config.go @@ -537,6 +537,15 @@ type Config struct { // NoDisconnectOnPongFailure controls if we'll disconnect if a peer // doesn't respond to a pong in time. NoDisconnectOnPongFailure bool `long:"no-disconnect-on-pong-failure" description:"If true, a peer will *not* be disconnected if a pong is not received in time or is mismatched. Defaults to false, meaning peers *will* be disconnected on pong failure."` + + // UpfrontShutdownAddr specifies an address that our funds will be paid + // out to on cooperative channel close. This applies to all new channel + // opens unless overridden by an option in openchannel or by a channel + // acceptor. + // Note: If this field is set when opening a channel with a peer that + // does not advertise support for the upfront shutdown feature, the + // channel open will fail. + UpfrontShutdownAddr string `long:"upfront-shutdown-address" description:"The address to which funds will be paid out during a cooperative channel close. This applies to all channels opened after this option is set, unless overridden for a specific channel opening. Note: If this option is set, any channel opening will fail if the peer does not explicitly advertise support for the upfront-shutdown feature bit."` } // GRPCConfig holds the configuration options for the gRPC server. diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 4fb4e12c65..e7446857c8 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -37,6 +37,12 @@ # Improvements ## Functional Updates +* [Added support](https://github.com/lightningnetwork/lnd/pull/9432) for the + `upfront-shutdown-address` configuration in `lnd.conf`, allowing users to + specify an address for cooperative channel closures where funds will be sent. + This applies to both funders and fundees, with the ability to override the + value during channel opening or acceptance. + ## RPC Updates ## lncli Updates @@ -65,3 +71,4 @@ # Contributors (Alphabetical Order) * Elle Mouton +* Nishant Bansal diff --git a/funding/manager.go b/funding/manager.go index 8176e6aa22..5711ed8e22 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -573,6 +573,10 @@ type Config struct { // implementations to inject and process custom records over channel // related wire messages. AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator] + + // ShutdownScript is an optional upfront-shutdown script to which our + // funds should be paid on a cooperative close. + ShutdownScript fn.Option[lnwire.DeliveryAddress] } // Manager acts as an orchestrator/bridge between the wallet's @@ -1760,12 +1764,24 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, return } + // If the fundee didn't provide an upfront-shutdown address via + // the channel acceptor, fall back to the configured shutdown + // script (if any). + shutdownScript := acceptorResp.UpfrontShutdown + if len(shutdownScript) == 0 { + f.cfg.ShutdownScript.WhenSome( + func(script lnwire.DeliveryAddress) { + shutdownScript = script + }, + ) + } + // Check whether the peer supports upfront shutdown, and get a new // wallet address if our node is configured to set shutdown addresses by // default. We use the upfront shutdown script provided by our channel // acceptor (if any) in lieu of user input. shutdown, err := getUpfrontShutdownScript( - f.cfg.EnableUpfrontShutdown, peer, acceptorResp.UpfrontShutdown, + f.cfg.EnableUpfrontShutdown, peer, shutdownScript, f.selectShutdownScript, ) if err != nil { @@ -4849,12 +4865,23 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { } } + // If the funder did not provide an upfront-shutdown address, fall back + // to the configured shutdown script (if any). + shutdownScript := msg.ShutdownScript + if len(shutdownScript) == 0 { + f.cfg.ShutdownScript.WhenSome( + func(script lnwire.DeliveryAddress) { + shutdownScript = script + }, + ) + } + // Check whether the peer supports upfront shutdown, and get an address // which should be used (either a user specified address or a new // address from the wallet if our node is configured to set shutdown // address by default). shutdown, err := getUpfrontShutdownScript( - f.cfg.EnableUpfrontShutdown, msg.Peer, msg.ShutdownScript, + f.cfg.EnableUpfrontShutdown, msg.Peer, shutdownScript, f.selectShutdownScript, ) if err != nil { diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 3fc0fba433..f5961147d3 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -286,6 +286,10 @@ var allTestCases = []*lntest.TestCase{ Name: "open channel reorg test", TestFunc: testOpenChannelAfterReorg, }, + { + Name: "open channel with shutdown address", + TestFunc: testOpenChannelWithShutdownAddr, + }, { Name: "sign psbt", TestFunc: testSignPsbt, diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index e1959b8b04..e6cdca4d81 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -1269,3 +1269,76 @@ func testFundingManagerFundingTimeout(ht *lntest.HarnessTest) { // Cleanup the mempool by mining blocks. ht.MineBlocksAndAssertNumTxes(6, 1) } + +// testOpenChannelWithShutdownAddr verifies that if the funder or fundee +// specifies an upfront shutdown address in the config, the funds are correctly +// transferred to the specified address during channel closure. +func testOpenChannelWithShutdownAddr(ht *lntest.HarnessTest) { + const ( + // Channel funding amount in sat. + channelAmount int64 = 100000 + + // Payment amount in sat. + paymentAmount int64 = 50000 + ) + + // Create nodes for testing, ensuring Alice has sufficient initial + // funds. + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + + // Generate upfront shutdown addresses for both nodes. + aliceShutdownAddr := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{ + Type: lnrpc.AddressType_UNUSED_WITNESS_PUBKEY_HASH, + }) + bobShutdownAddr := bob.RPC.NewAddress(&lnrpc.NewAddressRequest{ + Type: lnrpc.AddressType_UNUSED_WITNESS_PUBKEY_HASH, + }) + + // Update nodes with upfront shutdown addresses and restart them. + aliceNodeArgs := []string{ + fmt.Sprintf( + "--upfront-shutdown-address=%s", + aliceShutdownAddr.Address, + ), + } + ht.RestartNodeWithExtraArgs(alice, aliceNodeArgs) + + bobNodeArgs := []string{ + fmt.Sprintf( + "--upfront-shutdown-address=%s", + bobShutdownAddr.Address, + ), + } + ht.RestartNodeWithExtraArgs(bob, bobNodeArgs) + + // Connect Alice and Bob. + ht.ConnectNodes(alice, bob) + + // Open a channel between Alice and Bob. + openChannelParams := lntest.OpenChannelParams{ + Amt: btcutil.Amount(channelAmount), + PushAmt: btcutil.Amount(paymentAmount), + } + channelPoint := ht.OpenChannel(alice, bob, openChannelParams) + + // Now close out the channel and obtain the raw closing TX. + closingTxid := ht.CloseChannel(alice, channelPoint) + closingTx := ht.GetRawTransaction(closingTxid).MsgTx() + + // Calculate Alice's updated balance. + aliceFee := ht.CalculateTxFee(closingTx) + aliceExpectedBalance := channelAmount - paymentAmount - int64(aliceFee) + + // Ensure Alice sees the change output in the list of unspent outputs. + // We expect 6 confirmed UTXOs, as 5 UTXOs of 1 BTC each were sent to + // the node during NewNodeWithCoins. + aliceUTXOConfirmed := ht.AssertNumUTXOsConfirmed(alice, 6)[0] + require.Equal(ht, aliceShutdownAddr.Address, aliceUTXOConfirmed.Address) + require.Equal(ht, aliceExpectedBalance, aliceUTXOConfirmed.AmountSat) + + // Ensure Bob see the change output in the list of unspent outputs. + bobUTXOConfirmed := ht.AssertNumUTXOsConfirmed(bob, 1)[0] + require.Equal(ht, bobShutdownAddr.Address, bobUTXOConfirmed.Address) + require.Equal(ht, paymentAmount, bobUTXOConfirmed.AmountSat) +} diff --git a/peer/brontide.go b/peer/brontide.go index 4a196fb95d..8d02ca6e53 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -3589,9 +3589,9 @@ func (p *Brontide) initNegotiateChanCloser(req *htlcswitch.ChanClose, return nil } -// chooseAddr returns the provided address if it is non-zero length, otherwise +// ChooseAddr returns the provided address if it is non-zero length, otherwise // None. -func chooseAddr(addr lnwire.DeliveryAddress) fn.Option[lnwire.DeliveryAddress] { +func ChooseAddr(addr lnwire.DeliveryAddress) fn.Option[lnwire.DeliveryAddress] { if len(addr) == 0 { return fn.None[lnwire.DeliveryAddress]() } @@ -3930,10 +3930,10 @@ func (p *Brontide) initRbfChanCloser( ChanType: channel.ChanType(), DefaultFeeRate: defaultFeePerKw.FeePerVByte(), ThawHeight: fn.Some(thawHeight), - RemoteUpfrontShutdown: chooseAddr( + RemoteUpfrontShutdown: ChooseAddr( channel.RemoteUpfrontShutdownScript(), ), - LocalUpfrontShutdown: chooseAddr( + LocalUpfrontShutdown: ChooseAddr( channel.LocalUpfrontShutdownScript(), ), NewDeliveryScript: func() (lnwire.DeliveryAddress, error) { diff --git a/sample-lnd.conf b/sample-lnd.conf index c3b3a96b1a..ed5dabd43c 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -589,6 +589,15 @@ ; pong failure. ; no-disconnect-on-pong-failure=false +; The address to which funds will be paid out during a cooperative channel +; close. This applies to all channels opened after this option is set, unless +; overridden for a specific channel opening. +; +; Note: If this option is set, any channel opening will fail if the peer does +; not explicitly advertise support for the upfront-shutdown feature bit. +; upfront-shutdown-address= + + [fee] ; Optional URL for external fee estimation. If no URL is specified, the method diff --git a/server.go b/server.go index d0289de2a3..3c011f8b0b 100644 --- a/server.go +++ b/server.go @@ -61,6 +61,7 @@ import ( "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwallet/chancloser" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwallet/rpcwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -1445,6 +1446,15 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, devCfg, reservationTimeout, zombieSweeperInterval) } + // Attempt to parse the provided upfront-shutdown address (if any). + script, err := chancloser.ParseUpfrontShutdownAddress( + cfg.UpfrontShutdownAddr, cfg.ActiveNetParams.Params, + ) + if err != nil { + return nil, fmt.Errorf("error parsing upfront shutdown: %w", + err) + } + //nolint:ll s.fundingMgr, err = funding.NewFundingManager(funding.Config{ Dev: devCfg, @@ -1623,6 +1633,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, AuxSigner: implCfg.AuxSigner, AuxResolver: implCfg.AuxContractResolver, AuxChannelNegotiator: implCfg.AuxChannelNegotiator, + ShutdownScript: peer.ChooseAddr(script), }) if err != nil { return nil, err