From ac523f93ee44d6be17f45460952fb3cf2a0b2fdf Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Sat, 10 Jun 2023 21:26:50 +0200 Subject: [PATCH] chanfunding: adds ability to fund a selected set of coins --- lnwallet/chanfunding/assembler.go | 7 ++ lnwallet/chanfunding/wallet_assembler.go | 101 ++++++++++++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/lnwallet/chanfunding/assembler.go b/lnwallet/chanfunding/assembler.go index 33dcdb3ff..b0acd85ba 100644 --- a/lnwallet/chanfunding/assembler.go +++ b/lnwallet/chanfunding/assembler.go @@ -84,6 +84,13 @@ type Request struct { // e.g. anchor channels. WalletReserve btcutil.Amount + // Outpoints is a list of client-selected outpoints that should be used + // for funding a channel. If LocalAmt is specified then this amount is + // allocated from the sum of outpoints towards funding. If the + // FundUpToMaxAmt is specified the entirety of selected funds is + // allocated towards channel funding. + Outpoints []wire.OutPoint + // 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/wallet_assembler.go b/lnwallet/chanfunding/wallet_assembler.go index fa60eddf8..8fb2b935d 100644 --- a/lnwallet/chanfunding/wallet_assembler.go +++ b/lnwallet/chanfunding/wallet_assembler.go @@ -260,23 +260,64 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { log.Infof("Performing funding tx coin selection using %v "+ "sat/kw as fee rate", int64(r.FeeRate)) + var ( + // allCoins refers to the entirety of coins in our + // wallet that are available for funding a channel. + allCoins []Coin + + // manuallySelectedCoins refers to the client-side + // selected coins that should be considered available + // for funding a channel. + manuallySelectedCoins []Coin + err error + ) + + // Convert manually selected outpoints to coins. + manuallySelectedCoins, err = outpointsToCoins( + r.Outpoints, w.cfg.CoinSource.CoinFromOutPoint, + ) + if err != nil { + return err + } + // Find all unlocked unspent witness outputs that satisfy the // minimum number of confirmations required. Coin selection in // this function currently ignores the configured coin selection // strategy. - coins, err := w.cfg.CoinSource.ListCoins( + allCoins, err = w.cfg.CoinSource.ListCoins( r.MinConfs, math.MaxInt32, ) if err != nil { return err } + // Ensure that all manually selected coins remain unspent. + unspent := make(map[wire.OutPoint]struct{}) + for _, coin := range allCoins { + unspent[coin.OutPoint] = struct{}{} + } + for _, coin := range manuallySelectedCoins { + if _, ok := unspent[coin.OutPoint]; !ok { + return fmt.Errorf("outpoint already spent: %v", + coin.OutPoint) + } + } + var ( + coins []Coin selectedCoins []Coin localContributionAmt btcutil.Amount changeAmt btcutil.Amount ) + // If outputs were specified manually then we'll take the + // corresponding coins as basis for coin selection. Otherwise, + // all available coins from our wallet are used. + coins = allCoins + if len(manuallySelectedCoins) > 0 { + coins = manuallySelectedCoins + } + // Perform coin selection over our available, unlocked unspent // outputs in order to find enough coins to meet the funding // amount requirements. @@ -305,10 +346,45 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { // we will call the specialized coin selection function for // that. case r.FundUpToMaxAmt != 0 && r.MinFundAmt != 0: + + // We need to ensure that manually selected coins, which + // are spent entirely on the channel funding, leave + // enough funds in the wallet to cover for a reserve. + reserve := r.WalletReserve + if len(manuallySelectedCoins) > 0 { + sumCoins := func(coins []Coin) btcutil.Amount { + var sum btcutil.Amount + for _, coin := range coins { + sum += btcutil.Amount( + coin.Value, + ) + } + + return sum + } + + sumManual := sumCoins(manuallySelectedCoins) + sumAll := sumCoins(allCoins) + + // If sufficient reserve funds are available we + // don't have to provide for it during coin + // selection. The manually selected coins can be + // spent entirely on the channel funding. If + // the excess of coins cover the reserve + // partially then we have to provide for the + // rest during coin selection. + excess := sumAll - sumManual + if excess >= reserve { + reserve = 0 + } else { + reserve -= excess + } + } + selectedCoins, localContributionAmt, changeAmt, err = CoinSelectUpToAmount( r.FeeRate, r.MinFundAmt, r.FundUpToMaxAmt, - r.WalletReserve, w.cfg.DustLimit, coins, + reserve, w.cfg.DustLimit, coins, ) if err != nil { return err @@ -422,6 +498,27 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { return intent, nil } +// outpointsToCoins maps outpoints to coins in our wallet iff these coins are +// existent and returns an error otherwise. +func outpointsToCoins(outpoints []wire.OutPoint, + coinFromOutPoint func(wire.OutPoint) (*Coin, error)) ([]Coin, error) { + + var selectedCoins []Coin + for _, outpoint := range outpoints { + coin, err := coinFromOutPoint( + outpoint, + ) + if err != nil { + return nil, err + } + selectedCoins = append( + selectedCoins, *coin, + ) + } + + return selectedCoins, nil +} + // FundingTxAvailable is an empty method that an assembler can implement to // signal to callers that its able to provide the funding transaction for the // channel via the intent it returns.