sweep: allow specifying starting fee rate for fee func

This commit is contained in:
yyforyongyu
2024-04-11 17:08:36 +08:00
parent db3aad31aa
commit b6a2984167
8 changed files with 140 additions and 20 deletions

View File

@@ -570,6 +570,12 @@ func (b *BudgetAggregator) ClusterInputs(inputs InputsMap,
// createInputSet takes a set of inputs which share the same deadline height // createInputSet takes a set of inputs which share the same deadline height
// and turns them into a list of `InputSet`, each set is then used to create a // and turns them into a list of `InputSet`, each set is then used to create a
// sweep transaction. // sweep transaction.
//
// TODO(yy): by the time we call this method, all the invalid/uneconomical
// inputs have been filtered out, all the inputs have been sorted based on
// their budgets, and we are about to create input sets. The only thing missing
// here is, we need to group the inputs here even further based on whether
// their budgets can cover the starting fee rate used for this input set.
func (b *BudgetAggregator) createInputSets(inputs []SweeperInput, func (b *BudgetAggregator) createInputSets(inputs []SweeperInput,
deadlineHeight int32) []InputSet { deadlineHeight int32) []InputSet {
@@ -621,8 +627,10 @@ func (b *BudgetAggregator) createInputSets(inputs []SweeperInput,
return sets return sets
} }
// filterInputs filters out inputs that have a budget below the min relay fee // filterInputs filters out inputs that have,
// or have a required output that's below the dust. // - a budget below the min relay fee.
// - a budget below its requested starting fee.
// - a required output that's below the dust.
func (b *BudgetAggregator) filterInputs(inputs InputsMap) InputsMap { func (b *BudgetAggregator) filterInputs(inputs InputsMap) InputsMap {
// Get the current min relay fee for this round. // Get the current min relay fee for this round.
minFeeRate := b.estimator.RelayFeePerKW() minFeeRate := b.estimator.RelayFeePerKW()
@@ -655,6 +663,19 @@ func (b *BudgetAggregator) filterInputs(inputs InputsMap) InputsMap {
continue continue
} }
// Skip inputs that has cannot cover its starting fees.
startingFeeRate := pi.params.StartingFeeRate.UnwrapOr(
chainfee.SatPerKWeight(0),
)
startingFee := startingFeeRate.FeeForWeight(int64(size))
if pi.params.Budget < startingFee {
log.Errorf("Skipped input=%v: has budget=%v, but the "+
"starting fee requires %v", op,
pi.params.Budget, minFee)
continue
}
// If the input comes with a required tx out that is below // If the input comes with a required tx out that is below
// dust, we won't add it. // dust, we won't add it.
// //

View File

@@ -114,6 +114,10 @@ type BumpRequest struct {
// MaxFeeRate is the maximum fee rate that can be used for fee bumping. // MaxFeeRate is the maximum fee rate that can be used for fee bumping.
MaxFeeRate chainfee.SatPerKWeight MaxFeeRate chainfee.SatPerKWeight
// StartingFeeRate is an optional parameter that can be used to specify
// the initial fee rate to use for the fee function.
StartingFeeRate fn.Option[chainfee.SatPerKWeight]
} }
// MaxFeeRateAllowed returns the maximum fee rate allowed for the given // MaxFeeRateAllowed returns the maximum fee rate allowed for the given
@@ -380,6 +384,7 @@ func (t *TxPublisher) initializeFeeFunction(
// TODO(yy): return based on differet req.Strategy? // TODO(yy): return based on differet req.Strategy?
return NewLinearFeeFunction( return NewLinearFeeFunction(
maxFeeRateAllowed, confTarget, t.cfg.Estimator, maxFeeRateAllowed, confTarget, t.cfg.Estimator,
req.StartingFeeRate,
) )
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
) )
@@ -110,8 +111,10 @@ var _ FeeFunction = (*LinearFeeFunction)(nil)
// NewLinearFeeFunction creates a new linear fee function and initializes it // NewLinearFeeFunction creates a new linear fee function and initializes it
// with a starting fee rate which is an estimated value returned from the fee // with a starting fee rate which is an estimated value returned from the fee
// estimator using the initial conf target. // estimator using the initial conf target.
func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight, confTarget uint32, func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight,
estimator chainfee.Estimator) (*LinearFeeFunction, error) { confTarget uint32, estimator chainfee.Estimator,
startingFeeRate fn.Option[chainfee.SatPerKWeight]) (
*LinearFeeFunction, error) {
// If the deadline has already been reached, there's nothing the fee // If the deadline has already been reached, there's nothing the fee
// function can do. In this case, we'll use the max fee rate // function can do. In this case, we'll use the max fee rate
@@ -130,11 +133,17 @@ func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight, confTarget uint32,
estimator: estimator, estimator: estimator,
} }
// Estimate the initial fee rate. // If the caller specifies the starting fee rate, we'll use it instead
// // of estimating it based on the deadline.
// NOTE: estimateFeeRate guarantees the returned fee rate is capped by start, err := startingFeeRate.UnwrapOrFuncErr(
// the ending fee rate, so we don't need to worry about overpay. func() (chainfee.SatPerKWeight, error) {
start, err := l.estimateFeeRate(confTarget) // Estimate the initial fee rate.
//
// NOTE: estimateFeeRate guarantees the returned fee
// rate is capped by the ending fee rate, so we don't
// need to worry about overpay.
return l.estimateFeeRate(confTarget)
})
if err != nil { if err != nil {
return nil, fmt.Errorf("estimate initial fee rate: %w", err) return nil, fmt.Errorf("estimate initial fee rate: %w", err)
} }

View File

@@ -3,6 +3,7 @@ package sweep
import ( import (
"testing" "testing"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -21,10 +22,12 @@ func TestLinearFeeFunctionNew(t *testing.T) {
estimatedFeeRate := chainfee.SatPerKWeight(500) estimatedFeeRate := chainfee.SatPerKWeight(500)
minRelayFeeRate := chainfee.SatPerKWeight(100) minRelayFeeRate := chainfee.SatPerKWeight(100)
confTarget := uint32(6) confTarget := uint32(6)
noStartFeeRate := fn.None[chainfee.SatPerKWeight]()
startFeeRate := chainfee.SatPerKWeight(1000)
// Assert init fee function with zero conf value will end up using the // Assert init fee function with zero conf value will end up using the
// max fee rate. // max fee rate.
f, err := NewLinearFeeFunction(maxFeeRate, 0, estimator) f, err := NewLinearFeeFunction(maxFeeRate, 0, estimator, noStartFeeRate)
rt.NoError(err) rt.NoError(err)
rt.NotNil(f) rt.NotNil(f)
@@ -39,7 +42,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
estimator.On("EstimateFeePerKW", confTarget).Return( estimator.On("EstimateFeePerKW", confTarget).Return(
chainfee.SatPerKWeight(0), errDummy).Once() chainfee.SatPerKWeight(0), errDummy).Once()
f, err = NewLinearFeeFunction(maxFeeRate, confTarget, estimator) f, err = NewLinearFeeFunction(
maxFeeRate, confTarget, estimator, noStartFeeRate,
)
rt.ErrorIs(err, errDummy) rt.ErrorIs(err, errDummy)
rt.Nil(f) rt.Nil(f)
@@ -53,7 +58,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
maxFeeRate+1, nil).Once() maxFeeRate+1, nil).Once()
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once() estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
f, err = NewLinearFeeFunction(maxFeeRate, smallConf, estimator) f, err = NewLinearFeeFunction(
maxFeeRate, smallConf, estimator, noStartFeeRate,
)
rt.NoError(err) rt.NoError(err)
rt.NotNil(f) rt.NotNil(f)
@@ -65,7 +72,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
maxFeeRate, nil).Once() maxFeeRate, nil).Once()
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once() estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
f, err = NewLinearFeeFunction(maxFeeRate, confTarget, estimator) f, err = NewLinearFeeFunction(
maxFeeRate, confTarget, estimator, noStartFeeRate,
)
rt.ErrorContains(err, "fee rate delta is zero") rt.ErrorContains(err, "fee rate delta is zero")
rt.Nil(f) rt.Nil(f)
@@ -75,7 +84,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
estimator.On("RelayFeePerKW").Return(minRelayFeeRate).Once() estimator.On("RelayFeePerKW").Return(minRelayFeeRate).Once()
largeConf := uint32(1008) largeConf := uint32(1008)
f, err = NewLinearFeeFunction(maxFeeRate, largeConf, estimator) f, err = NewLinearFeeFunction(
maxFeeRate, largeConf, estimator, noStartFeeRate,
)
rt.NoError(err) rt.NoError(err)
rt.NotNil(f) rt.NotNil(f)
@@ -93,7 +104,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
estimatedFeeRate, nil).Once() estimatedFeeRate, nil).Once()
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once() estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
f, err = NewLinearFeeFunction(maxFeeRate, confTarget, estimator) f, err = NewLinearFeeFunction(
maxFeeRate, confTarget, estimator, noStartFeeRate,
)
rt.NoError(err) rt.NoError(err)
rt.NotNil(f) rt.NotNil(f)
@@ -103,6 +116,22 @@ func TestLinearFeeFunctionNew(t *testing.T) {
rt.Equal(estimatedFeeRate, f.currentFeeRate) rt.Equal(estimatedFeeRate, f.currentFeeRate)
rt.NotZero(f.deltaFeeRate) rt.NotZero(f.deltaFeeRate)
rt.Equal(confTarget, f.width) rt.Equal(confTarget, f.width)
// Check a successfully created fee function using the specified
// starting fee rate.
//
// NOTE: by NOT mocking the fee estimator, we assert the
// estimateFeeRate is NOT called.
f, err = NewLinearFeeFunction(
maxFeeRate, confTarget, estimator, fn.Some(startFeeRate),
)
rt.NoError(err)
rt.NotNil(f)
// Assert the customized starting fee rate is used.
rt.Equal(startFeeRate, f.startingFeeRate)
rt.Equal(startFeeRate, f.currentFeeRate)
} }
// TestLinearFeeFunctionFeeRateAtPosition checks the expected feerate is // TestLinearFeeFunctionFeeRateAtPosition checks the expected feerate is
@@ -184,7 +213,10 @@ func TestLinearFeeFunctionIncrement(t *testing.T) {
estimatedFeeRate, nil).Once() estimatedFeeRate, nil).Once()
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once() estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
f, err := NewLinearFeeFunction(maxFeeRate, confTarget, estimator) f, err := NewLinearFeeFunction(
maxFeeRate, confTarget, estimator,
fn.None[chainfee.SatPerKWeight](),
)
rt.NoError(err) rt.NoError(err)
// We now increase the position from 1 to 9. // We now increase the position from 1 to 9.
@@ -232,7 +264,10 @@ func TestLinearFeeFunctionIncreaseFeeRate(t *testing.T) {
estimatedFeeRate, nil).Once() estimatedFeeRate, nil).Once()
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once() estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
f, err := NewLinearFeeFunction(maxFeeRate, confTarget, estimator) f, err := NewLinearFeeFunction(
maxFeeRate, confTarget, estimator,
fn.None[chainfee.SatPerKWeight](),
)
rt.NoError(err) rt.NoError(err)
// If we are increasing the fee rate using the initial conf target, we // If we are increasing the fee rate using the initial conf target, we

View File

@@ -8,6 +8,7 @@ import (
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -510,6 +511,13 @@ func (m *MockInputSet) Budget() btcutil.Amount {
return args.Get(0).(btcutil.Amount) return args.Get(0).(btcutil.Amount)
} }
// StartingFeeRate returns the max starting fee rate found in the inputs.
func (m *MockInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] {
args := m.Called()
return args.Get(0).(fn.Option[chainfee.SatPerKWeight])
}
// MockBumper is a mock implementation of the interface Bumper. // MockBumper is a mock implementation of the interface Bumper.
type MockBumper struct { type MockBumper struct {
mock.Mock mock.Mock

View File

@@ -65,6 +65,10 @@ type Params struct {
// without waiting for blocks to come to trigger the sweeping of // without waiting for blocks to come to trigger the sweeping of
// inputs. // inputs.
Immediate bool Immediate bool
// StartingFeeRate is an optional parameter that can be used to specify
// the initial fee rate to use for the fee function.
StartingFeeRate fn.Option[chainfee.SatPerKWeight]
} }
// ParamsUpdate contains a new set of parameters to update a pending sweep with. // ParamsUpdate contains a new set of parameters to update a pending sweep with.
@@ -77,6 +81,10 @@ type ParamsUpdate struct {
// Immediate indicates that the input should be swept immediately // Immediate indicates that the input should be swept immediately
// without waiting for blocks to come. // without waiting for blocks to come.
Immediate bool Immediate bool
// StartingFeeRate is an optional parameter that can be used to specify
// the initial fee rate to use for the fee function.
StartingFeeRate fn.Option[chainfee.SatPerKWeight]
} }
// String returns a human readable interpretation of the sweep parameters. // String returns a human readable interpretation of the sweep parameters.
@@ -91,9 +99,9 @@ func (p Params) String() string {
exclusiveGroup = fmt.Sprintf("%d", *p.ExclusiveGroup) exclusiveGroup = fmt.Sprintf("%d", *p.ExclusiveGroup)
} }
return fmt.Sprintf("fee=%v, immediate=%v, exclusive_group=%v, budget=%v, "+ return fmt.Sprintf("startingFeeRate=%v, immediate=%v, "+
"deadline=%v", p.Fee, p.Immediate, exclusiveGroup, p.Budget, "exclusive_group=%v, budget=%v, deadline=%v", p.StartingFeeRate,
deadline) p.Immediate, exclusiveGroup, p.Budget, deadline)
} }
// SweepState represents the current state of a pending input. // SweepState represents the current state of a pending input.
@@ -830,6 +838,7 @@ func (s *UtxoSweeper) sweep(set InputSet) error {
DeadlineHeight: set.DeadlineHeight(), DeadlineHeight: set.DeadlineHeight(),
DeliveryAddress: s.currentOutputScript, DeliveryAddress: s.currentOutputScript,
MaxFeeRate: s.cfg.MaxFeeRate.FeePerKWeight(), MaxFeeRate: s.cfg.MaxFeeRate.FeePerKWeight(),
StartingFeeRate: set.StartingFeeRate(),
// TODO(yy): pass the strategy here. // TODO(yy): pass the strategy here.
} }

View File

@@ -2734,9 +2734,13 @@ func TestSweepPendingInputs(t *testing.T) {
setNeedWallet.On("Inputs").Return(nil).Times(4) setNeedWallet.On("Inputs").Return(nil).Times(4)
setNeedWallet.On("DeadlineHeight").Return(testHeight).Once() setNeedWallet.On("DeadlineHeight").Return(testHeight).Once()
setNeedWallet.On("Budget").Return(btcutil.Amount(1)).Once() setNeedWallet.On("Budget").Return(btcutil.Amount(1)).Once()
setNeedWallet.On("StartingFeeRate").Return(
fn.None[chainfee.SatPerKWeight]()).Once()
normalSet.On("Inputs").Return(nil).Times(4) normalSet.On("Inputs").Return(nil).Times(4)
normalSet.On("DeadlineHeight").Return(testHeight).Once() normalSet.On("DeadlineHeight").Return(testHeight).Once()
normalSet.On("Budget").Return(btcutil.Amount(1)).Once() normalSet.On("Budget").Return(btcutil.Amount(1)).Once()
normalSet.On("StartingFeeRate").Return(
fn.None[chainfee.SatPerKWeight]()).Once()
// Make pending inputs for testing. We don't need real values here as // Make pending inputs for testing. We don't need real values here as
// the returned clusters are mocked. // the returned clusters are mocked.

View File

@@ -77,6 +77,10 @@ type InputSet interface {
// Budget givens the total amount that can be used as fees by this // Budget givens the total amount that can be used as fees by this
// input set. // input set.
Budget() btcutil.Amount Budget() btcutil.Amount
// StartingFeeRate returns the max starting fee rate found in the
// inputs.
StartingFeeRate() fn.Option[chainfee.SatPerKWeight]
} }
type txInputSetState struct { type txInputSetState struct {
@@ -205,6 +209,13 @@ func (t *txInputSet) DeadlineHeight() int32 {
return 0 return 0
} }
// StartingFeeRate returns the max starting fee rate found in the inputs.
//
// NOTE: this field is only used for `BudgetInputSet`.
func (t *txInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] {
return fn.None[chainfee.SatPerKWeight]()
}
// NeedWalletInput returns true if the input set needs more wallet inputs. // NeedWalletInput returns true if the input set needs more wallet inputs.
func (t *txInputSet) NeedWalletInput() bool { func (t *txInputSet) NeedWalletInput() bool {
return !t.enoughInput() return !t.enoughInput()
@@ -800,3 +811,21 @@ func (b *BudgetInputSet) Inputs() []input.Input {
return inputs return inputs
} }
// StartingFeeRate returns the max starting fee rate found in the inputs.
//
// NOTE: part of the InputSet interface.
func (b *BudgetInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] {
maxFeeRate := chainfee.SatPerKWeight(0)
startingFeeRate := fn.None[chainfee.SatPerKWeight]()
for _, inp := range b.inputs {
feerate := inp.params.StartingFeeRate.UnwrapOr(0)
if feerate > maxFeeRate {
maxFeeRate = feerate
startingFeeRate = fn.Some(maxFeeRate)
}
}
return startingFeeRate
}