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
No known key found for this signature in database
GPG key ID: 9BCD95C4FF296868
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
// 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.
//

View file

@ -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,
)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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.
}

View file

@ -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.

View file

@ -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
}