mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-22 22:25:24 +01:00
sweep: allow specifying starting fee rate for fee func
This commit is contained in:
parent
db3aad31aa
commit
b6a2984167
8 changed files with 140 additions and 20 deletions
|
@ -570,6 +570,12 @@ func (b *BudgetAggregator) ClusterInputs(inputs InputsMap,
|
|||
// 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
|
||||
// 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,
|
||||
deadlineHeight int32) []InputSet {
|
||||
|
||||
|
@ -621,8 +627,10 @@ func (b *BudgetAggregator) createInputSets(inputs []SweeperInput,
|
|||
return sets
|
||||
}
|
||||
|
||||
// filterInputs filters out inputs that have a budget below the min relay fee
|
||||
// or have a required output that's below the dust.
|
||||
// filterInputs filters out inputs that have,
|
||||
// - 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 {
|
||||
// Get the current min relay fee for this round.
|
||||
minFeeRate := b.estimator.RelayFeePerKW()
|
||||
|
@ -655,6 +663,19 @@ func (b *BudgetAggregator) filterInputs(inputs InputsMap) InputsMap {
|
|||
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
|
||||
// dust, we won't add it.
|
||||
//
|
||||
|
|
|
@ -114,6 +114,10 @@ type BumpRequest struct {
|
|||
|
||||
// MaxFeeRate is the maximum fee rate that can be used for fee bumping.
|
||||
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
|
||||
|
@ -380,6 +384,7 @@ func (t *TxPublisher) initializeFeeFunction(
|
|||
// TODO(yy): return based on differet req.Strategy?
|
||||
return NewLinearFeeFunction(
|
||||
maxFeeRateAllowed, confTarget, t.cfg.Estimator,
|
||||
req.StartingFeeRate,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
@ -110,8 +111,10 @@ var _ FeeFunction = (*LinearFeeFunction)(nil)
|
|||
// NewLinearFeeFunction creates a new linear fee function and initializes it
|
||||
// with a starting fee rate which is an estimated value returned from the fee
|
||||
// estimator using the initial conf target.
|
||||
func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight, confTarget uint32,
|
||||
estimator chainfee.Estimator) (*LinearFeeFunction, error) {
|
||||
func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight,
|
||||
confTarget uint32, estimator chainfee.Estimator,
|
||||
startingFeeRate fn.Option[chainfee.SatPerKWeight]) (
|
||||
*LinearFeeFunction, error) {
|
||||
|
||||
// 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
|
||||
|
@ -130,11 +133,17 @@ func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight, confTarget uint32,
|
|||
estimator: estimator,
|
||||
}
|
||||
|
||||
// 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.
|
||||
start, err := l.estimateFeeRate(confTarget)
|
||||
// If the caller specifies the starting fee rate, we'll use it instead
|
||||
// of estimating it based on the deadline.
|
||||
start, err := startingFeeRate.UnwrapOrFuncErr(
|
||||
func() (chainfee.SatPerKWeight, error) {
|
||||
// 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 {
|
||||
return nil, fmt.Errorf("estimate initial fee rate: %w", err)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package sweep
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -21,10 +22,12 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
estimatedFeeRate := chainfee.SatPerKWeight(500)
|
||||
minRelayFeeRate := chainfee.SatPerKWeight(100)
|
||||
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
|
||||
// max fee rate.
|
||||
f, err := NewLinearFeeFunction(maxFeeRate, 0, estimator)
|
||||
f, err := NewLinearFeeFunction(maxFeeRate, 0, estimator, noStartFeeRate)
|
||||
rt.NoError(err)
|
||||
rt.NotNil(f)
|
||||
|
||||
|
@ -39,7 +42,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
chainfee.SatPerKWeight(0), errDummy).Once()
|
||||
|
||||
f, err = NewLinearFeeFunction(maxFeeRate, confTarget, estimator)
|
||||
f, err = NewLinearFeeFunction(
|
||||
maxFeeRate, confTarget, estimator, noStartFeeRate,
|
||||
)
|
||||
rt.ErrorIs(err, errDummy)
|
||||
rt.Nil(f)
|
||||
|
||||
|
@ -53,7 +58,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
maxFeeRate+1, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
|
||||
f, err = NewLinearFeeFunction(maxFeeRate, smallConf, estimator)
|
||||
f, err = NewLinearFeeFunction(
|
||||
maxFeeRate, smallConf, estimator, noStartFeeRate,
|
||||
)
|
||||
rt.NoError(err)
|
||||
rt.NotNil(f)
|
||||
|
||||
|
@ -65,7 +72,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
maxFeeRate, nil).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.Nil(f)
|
||||
|
||||
|
@ -75,7 +84,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
estimator.On("RelayFeePerKW").Return(minRelayFeeRate).Once()
|
||||
|
||||
largeConf := uint32(1008)
|
||||
f, err = NewLinearFeeFunction(maxFeeRate, largeConf, estimator)
|
||||
f, err = NewLinearFeeFunction(
|
||||
maxFeeRate, largeConf, estimator, noStartFeeRate,
|
||||
)
|
||||
rt.NoError(err)
|
||||
rt.NotNil(f)
|
||||
|
||||
|
@ -93,7 +104,9 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
estimatedFeeRate, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
|
||||
f, err = NewLinearFeeFunction(maxFeeRate, confTarget, estimator)
|
||||
f, err = NewLinearFeeFunction(
|
||||
maxFeeRate, confTarget, estimator, noStartFeeRate,
|
||||
)
|
||||
rt.NoError(err)
|
||||
rt.NotNil(f)
|
||||
|
||||
|
@ -103,6 +116,22 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
rt.Equal(estimatedFeeRate, f.currentFeeRate)
|
||||
rt.NotZero(f.deltaFeeRate)
|
||||
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
|
||||
|
@ -184,7 +213,10 @@ func TestLinearFeeFunctionIncrement(t *testing.T) {
|
|||
estimatedFeeRate, nil).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)
|
||||
|
||||
// We now increase the position from 1 to 9.
|
||||
|
@ -232,7 +264,10 @@ func TestLinearFeeFunctionIncreaseFeeRate(t *testing.T) {
|
|||
estimatedFeeRate, nil).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)
|
||||
|
||||
// If we are increasing the fee rate using the initial conf target, we
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
|
@ -510,6 +511,13 @@ func (m *MockInputSet) Budget() 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.
|
||||
type MockBumper struct {
|
||||
mock.Mock
|
||||
|
|
|
@ -65,6 +65,10 @@ type Params struct {
|
|||
// without waiting for blocks to come to trigger the sweeping of
|
||||
// inputs.
|
||||
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.
|
||||
|
@ -77,6 +81,10 @@ type ParamsUpdate struct {
|
|||
// Immediate indicates that the input should be swept immediately
|
||||
// without waiting for blocks to come.
|
||||
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.
|
||||
|
@ -91,9 +99,9 @@ func (p Params) String() string {
|
|||
exclusiveGroup = fmt.Sprintf("%d", *p.ExclusiveGroup)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("fee=%v, immediate=%v, exclusive_group=%v, budget=%v, "+
|
||||
"deadline=%v", p.Fee, p.Immediate, exclusiveGroup, p.Budget,
|
||||
deadline)
|
||||
return fmt.Sprintf("startingFeeRate=%v, immediate=%v, "+
|
||||
"exclusive_group=%v, budget=%v, deadline=%v", p.StartingFeeRate,
|
||||
p.Immediate, exclusiveGroup, p.Budget, deadline)
|
||||
}
|
||||
|
||||
// SweepState represents the current state of a pending input.
|
||||
|
@ -830,6 +838,7 @@ func (s *UtxoSweeper) sweep(set InputSet) error {
|
|||
DeadlineHeight: set.DeadlineHeight(),
|
||||
DeliveryAddress: s.currentOutputScript,
|
||||
MaxFeeRate: s.cfg.MaxFeeRate.FeePerKWeight(),
|
||||
StartingFeeRate: set.StartingFeeRate(),
|
||||
// TODO(yy): pass the strategy here.
|
||||
}
|
||||
|
||||
|
|
|
@ -2734,9 +2734,13 @@ func TestSweepPendingInputs(t *testing.T) {
|
|||
setNeedWallet.On("Inputs").Return(nil).Times(4)
|
||||
setNeedWallet.On("DeadlineHeight").Return(testHeight).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("DeadlineHeight").Return(testHeight).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
|
||||
// the returned clusters are mocked.
|
||||
|
|
|
@ -77,6 +77,10 @@ type InputSet interface {
|
|||
// Budget givens the total amount that can be used as fees by this
|
||||
// input set.
|
||||
Budget() btcutil.Amount
|
||||
|
||||
// StartingFeeRate returns the max starting fee rate found in the
|
||||
// inputs.
|
||||
StartingFeeRate() fn.Option[chainfee.SatPerKWeight]
|
||||
}
|
||||
|
||||
type txInputSetState struct {
|
||||
|
@ -205,6 +209,13 @@ func (t *txInputSet) DeadlineHeight() int32 {
|
|||
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.
|
||||
func (t *txInputSet) NeedWalletInput() bool {
|
||||
return !t.enoughInput()
|
||||
|
@ -800,3 +811,21 @@ func (b *BudgetInputSet) Inputs() []input.Input {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue