From c7bea07d58c5b458941f4f3abba6f7efdd8c4459 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 25 Mar 2025 10:28:05 +0800 Subject: [PATCH] sweep: start the sweeping if there are normal inputs We now start the sweeping process if there are normal inputs to partially cover the budget. --- sweep/tx_input_set.go | 58 ++++++++++++++++++++++++++---- sweep/tx_input_set_test.go | 72 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go index db4b893ed..ce69a9158 100644 --- a/sweep/tx_input_set.go +++ b/sweep/tx_input_set.go @@ -327,6 +327,21 @@ func (b *BudgetInputSet) copyInputs() []*SweeperInput { return inputs } +// hasNormalInput return a bool to indicate whether there exists an input that +// doesn't require a TxOut. When an input has no required outputs, it's either a +// wallet input, or an input we want to sweep. +func (b *BudgetInputSet) hasNormalInput() bool { + for _, inp := range b.inputs { + if inp.RequiredTxOut() != nil { + continue + } + + return true + } + + return false +} + // AddWalletInputs adds wallet inputs to the set until the specified budget is // met. When sweeping inputs with required outputs, although there's budget // specified, it cannot be directly spent from these required outputs. Instead, @@ -350,11 +365,6 @@ func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error { return fmt.Errorf("list unspent witness: %w", err) } - // Exit early if there are no wallet UTXOs. - if len(utxos) == 0 { - return fmt.Errorf("%w: empty wallet", ErrNotEnoughInputs) - } - // Sort the UTXOs by putting smaller values at the start of the slice // to avoid locking large UTXO for sweeping. // @@ -377,8 +387,20 @@ func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error { } } - log.Warn("Not enough wallet UTXOs to cover the budget, sweeping " + - "anyway...") + // Exit if there are no inputs can contribute to the fees. + if !b.hasNormalInput() { + return ErrNotEnoughInputs + } + + // If there's at least one input that can contribute to fees, we allow + // the sweep to continue, even though the full budget can't be met. + // Maybe later more wallet inputs will become available and we can add + // them if needed. + budget := b.Budget() + total, spendable := b.inputAmts() + log.Warnf("Not enough wallet UTXOs: need budget=%v, has spendable=%v, "+ + "total=%v, missing at least %v, sweeping anyway...", budget, + spendable, total, budget-spendable) return nil } @@ -416,6 +438,28 @@ func (b *BudgetInputSet) Inputs() []input.Input { return inputs } +// inputAmts returns two values for the set - the total input amount, and the +// spendable amount. Only the spendable amount can be used to pay the fees. +func (b *BudgetInputSet) inputAmts() (btcutil.Amount, btcutil.Amount) { + var ( + totalAmt btcutil.Amount + spendableAmt btcutil.Amount + ) + + for _, inp := range b.inputs { + output := btcutil.Amount(inp.SignDesc().Output.Value) + totalAmt += output + + if inp.RequiredTxOut() != nil { + continue + } + + spendableAmt += output + } + + return totalAmt, spendableAmt +} + // StartingFeeRate returns the max starting fee rate found in the inputs. // // NOTE: part of the InputSet interface. diff --git a/sweep/tx_input_set_test.go b/sweep/tx_input_set_test.go index 6e91a3fb1..159824878 100644 --- a/sweep/tx_input_set_test.go +++ b/sweep/tx_input_set_test.go @@ -456,6 +456,13 @@ func TestAddWalletInputsNotEnoughInputs(t *testing.T) { mockInput.On("RequiredTxOut").Return(&wire.TxOut{}) defer mockInput.AssertExpectations(t) + sd := &input.SignDescriptor{ + Output: &wire.TxOut{ + Value: budget, + }, + } + mockInput.On("SignDesc").Return(sd).Once() + // Create a pending input that requires 10k satoshis. pi := &SweeperInput{ Input: mockInput, @@ -484,6 +491,71 @@ func TestAddWalletInputsNotEnoughInputs(t *testing.T) { require.Len(t, set.inputs, 2) } +// TestAddWalletInputsEmptyWalletSuccess checks that when the wallet is empty, +// if there is a normal input, no error is returned. +func TestAddWalletInputsEmptyWalletSuccess(t *testing.T) { + t.Parallel() + + wallet := &MockWallet{} + defer wallet.AssertExpectations(t) + + // Specify the min and max confs used in + // ListUnspentWitnessFromDefaultAccount. + minConf, maxConf := int32(1), int32(math.MaxInt32) + + // Assume the desired budget is 10k satoshis. + const budget = 10_000 + + // Create a mock input that has required outputs. + mockInput1 := &input.MockInput{} + defer mockInput1.AssertExpectations(t) + + mockInput1.On("RequiredTxOut").Return(&wire.TxOut{}) + + sd := &input.SignDescriptor{ + Output: &wire.TxOut{ + Value: budget, + }, + } + mockInput1.On("SignDesc").Return(sd).Once() + + // Create a pending input that requires 10k satoshis. + pi1 := &SweeperInput{ + Input: mockInput1, + params: Params{Budget: budget}, + } + + // Create a mock input that doesn't require outputs. + mockInput2 := &input.MockInput{} + defer mockInput2.AssertExpectations(t) + + mockInput2.On("RequiredTxOut").Return(nil) + sd2 := &input.SignDescriptor{ + Output: &wire.TxOut{ + Value: budget, + }, + } + mockInput2.On("SignDesc").Return(sd2).Once() + + // Create a pending input that requires 10k satoshis. + pi2 := &SweeperInput{ + Input: mockInput2, + params: Params{Budget: budget}, + } + + // Mock the wallet to return empty utxos. + wallet.On("ListUnspentWitnessFromDefaultAccount", + minConf, maxConf).Return([]*lnwallet.Utxo{}, nil).Once() + + // Initialize an input set with the pending inputs. + set := BudgetInputSet{inputs: []*SweeperInput{pi1, pi2}} + + // Add wallet inputs to the input set, which should return no error + // although the wallet is empty. + err := set.AddWalletInputs(wallet) + require.NoError(t, err) +} + // TestAddWalletInputsSuccess checks that when there are enough wallet utxos, // they are added to the input set. func TestAddWalletInputsSuccess(t *testing.T) {