diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index b8fcbc776..6a1b8d116 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -1551,21 +1551,82 @@ func (w *WalletKit) fundPsbtInternalWallet(account string, return err } + // filterFn makes sure utxos which are unconfirmed and + // still used by the sweeper are not used. + filterFn := func(u *lnwallet.Utxo) bool { + // Confirmed utxos are always allowed. + if u.Confirmations > 0 { + return true + } + + // Unconfirmed utxos in use by the sweeper are + // not stable to use because they can be + // replaced. + if w.cfg.Sweeper.IsSweeperOutpoint(u.OutPoint) { + log.Warnf("Cannot use unconfirmed "+ + "utxo=%v because it is "+ + "unstable and could be "+ + "replaced", u.OutPoint) + + return false + } + + return true + } + + eligible := fn.Filter(filterFn, utxos) + // Validate all inputs against our known list of UTXOs // now. - err = verifyInputsUnspent(packet.UnsignedTx.TxIn, utxos) + err = verifyInputsUnspent( + packet.UnsignedTx.TxIn, eligible, + ) if err != nil { return err } } + // currentHeight is needed to determine whether the internal + // wallet utxo is still unconfirmed. + _, currentHeight, err := w.cfg.Chain.GetBestBlock() + if err != nil { + return fmt.Errorf("unable to retrieve current "+ + "height: %v", err) + } + + // restrictUnstableUtxos is a filter function which disallows + // the usage of unconfirmed outputs published (still in use) by + // the sweeper. + restrictUnstableUtxos := func(utxo wtxmgr.Credit) bool { + // Wallet utxos which are unmined have a height + // of -1. + if utxo.Height != -1 && utxo.Height <= currentHeight { + // Confirmed utxos are always allowed. + return true + } + + // Utxos used by the sweeper are not used for + // channel openings. + allowed := !w.cfg.Sweeper.IsSweeperOutpoint( + utxo.OutPoint, + ) + if !allowed { + log.Warnf("Cannot use unconfirmed "+ + "utxo=%v because it is "+ + "unstable and could be "+ + "replaced", utxo.OutPoint) + } + + return allowed + } + // We made sure the input from the user is as sane as possible. // We can now ask the wallet to fund the TX. This will not yet // lock any coins but might still change the wallet DB by // generating a new change address. changeIndex, err := w.cfg.Wallet.FundPsbt( - packet, minConfs, feeSatPerKW, account, - keyScope, strategy, + packet, minConfs, feeSatPerKW, account, keyScope, + strategy, restrictUnstableUtxos, ) if err != nil { return fmt.Errorf("wallet couldn't fund PSBT: %w", err) diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go index 21d78add3..52aecb382 100644 --- a/lntest/mock/walletcontroller.go +++ b/lntest/mock/walletcontroller.go @@ -208,7 +208,8 @@ func (w *WalletController) ListLeasedOutputs() ([]*base.ListLeasedOutputResult, // FundPsbt currently does nothing. func (w *WalletController) FundPsbt(*psbt.Packet, int32, chainfee.SatPerKWeight, - string, *waddrmgr.KeyScope, base.CoinSelectionStrategy) (int32, error) { + string, *waddrmgr.KeyScope, base.CoinSelectionStrategy, + func(utxo wtxmgr.Credit) bool) (int32, error) { return 0, nil } diff --git a/lnwallet/btcwallet/psbt.go b/lnwallet/btcwallet/psbt.go index 0fdf76c38..ec88cd92f 100644 --- a/lnwallet/btcwallet/psbt.go +++ b/lnwallet/btcwallet/psbt.go @@ -15,6 +15,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wtxmgr" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -60,6 +61,9 @@ var ( // imported public keys. For custom account, no key scope should be provided // as the coin selection key scope will always be used to generate the change // address. +// The function argument `allowUtxo` specifies a filter function for utxos +// during coin selection. It should return true for utxos that can be used and +// false for those that should be excluded. // // NOTE: If the packet doesn't contain any inputs, coin selection is performed // automatically. The account parameter must be non-empty as it determines which @@ -74,7 +78,8 @@ var ( func (b *BtcWallet) FundPsbt(packet *psbt.Packet, minConfs int32, feeRate chainfee.SatPerKWeight, accountName string, changeScope *waddrmgr.KeyScope, - strategy wallet.CoinSelectionStrategy) (int32, error) { + strategy wallet.CoinSelectionStrategy, + allowUtxo func(wtxmgr.Credit) bool) (int32, error) { // The fee rate is passed in using units of sat/kw, so we'll convert // this to sat/KB as the CreateSimpleTx method requires this unit. @@ -130,6 +135,9 @@ func (b *BtcWallet) FundPsbt(packet *psbt.Packet, minConfs int32, if changeScope != nil { opts = append(opts, wallet.WithCustomChangeScope(changeScope)) } + if allowUtxo != nil { + opts = append(opts, wallet.WithUtxoFilter(allowUtxo)) + } // Let the wallet handle coin selection and/or fee estimation based on // the partial TX information in the packet. diff --git a/lnwallet/interface.go b/lnwallet/interface.go index 59e6f5aab..a48e92560 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -468,7 +468,8 @@ type WalletController interface { FundPsbt(packet *psbt.Packet, minConfs int32, feeRate chainfee.SatPerKWeight, account string, changeScope *waddrmgr.KeyScope, - strategy base.CoinSelectionStrategy) (int32, error) + strategy base.CoinSelectionStrategy, + allowUtxo func(wtxmgr.Credit) bool) (int32, error) // SignPsbt expects a partial transaction with all inputs and outputs // fully declared and tries to sign all unsigned inputs that have all diff --git a/lnwallet/mock.go b/lnwallet/mock.go index 0146df57e..1873de79a 100644 --- a/lnwallet/mock.go +++ b/lnwallet/mock.go @@ -217,7 +217,8 @@ func (w *mockWalletController) ListLeasedOutputs() ( // FundPsbt currently does nothing. func (w *mockWalletController) FundPsbt(*psbt.Packet, int32, chainfee.SatPerKWeight, string, *waddrmgr.KeyScope, - base.CoinSelectionStrategy) (int32, error) { + base.CoinSelectionStrategy, func(utxo wtxmgr.Credit) bool) (int32, + error) { return 0, nil }