From 861dc145bf0767481e74a1d12b83503635a0d43e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 19 Mar 2025 17:32:42 +0800 Subject: [PATCH] sweep: create sweep tx even the budget cannot be met We now always create the sweeping tx even though the budget cannot be covered so we don't miss the deadline. Note that the fee bump will fail once the provided wallet input cannot cover the increase fees, which is fine as these inputs will be marked as failed and be retried again in the next block. When that happens, if there are new wallet UTXOs, a new batch will be created to perform the fee bump. --- sweep/fee_bumper.go | 16 +++++++++++++--- sweep/tx_input_set.go | 18 +++++++----------- sweep/tx_input_set_test.go | 26 ++++++++++++-------------- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index b081fe479..99360d6fb 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -1114,6 +1114,14 @@ func (t *TxPublisher) handleInitialTxError(r *monitorRecord, err error) { case errors.Is(err, ErrZeroFeeRateDelta): result.Event = TxFailed + // When the error is due to not enough inputs to cover the budget, we'll + // send a TxFailed event so these inputs can be retried when the wallet + // has more UTXOs. + case errors.Is(err, ErrNotEnoughInputs), + errors.Is(err, ErrNotEnoughBudget): + + result.Event = TxFailed + // When there are missing inputs, we'll create a TxUnknownSpend bump // result here so the rest of the inputs can be retried. case errors.Is(err, ErrInputMissing): @@ -1757,9 +1765,11 @@ func prepareSweepTx(inputs []input.Input, changePkScript lnwallet.AddrWithKey, // Make sure total output amount is less than total input amount. if requiredOutput+txFee > totalInput { - return 0, noChange, noLocktime, fmt.Errorf("insufficient "+ - "input to create sweep tx: input_sum=%v, "+ - "output_sum=%v", totalInput, requiredOutput+txFee) + log.Errorf("Insufficient input to create sweep tx: "+ + "input_sum=%v, output_sum=%v", totalInput, + requiredOutput+txFee) + + return 0, noChange, noLocktime, ErrNotEnoughInputs } // The value remaining after the required output and fees is the diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go index ff3b7f309..db4b893ed 100644 --- a/sweep/tx_input_set.go +++ b/sweep/tx_input_set.go @@ -296,7 +296,9 @@ func (b *BudgetInputSet) NeedWalletInput() bool { } // Get the amount left after covering the input's own budget. - // This amount can then be lent to the above input. + // This amount can then be lent to the above input. For a wallet + // input, its `Budget` is set to zero, which means the whole + // input can be borrowed to cover the budget. budget := inp.params.Budget output := btcutil.Amount(inp.SignDesc().Output.Value) budgetBorrowable += output - budget @@ -350,7 +352,7 @@ func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error { // Exit early if there are no wallet UTXOs. if len(utxos) == 0 { - return ErrNotEnoughInputs + return fmt.Errorf("%w: empty wallet", ErrNotEnoughInputs) } // Sort the UTXOs by putting smaller values at the start of the slice @@ -362,11 +364,6 @@ func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error { return utxos[i].Value < utxos[j].Value }) - // Make a copy of the current inputs. If the wallet doesn't have enough - // utxos to cover the budget, we will revert the current set to its - // original state by removing the added wallet inputs. - originalInputs := b.copyInputs() - // Add wallet inputs to the set until the specified budget is covered. for _, utxo := range utxos { err := b.addWalletInput(utxo) @@ -380,11 +377,10 @@ func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error { } } - // The wallet doesn't have enough utxos to cover the budget. Revert the - // input set to its original state. - b.inputs = originalInputs + log.Warn("Not enough wallet UTXOs to cover the budget, sweeping " + + "anyway...") - return ErrNotEnoughInputs + return nil } // Budget returns the total budget of the set. diff --git a/sweep/tx_input_set_test.go b/sweep/tx_input_set_test.go index 0ea056e49..6e91a3fb1 100644 --- a/sweep/tx_input_set_test.go +++ b/sweep/tx_input_set_test.go @@ -386,12 +386,12 @@ func TestNeedWalletInput(t *testing.T) { } } -// TestAddWalletInputReturnErr tests the three possible errors returned from +// TestAddWalletInputsReturnErr tests the three possible errors returned from // AddWalletInputs: // - error from ListUnspentWitnessFromDefaultAccount. // - error from createWalletTxInput. // - error when wallet doesn't have utxos. -func TestAddWalletInputReturnErr(t *testing.T) { +func TestAddWalletInputsReturnErr(t *testing.T) { t.Parallel() wallet := &MockWallet{} @@ -436,10 +436,9 @@ func TestAddWalletInputReturnErr(t *testing.T) { require.ErrorIs(t, err, ErrNotEnoughInputs) } -// TestAddWalletInputNotEnoughInputs checks that when there are not enough -// wallet utxos, an error is returned and the budget set is reset to its -// initial state. -func TestAddWalletInputNotEnoughInputs(t *testing.T) { +// TestAddWalletInputsNotEnoughInputs checks that when there are not enough +// wallet utxos, no error is returned as long as the wallet is not empty. +func TestAddWalletInputsNotEnoughInputs(t *testing.T) { t.Parallel() wallet := &MockWallet{} @@ -476,19 +475,18 @@ func TestAddWalletInputNotEnoughInputs(t *testing.T) { // Initialize an input set with the pending input. set := BudgetInputSet{inputs: []*SweeperInput{pi}} - // Add wallet inputs to the input set, which should give us an error as - // the wallet cannot cover the budget. + // Add wallet inputs to the input set, which should return no error + // although the wallet cannot cover the budget. err := set.AddWalletInputs(wallet) - require.ErrorIs(t, err, ErrNotEnoughInputs) + require.NoError(t, err) - // Check that the budget set is reverted to its initial state. - require.Len(t, set.inputs, 1) - require.Equal(t, pi, set.inputs[0]) + // Check that the budget set is updated. + require.Len(t, set.inputs, 2) } -// TestAddWalletInputSuccess checks that when there are enough wallet utxos, +// TestAddWalletInputsSuccess checks that when there are enough wallet utxos, // they are added to the input set. -func TestAddWalletInputSuccess(t *testing.T) { +func TestAddWalletInputsSuccess(t *testing.T) { t.Parallel() wallet := &MockWallet{}