From 62a52b4d7c372ec8a221c18bc5de729d41a26b4d Mon Sep 17 00:00:00 2001 From: ziggie Date: Sat, 30 Mar 2024 16:55:25 +0000 Subject: [PATCH] multi: Utxo restriction single funding case. Restrict the utxo selection when opening a single internal wallet funded backed channel. --- funding/manager.go | 30 ++++++++++++++++++--- lnwallet/chanfunding/wallet_assembler.go | 3 ++- lnwallet/test/test_interface.go | 4 ++- lnwallet/wallet.go | 34 +++++++++++++++++++++--- server.go | 5 ++-- sweep/sweeper.go | 23 ++++++++++++++++ 6 files changed, 87 insertions(+), 12 deletions(-) diff --git a/funding/manager.go b/funding/manager.go index e110269a7..b012de48d 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -537,6 +537,12 @@ type Config struct { // AliasManager is an implementation of the aliasHandler interface that // abstracts away the handling of many alias functions. AliasManager aliasHandler + + // IsSweeperOutpoint queries the sweeper store for successfully + // published sweeps. This is useful to decide for the internal wallet + // backed funding flow to not use utxos still being swept by the sweeper + // subsystem. + IsSweeperOutpoint func(wire.OutPoint) bool } // Manager acts as an orchestrator/bridge between the wallet's @@ -4600,10 +4606,26 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { MinConfs: msg.MinConfs, CommitType: commitType, ChanFunder: msg.ChanFunder, - ZeroConf: zeroConf, - OptionScidAlias: scid, - ScidAliasFeature: scidFeatureVal, - Memo: msg.Memo, + // Unconfirmed Utxos which are marked by the sweeper subsystem + // are excluded from the coin selection because they are not + // final and can be RBFed by the sweeper subsystem. + AllowUtxoForFunding: func(u lnwallet.Utxo) bool { + // Utxos with at least 1 confirmation are safe to use + // for channel openings because they don't bare the risk + // of being replaced (BIP 125 RBF). + if u.Confirmations > 0 { + return true + } + + // Query the sweeper storage to make sure we don't use + // an unconfirmed utxo still in use by the sweeper + // subsystem. + return !f.cfg.IsSweeperOutpoint(u.OutPoint) + }, + ZeroConf: zeroConf, + OptionScidAlias: scid, + ScidAliasFeature: scidFeatureVal, + Memo: msg.Memo, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) diff --git a/lnwallet/chanfunding/wallet_assembler.go b/lnwallet/chanfunding/wallet_assembler.go index d4ee080bc..4f2aa759b 100644 --- a/lnwallet/chanfunding/wallet_assembler.go +++ b/lnwallet/chanfunding/wallet_assembler.go @@ -334,7 +334,8 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { } for _, coin := range manuallySelectedCoins { if _, ok := unspent[coin.OutPoint]; !ok { - return fmt.Errorf("outpoint already spent: %v", + return fmt.Errorf("outpoint already spent or "+ + "locked by another subsystem: %v", coin.OutPoint) } } diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index 401c46683..12b3aafc9 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -2963,7 +2963,9 @@ func testSingleFunderExternalFundingTx(miner *rpctest.Harness, // we'll create a new chanfunding.Assembler hacked by Alice's wallet. aliceChanFunder := chanfunding.NewWalletAssembler( chanfunding.WalletConfig{ - CoinSource: lnwallet.NewCoinSource(alice), + CoinSource: lnwallet.NewCoinSource( + alice, nil, + ), CoinSelectLocker: alice, CoinLeaser: alice, Signer: alice.Cfg.Signer, diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 7786791f9..b99b6eed2 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -184,6 +184,15 @@ type InitFundingReserveMsg struct { // used. ChanFunder chanfunding.Assembler + // AllowUtxoForFunding enables the channel funding workflow to restrict + // the selection of utxos when selecting the inputs for the channel + // opening. This does ONLY apply for the internal wallet backed channel + // opening case. + // + // NOTE: This is very useful when opening channels with unconfirmed + // inputs to make sure stable non-replaceable inputs are used. + AllowUtxoForFunding func(Utxo) bool + // ZeroConf is a boolean that is true if a zero-conf channel was // negotiated. ZeroConf bool @@ -849,7 +858,9 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg // P2WPKH dust limit and to avoid threading through two // different dust limits. cfg := chanfunding.WalletConfig{ - CoinSource: &CoinSource{l}, + CoinSource: NewCoinSource( + l, req.AllowUtxoForFunding, + ), CoinSelectLocker: l, CoinLeaser: l, Signer: l.Cfg.Signer, @@ -2525,12 +2536,16 @@ func (l *LightningWallet) CancelRebroadcast(txid chainhash.Hash) { // CoinSource is a wrapper around the wallet that implements the // chanfunding.CoinSource interface. type CoinSource struct { - wallet *LightningWallet + wallet *LightningWallet + allowUtxo func(Utxo) bool } // NewCoinSource creates a new instance of the CoinSource wrapper struct. -func NewCoinSource(w *LightningWallet) *CoinSource { - return &CoinSource{wallet: w} +func NewCoinSource(w *LightningWallet, allowUtxo func(Utxo) bool) *CoinSource { + return &CoinSource{ + wallet: w, + allowUtxo: allowUtxo, + } } // ListCoins returns all UTXOs from the source that have between @@ -2546,7 +2561,18 @@ func (c *CoinSource) ListCoins(minConfs int32, } var coins []wallet.Coin + for _, utxo := range utxos { + // If there is a filter function supplied all utxos not adhering + // to these conditions will be discared. + if c.allowUtxo != nil && !c.allowUtxo(*utxo) { + walletLog.Infof("Cannot use unconfirmed "+ + "utxo=%v because it is unstable and could be "+ + "replaced", utxo.OutPoint) + + continue + } + coins = append(coins, wallet.Coin{ TxOut: wire.TxOut{ Value: int64(utxo.Value), diff --git a/server.go b/server.go index e2c0b831a..2979880a2 100644 --- a/server.go +++ b/server.go @@ -1489,8 +1489,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, EnableUpfrontShutdown: cfg.EnableUpfrontShutdown, MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte( s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(), - DeleteAliasEdge: deleteAliasEdge, - AliasManager: s.aliasMgr, + DeleteAliasEdge: deleteAliasEdge, + AliasManager: s.aliasMgr, + IsSweeperOutpoint: s.sweeper.IsSweeperOutpoint, }) if err != nil { return nil, err diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 2754485ea..e8e7d77a4 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -1735,3 +1735,26 @@ func (s *UtxoSweeper) handleBumpEvent(r *BumpResult) error { return nil } + +// IsSweeperOutpoint determines whether the outpoint was created by the sweeper. +// +// NOTE: It is enough to check the txid because the sweeper will create +// outpoints which solely belong to the internal LND wallet. +func (s *UtxoSweeper) IsSweeperOutpoint(op wire.OutPoint) bool { + found, err := s.cfg.Store.IsOurTx(op.Hash) + // In case there is an error fetching the transaction details from the + // sweeper store we assume the outpoint is still used by the sweeper + // (worst case scenario). + // + // TODO(ziggie): Ensure that confirmed outpoints are deleted from the + // bucket. + if err != nil && !errors.Is(err, errNoTxHashesBucket) { + log.Errorf("failed to fetch info for outpoint(%v:%d) "+ + "with: %v, we assume it is still in use by the sweeper", + op.Hash, op.Index, err) + + return true + } + + return found +}