From 5d886938522d77663b5d5b128d120e4ac1042a50 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 10 Jan 2023 13:42:22 +1030 Subject: [PATCH] lnd+lnwallet: fundmax flag for openchannel --- funding/manager.go | 37 ++++----- funding/manager_test.go | 164 ++++++++++++++++++++++++++++++++++++++-- lnwallet/wallet.go | 30 +++++++- 3 files changed, 206 insertions(+), 25 deletions(-) diff --git a/funding/manager.go b/funding/manager.go index 5536353b9..c55cbba32 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -4088,23 +4088,26 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { } req := &lnwallet.InitFundingReserveMsg{ - ChainHash: &msg.ChainHash, - PendingChanID: chanID, - NodeID: peerKey, - NodeAddr: msg.Peer.Address(), - SubtractFees: msg.SubtractFees, - LocalFundingAmt: localAmt, - RemoteFundingAmt: 0, - CommitFeePerKw: commitFeePerKw, - FundingFeePerKw: msg.FundingFeePerKw, - PushMSat: msg.PushAmt, - Flags: channelFlags, - MinConfs: msg.MinConfs, - CommitType: commitType, - ChanFunder: msg.ChanFunder, - ZeroConf: zeroConf, - OptionScidAlias: scid, - ScidAliasFeature: scidFeatureVal, + ChainHash: &msg.ChainHash, + PendingChanID: chanID, + NodeID: peerKey, + NodeAddr: msg.Peer.Address(), + SubtractFees: msg.SubtractFees, + LocalFundingAmt: localAmt, + RemoteFundingAmt: 0, + FundUpToMaxAmt: msg.FundUpToMaxAmt, + MinFundAmt: msg.MinFundAmt, + RemoteChanReserve: chanReserve, + CommitFeePerKw: commitFeePerKw, + FundingFeePerKw: msg.FundingFeePerKw, + PushMSat: msg.PushAmt, + Flags: channelFlags, + MinConfs: msg.MinConfs, + CommitType: commitType, + ChanFunder: msg.ChanFunder, + ZeroConf: zeroConf, + OptionScidAlias: scid, + ScidAliasFeature: scidFeatureVal, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) diff --git a/funding/manager_test.go b/funding/manager_test.go index dc40c4d8f..fb5d44cd1 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -710,7 +710,7 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt, *wire.OutPoint, *wire.MsgTx) { publ := fundChannel( - t, alice, bob, localFundingAmt, pushAmt, false, numConfs, + t, alice, bob, localFundingAmt, pushAmt, false, 0, 0, numConfs, updateChan, announceChan, nil, ) fundingOutPoint := &wire.OutPoint{ @@ -723,7 +723,8 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt, // fundChannel takes the funding process to the point where the funding // transaction is confirmed on-chain. Returns the funding tx. func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, - pushAmt btcutil.Amount, subtractFees bool, numConfs uint32, + pushAmt btcutil.Amount, subtractFees bool, fundUpToMaxAmt, + minFundAmt btcutil.Amount, numConfs uint32, //nolint:unparam updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool, chanType *lnwire.ChannelType) *wire.MsgTx { @@ -734,6 +735,8 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, TargetPubkey: bob.privKey.PubKey(), ChainHash: *fundingNetParams.GenesisHash, SubtractFees: subtractFees, + FundUpToMaxAmt: fundUpToMaxAmt, + MinFundAmt: minFundAmt, LocalFundingAmt: localFundingAmt, PushAmt: lnwire.NewMSatFromSatoshis(pushAmt), FundingFeePerKw: 1000, @@ -3730,7 +3733,7 @@ func TestFundingManagerFundAll(t *testing.T) { // Initiate a fund channel, and inspect the funding tx. pushAmt := btcutil.Amount(0) fundingTx := fundChannel( - t, alice, bob, test.spendAmt, pushAmt, true, 1, + t, alice, bob, test.spendAmt, pushAmt, true, 0, 0, 1, updateChan, true, nil, ) @@ -3761,6 +3764,157 @@ func TestFundingManagerFundAll(t *testing.T) { } } +// TestFundingManagerFundMax tests that we can initiate a funding request to use +// the maximum allowed funds remaining in the wallet. +func TestFundingManagerFundMax(t *testing.T) { + t.Parallel() + + // Helper function to create a test utxos + constructTestUtxos := func(values ...btcutil.Amount) []*lnwallet.Utxo { + var utxos []*lnwallet.Utxo + for _, value := range values { + utxos = append(utxos, &lnwallet.Utxo{ + AddressType: lnwallet.WitnessPubKey, + Value: value, + PkScript: mock.CoinPkScript, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }) + } + + return utxos + } + + tests := []struct { + name string + coins []*lnwallet.Utxo + fundUpToMaxAmt btcutil.Amount + minFundAmt btcutil.Amount + pushAmt btcutil.Amount + change bool + }{ + { + // We will spend all the funds in the wallet, and expect + // no change output due to the dust limit. + coins: constructTestUtxos( + MaxBtcFundingAmount + 1, + ), + fundUpToMaxAmt: MaxBtcFundingAmount, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: false, + }, + { + // We spend less than the funds in the wallet, so a + // change output should be created. + coins: constructTestUtxos( + 2 * MaxBtcFundingAmount, + ), + fundUpToMaxAmt: MaxBtcFundingAmount, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: true, + }, + { + // We spend less than the funds in the wallet when + // setting a smaller channel size, so a change output + // should be created. + coins: constructTestUtxos( + MaxBtcFundingAmount, + ), + fundUpToMaxAmt: MaxBtcFundingAmount / 2, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: true, + }, + { + // We are using the entirety of two inputs for the + // funding of a channel, hence expect no change output. + coins: constructTestUtxos( + MaxBtcFundingAmount/2, MaxBtcFundingAmount/2, + ), + fundUpToMaxAmt: MaxBtcFundingAmount, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: false, + }, + { + // We are using a fraction of two inputs for the funding + // of our channel, hence expect a change output. + coins: constructTestUtxos( + MaxBtcFundingAmount/2, MaxBtcFundingAmount/2, + ), + fundUpToMaxAmt: MaxBtcFundingAmount / 2, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: true, + }, + { + // We are funding a channel with half of the balance in + // our wallet hence expect a change output. Furthermore + // we push half of the funding amount to the remote end + // which we expect to succeed. + coins: constructTestUtxos(MaxBtcFundingAmount), + fundUpToMaxAmt: MaxBtcFundingAmount / 2, + minFundAmt: MinChanFundingSize / 4, + pushAmt: MaxBtcFundingAmount / 4, + change: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // We set up our mock wallet to control a list of UTXOs + // that sum to more than the max channel size. + addFunds := func(fundingCfg *Config) { + wc := fundingCfg.Wallet.WalletController + mockWc, ok := wc.(*mock.WalletController) + if ok { + mockWc.Utxos = test.coins + } + } + alice, bob := setupFundingManagers(t, addFunds) + defer tearDownFundingManagers(t, alice, bob) + + // We will consume the channel updates as we go, so no + // buffering is needed. + updateChan := make(chan *lnrpc.OpenStatusUpdate) + + // Initiate a fund channel, and inspect the funding tx. + pushAmt := test.pushAmt + fundingTx := fundChannel( + t, alice, bob, 0, pushAmt, false, + test.fundUpToMaxAmt, test.minFundAmt, 1, + updateChan, true, nil, + ) + + // Check whether the expected change output is present. + if test.change { + require.EqualValues(t, 2, len(fundingTx.TxOut)) + } + + if !test.change { + require.EqualValues(t, 1, len(fundingTx.TxOut)) + } + + // Inputs should be all funds in the wallet. + require.Equal(t, len(test.coins), len(fundingTx.TxIn)) + + for i, txIn := range fundingTx.TxIn { + require.Equal( + t, test.coins[i].OutPoint, + txIn.PreviousOutPoint, + ) + } + }) + } +} + // TestGetUpfrontShutdown tests different combinations of inputs for getting a // shutdown script. It varies whether the peer has the feature set, whether // the user has provided a script and our local configuration to test that @@ -4212,8 +4366,8 @@ func TestFundingManagerZeroConf(t *testing.T) { // Call fundChannel with the zero-conf ChannelType. fundingTx := fundChannel( - t, alice, bob, fundingAmt, pushAmt, false, 1, updateChan, true, - &channelType, + t, alice, bob, fundingAmt, pushAmt, false, 0, 0, 1, updateChan, + true, &channelType, ) fundingOp := &wire.OutPoint{ Hash: fundingTx.TxHash(), diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index fec421cc0..bde5ceaa5 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -121,6 +121,20 @@ type InitFundingReserveMsg struct { // to this channel. RemoteFundingAmt btcutil.Amount + // FundUpToMaxAmt defines if channel funding should try to add as many + // funds to the channel opening as possible up to this amount. If used, + // then MinFundAmt is treated as the minimum amount of funds that must + // be available to open the channel. If set to zero it is ignored. + FundUpToMaxAmt btcutil.Amount + + // MinFundAmt denotes the minimum channel capacity that has to be + // allocated iff the FundUpToMaxAmt is set. + MinFundAmt btcutil.Amount + + // RemoteChanReserve is the channel reserve we required for the remote + // peer. + RemoteChanReserve btcutil.Amount + // CommitFeePerKw is the starting accepted satoshis/Kw fee for the set // of initial commitment transactions. In order to ensure timely // confirmation, it is recommended that this fee should be generous, @@ -772,8 +786,12 @@ func (l *LightningWallet) CancelFundingIntent(pid [32]byte) error { // handleFundingReserveRequest processes a message intending to create, and // validate a funding reservation request. func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) { + + noFundsCommitted := req.LocalFundingAmt == 0 && + req.RemoteFundingAmt == 0 && req.FundUpToMaxAmt == 0 + // It isn't possible to create a channel with zero funds committed. - if req.LocalFundingAmt+req.RemoteFundingAmt == 0 { + if noFundsCommitted { err := ErrZeroCapacity() req.err <- err req.resp <- nil @@ -841,8 +859,14 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg // the fee rate passed in to perform coin selection. var err error fundingReq := &chanfunding.Request{ - RemoteAmt: req.RemoteFundingAmt, - LocalAmt: req.LocalFundingAmt, + RemoteAmt: req.RemoteFundingAmt, + LocalAmt: req.LocalFundingAmt, + FundUpToMaxAmt: req.FundUpToMaxAmt, + MinFundAmt: req.MinFundAmt, + RemoteChanReserve: req.RemoteChanReserve, + PushAmt: lnwire.MilliSatoshi.ToSatoshis( + req.PushMSat, + ), MinConfs: req.MinConfs, SubtractFees: req.SubtractFees, FeeRate: req.FundingFeePerKw,