From 8c1cf2170772636c1c4f5159d03bec799034014d Mon Sep 17 00:00:00 2001 From: Bjarne Magnussen Date: Wed, 22 Dec 2021 11:47:13 +0100 Subject: [PATCH] chanfunding: allow to set a reserved amount not used for funding --- lnwallet/chanfunding/assembler.go | 6 +++ lnwallet/chanfunding/coin_select.go | 55 +++++++++++++++++------- lnwallet/chanfunding/coin_select_test.go | 23 +++++++++- lnwallet/chanfunding/wallet_assembler.go | 2 +- lnwallet/wallet.go | 36 +++++++++++++--- 5 files changed, 99 insertions(+), 23 deletions(-) diff --git a/lnwallet/chanfunding/assembler.go b/lnwallet/chanfunding/assembler.go index 34ea5c611..33dcdb3ff 100644 --- a/lnwallet/chanfunding/assembler.go +++ b/lnwallet/chanfunding/assembler.go @@ -78,6 +78,12 @@ type Request struct { // responder as part of the initial channel creation. PushAmt btcutil.Amount + // WalletReserve is a reserved amount that is not used to fund the + // channel when a maximum amount defined by FundUpToMaxAmt is set. This + // is useful when a reserved wallet balance must stay available due to + // e.g. anchor channels. + WalletReserve btcutil.Amount + // MinConfs controls how many confirmations a coin need to be eligible // to be used as an input to the funding transaction. If this value is // set to zero, then zero conf outputs may be spent. diff --git a/lnwallet/chanfunding/coin_select.go b/lnwallet/chanfunding/coin_select.go index de2eb5be6..11f764373 100644 --- a/lnwallet/chanfunding/coin_select.go +++ b/lnwallet/chanfunding/coin_select.go @@ -255,11 +255,12 @@ func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt, } // CoinSelectUpToAmount attempts to select coins such that we'll select up to -// maxAmount exclusive of fees if sufficient funds are available. If -// insufficient funds are available this method selects all available coins. +// maxAmount exclusive of fees and optional reserve if sufficient funds are +// available. If insufficient funds are available this method selects all +// available coins. func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount, - dustLimit btcutil.Amount, coins []Coin) ([]Coin, btcutil.Amount, - btcutil.Amount, error) { + reserved, dustLimit btcutil.Amount, coins []Coin) ([]Coin, + btcutil.Amount, btcutil.Amount, error) { var ( // selectSubtractFee is tracking if our coin selection was @@ -269,6 +270,13 @@ func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount, outputAmount = maxAmount ) + // Get total balance from coins which we need for reserve considerations + // and fee santiy checks. + var totalBalance btcutil.Amount + for _, coin := range coins { + totalBalance += btcutil.Amount(coin.Value) + } + // First we try to select coins to create an output of the specified // maxAmount with or without a change output that covers the miner fee. selected, changeAmt, err := CoinSelect( @@ -276,25 +284,42 @@ func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount, ) var errInsufficientFunds *ErrInsufficientFunds - if errors.As(err, &errInsufficientFunds) { + if err == nil { //nolint:gocritic,ifElseChain + // If the coin selection succeeds we check if our total balance + // covers the selected set of coins including fees plus an + // optional anchor reserve. + + // First we sum up the value of all selected coins. + var sumSelected btcutil.Amount + for _, coin := range selected { + sumSelected += btcutil.Amount(coin.Value) + } + + // We then subtract the change amount from the value of all + // selected coins to obtain the actual amount that is selected. + sumSelected -= changeAmt + + // Next we check if our total balance can cover for the selected + // output plus the optional anchor reserve. + if totalBalance-sumSelected < reserved { + // If our local balance is insufficient to cover for the + // reserve we try to select an output amount that uses + // our total balance minus reserve and fees. + selectSubtractFee = true + } + } else if errors.As(err, &errInsufficientFunds) { // If the initial coin selection fails due to insufficient funds // we select our total available balance minus fees. selectSubtractFee = true - } else if err != nil { + } else { return nil, 0, 0, err } - // If we determined that our local balance is insufficient we check our - // total balance minus fees. + // If we determined that our local balance is insufficient we check + // our total balance minus fees and optional reserve. if selectSubtractFee { - // Get balance from coins. - var totalBalance btcutil.Amount - for _, coin := range coins { - totalBalance += btcutil.Amount(coin.Value) - } - selected, outputAmount, changeAmt, err = CoinSelectSubtractFees( - feeRate, totalBalance, dustLimit, coins, + feeRate, totalBalance-reserved, dustLimit, coins, ) if err != nil { return nil, 0, 0, err diff --git a/lnwallet/chanfunding/coin_select_test.go b/lnwallet/chanfunding/coin_select_test.go index 8ff89887d..3904508dd 100644 --- a/lnwallet/chanfunding/coin_select_test.go +++ b/lnwallet/chanfunding/coin_select_test.go @@ -550,6 +550,7 @@ func TestCoinSelectUpToAmount(t *testing.T) { name string minValue btcutil.Amount maxValue btcutil.Amount + reserved btcutil.Amount coins []Coin expectedInput []btcutil.Amount @@ -688,6 +689,26 @@ func TestCoinSelectUpToAmount(t *testing.T) { expectedInput: []btcutil.Amount{1 * coin}, expectedFundingAmt: 1*coin - fundingFee(feeRate, 1, false) - 1, expectedChange: 0, + }, { + // This test makes sure that if a reserved value is required + // then it is handled correctly by leaving exactly the reserved + // value as change and still maxing out the funding amount. + name: "sanity check for correct reserved amount subtract " + + "from total", + coins: []Coin{{ + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: 1 * coin, + }, + }}, + minValue: minValue, + maxValue: 1*coin - 9000, + reserved: 10000, + + expectedInput: []btcutil.Amount{1 * coin}, + expectedFundingAmt: 1*coin - + fundingFee(feeRate, 1, true) - 10000, + expectedChange: 10000, }} for _, test := range testCases { @@ -698,7 +719,7 @@ func TestCoinSelectUpToAmount(t *testing.T) { selected, localFundingAmt, changeAmt, err := CoinSelectUpToAmount( feeRate, test.minValue, test.maxValue, - dustLimit, test.coins, + test.reserved, dustLimit, test.coins, ) if len(test.expectErr) == 0 && err != nil { t.Fatalf(err.Error()) diff --git a/lnwallet/chanfunding/wallet_assembler.go b/lnwallet/chanfunding/wallet_assembler.go index 5321dbc9d..fa60eddf8 100644 --- a/lnwallet/chanfunding/wallet_assembler.go +++ b/lnwallet/chanfunding/wallet_assembler.go @@ -308,7 +308,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { selectedCoins, localContributionAmt, changeAmt, err = CoinSelectUpToAmount( r.FeeRate, r.MinFundAmt, r.FundUpToMaxAmt, - w.cfg.DustLimit, coins, + r.WalletReserve, w.cfg.DustLimit, coins, ) if err != nil { return err diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index bde5ceaa5..06f701af1 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -836,6 +836,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg localFundingAmt := req.LocalFundingAmt remoteFundingAmt := req.RemoteFundingAmt + hasAnchors := req.CommitType.HasAnchors() var ( fundingIntent chanfunding.Intent @@ -855,9 +856,33 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg // funder in the attached request to provision the inputs/outputs // that'll ultimately be used to construct the funding transaction. if !ok { + var err error + var numAnchorChans int + + // Get the number of anchor channels to determine if there is a + // reserved value that must be respected when funding up to the + // maximum amount. Since private channels (most likely) won't be + // used for routing other than the last hop, they bear a smaller + // risk that we must force close them in order to resolve a HTLC + // up/downstream. Hence we exclude them from the count of anchor + // channels in order to attribute the respective anchor amount + // to the channel capacity. + if req.FundUpToMaxAmt > 0 && req.MinFundAmt > 0 { + numAnchorChans, err = l.CurrentNumAnchorChans() + if err != nil { + req.err <- err + req.resp <- nil + return + } + + isPublic := req.Flags&lnwire.FFAnnounceChannel != 0 + if hasAnchors && isPublic { + numAnchorChans++ + } + } + // Coin selection is done on the basis of sat/kw, so we'll use // the fee rate passed in to perform coin selection. - var err error fundingReq := &chanfunding.Request{ RemoteAmt: req.RemoteFundingAmt, LocalAmt: req.LocalFundingAmt, @@ -867,6 +892,9 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg PushAmt: lnwire.MilliSatoshi.ToSatoshis( req.PushMSat, ), + WalletReserve: l.RequiredReserve( + uint32(numAnchorChans), + ), MinConfs: req.MinConfs, SubtractFees: req.SubtractFees, FeeRate: req.FundingFeePerKw, @@ -939,7 +967,6 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg // funding tx ready, so this will always pass. We'll do another check // when the PSBT has been verified. isPublic := req.Flags&lnwire.FFAnnounceChannel != 0 - hasAnchors := req.CommitType.HasAnchors() if enforceNewReservedValue { err = l.enforceNewReservedValue(fundingIntent, isPublic, hasAnchors) if err != nil { @@ -1160,10 +1187,7 @@ func (l *LightningWallet) CheckReservedValue(in []wire.OutPoint, } // We reserve a given amount for each anchor channel. - reserved := btcutil.Amount(numAnchorChans) * AnchorChanReservedValue - if reserved > MaxAnchorChanReservedValue { - reserved = MaxAnchorChanReservedValue - } + reserved := l.RequiredReserve(uint32(numAnchorChans)) if walletBalance < reserved { walletLog.Debugf("Reserved value=%v above final "+