mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-26 01:33:02 +01:00
sweep: add FeeFunction
interface and a linear implementation
This commit adds a new interface, `FeeFunction`, to deal with calculating fee rates. In addition, a simple linear function is implemented, hence `LinearFeeFunction`, which will be used to calculate fee rates when bumping fees. Check #4215 for other type of fee functions that can be implemented.
This commit is contained in:
parent
f7bc82a22d
commit
cd5d074099
265
sweep/fee_function.go
Normal file
265
sweep/fee_function.go
Normal file
@ -0,0 +1,265 @@
|
||||
package sweep
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMaxPosition is returned when trying to increase the position of
|
||||
// the fee function while it's already at its max.
|
||||
ErrMaxPosition = errors.New("position already at max")
|
||||
)
|
||||
|
||||
// FeeFunction defines an interface that is used to calculate fee rates for
|
||||
// transactions. It's expected the implementations use three params, the
|
||||
// starting fee rate, the ending fee rate, and number of blocks till deadline
|
||||
// block height, to build an algorithm to calculate the fee rate based on the
|
||||
// current block height.
|
||||
type FeeFunction interface {
|
||||
// FeeRate returns the current fee rate calculated by the fee function.
|
||||
FeeRate() chainfee.SatPerKWeight
|
||||
|
||||
// Increment increases the fee rate by one step. The definition of one
|
||||
// step is up to the implementation. After calling this method, it's
|
||||
// expected to change the state of the fee function such that calling
|
||||
// `FeeRate` again will return the increased value.
|
||||
//
|
||||
// It returns a boolean to indicate whether the fee rate is increased,
|
||||
// as fee bump should not be attempted if the increased fee rate is not
|
||||
// greater than the current fee rate, which may happen if the algorithm
|
||||
// gives the same fee rates at two positions.
|
||||
//
|
||||
// An error is returned when the max fee rate is reached.
|
||||
//
|
||||
// NOTE: we intentionally don't return the new fee rate here, so both
|
||||
// the implementation and the caller are aware of the state change.
|
||||
Increment() (bool, error)
|
||||
|
||||
// IncreaseFeeRate increases the fee rate to the new position
|
||||
// calculated using (width - confTarget). It returns a boolean to
|
||||
// indicate whether the fee rate is increased, and an error if the
|
||||
// position is greater than the width.
|
||||
//
|
||||
// NOTE: this method is provided to allow the caller to increase the
|
||||
// fee rate based on a conf target without taking care of the fee
|
||||
// function's current state (position).
|
||||
IncreaseFeeRate(confTarget uint32) (bool, error)
|
||||
}
|
||||
|
||||
// LinearFeeFunction implements the FeeFunction interface with a linear
|
||||
// function:
|
||||
//
|
||||
// feeRate = startingFeeRate + position * delta.
|
||||
// - width: deadlineBlockHeight - startingBlockHeight
|
||||
// - delta: (endingFeeRate - startingFeeRate) / width
|
||||
// - position: currentBlockHeight - startingBlockHeight
|
||||
//
|
||||
// The fee rate will be capped at endingFeeRate.
|
||||
//
|
||||
// TODO(yy): implement more functions specified here:
|
||||
// - https://github.com/lightningnetwork/lnd/issues/4215
|
||||
type LinearFeeFunction struct {
|
||||
// startingFeeRate specifies the initial fee rate to begin with.
|
||||
startingFeeRate chainfee.SatPerKWeight
|
||||
|
||||
// endingFeeRate specifies the max allowed fee rate.
|
||||
endingFeeRate chainfee.SatPerKWeight
|
||||
|
||||
// currentFeeRate specifies the current calculated fee rate.
|
||||
currentFeeRate chainfee.SatPerKWeight
|
||||
|
||||
// width is the number of blocks between the starting block height
|
||||
// and the deadline block height.
|
||||
width uint32
|
||||
|
||||
// position is the number of blocks between the starting block height
|
||||
// and the current block height.
|
||||
position uint32
|
||||
|
||||
// deltaFeeRate is the fee rate increase per block.
|
||||
deltaFeeRate chainfee.SatPerKWeight
|
||||
|
||||
// estimator is the fee estimator used to estimate the fee rate. We use
|
||||
// it to get the initial fee rate and, use it as a benchmark to decide
|
||||
// whether we want to used the estimated fee rate or the calculated fee
|
||||
// rate based on different strategies.
|
||||
estimator chainfee.Estimator
|
||||
}
|
||||
|
||||
// Compile-time check to ensure LinearFeeFunction satisfies the FeeFunction.
|
||||
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) {
|
||||
|
||||
// Sanity check conf target.
|
||||
if confTarget == 0 {
|
||||
return nil, fmt.Errorf("width must be greater than zero")
|
||||
}
|
||||
|
||||
l := &LinearFeeFunction{
|
||||
endingFeeRate: maxFeeRate,
|
||||
width: confTarget,
|
||||
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 err != nil {
|
||||
return nil, fmt.Errorf("estimate initial fee rate: %w", err)
|
||||
}
|
||||
|
||||
// Calculate how much fee rate should be increased per block.
|
||||
end := l.endingFeeRate
|
||||
delta := btcutil.Amount(end - start).MulF64(1 / float64(confTarget))
|
||||
|
||||
// We only allow the delta to be zero if the width is one - when the
|
||||
// delta is zero, it means the starting and ending fee rates are the
|
||||
// same, which means there's nothing to increase, so any width greater
|
||||
// than 1 doesn't provide any utility. This could happen when the
|
||||
// sweeper is offered to sweep an input that has passed its deadline.
|
||||
if delta == 0 && l.width != 1 {
|
||||
return nil, fmt.Errorf("fee rate delta is zero")
|
||||
}
|
||||
|
||||
// Attach the calculated values to the fee function.
|
||||
l.startingFeeRate = start
|
||||
l.currentFeeRate = start
|
||||
l.deltaFeeRate = chainfee.SatPerKWeight(delta)
|
||||
|
||||
log.Debugf("Linear fee function initialized with startingFeeRate=%v, "+
|
||||
"endingFeeRate=%v, width=%v, delta=%v", start, end,
|
||||
confTarget, l.deltaFeeRate)
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// FeeRate returns the current fee rate.
|
||||
//
|
||||
// NOTE: part of the FeeFunction interface.
|
||||
func (l *LinearFeeFunction) FeeRate() chainfee.SatPerKWeight {
|
||||
return l.currentFeeRate
|
||||
}
|
||||
|
||||
// Increment increases the fee rate by one position, returns a boolean to
|
||||
// indicate whether the fee rate was increased, and an error if the position is
|
||||
// greater than the width. The increased fee rate will be set as the current
|
||||
// fee rate, and the internal position will be incremented.
|
||||
//
|
||||
// NOTE: this method will change the state of the fee function as it increases
|
||||
// its current fee rate.
|
||||
//
|
||||
// NOTE: part of the FeeFunction interface.
|
||||
func (l *LinearFeeFunction) Increment() (bool, error) {
|
||||
return l.increaseFeeRate(l.position + 1)
|
||||
}
|
||||
|
||||
// IncreaseFeeRate calculate a new position using the given conf target, and
|
||||
// increases the fee rate to the new position by calling the Increment method.
|
||||
//
|
||||
// NOTE: this method will change the state of the fee function as it increases
|
||||
// its current fee rate.
|
||||
//
|
||||
// NOTE: part of the FeeFunction interface.
|
||||
func (l *LinearFeeFunction) IncreaseFeeRate(confTarget uint32) (bool, error) {
|
||||
// If the new position is already at the end, we return an error.
|
||||
if confTarget == 0 {
|
||||
return false, ErrMaxPosition
|
||||
}
|
||||
|
||||
newPosition := uint32(0)
|
||||
|
||||
// Only calculate the new position when the conf target is less than
|
||||
// the function's width - the width is the initial conf target, and we
|
||||
// expect the current conf target to decrease over time. However, we
|
||||
// still allow the supplied conf target to be greater than the width,
|
||||
// and we won't increase the fee rate in that case.
|
||||
if confTarget < l.width {
|
||||
newPosition = l.width - confTarget
|
||||
log.Tracef("Increasing position from %v to %v", l.position,
|
||||
newPosition)
|
||||
}
|
||||
|
||||
if newPosition <= l.position {
|
||||
log.Tracef("Skipped increase feerate: position=%v, "+
|
||||
"newPosition=%v ", l.position, newPosition)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return l.increaseFeeRate(newPosition)
|
||||
}
|
||||
|
||||
// increaseFeeRate increases the fee rate by the specified position, returns a
|
||||
// boolean to indicate whether the fee rate was increased, and an error if the
|
||||
// position is greater than the width. The increased fee rate will be set as
|
||||
// the current fee rate, and the internal position will be set to the specified
|
||||
// position.
|
||||
//
|
||||
// NOTE: this method will change the state of the fee function as it increases
|
||||
// its current fee rate.
|
||||
func (l *LinearFeeFunction) increaseFeeRate(position uint32) (bool, error) {
|
||||
// If the new position is already at the end, we return an error.
|
||||
if l.position >= l.width {
|
||||
return false, ErrMaxPosition
|
||||
}
|
||||
|
||||
// Get the old fee rate.
|
||||
oldFeeRate := l.currentFeeRate
|
||||
|
||||
// Update its internal state.
|
||||
l.position = position
|
||||
l.currentFeeRate = l.feeRateAtPosition(position)
|
||||
|
||||
log.Tracef("Fee rate increased from %v to %v at position %v",
|
||||
oldFeeRate, l.currentFeeRate, l.position)
|
||||
|
||||
return l.currentFeeRate > oldFeeRate, nil
|
||||
}
|
||||
|
||||
// feeRateAtPosition calculates the fee rate at a given position and caps it at
|
||||
// the ending fee rate.
|
||||
func (l *LinearFeeFunction) feeRateAtPosition(p uint32) chainfee.SatPerKWeight {
|
||||
if p >= l.width {
|
||||
return l.endingFeeRate
|
||||
}
|
||||
|
||||
feeRateDelta := btcutil.Amount(l.deltaFeeRate).MulF64(float64(p))
|
||||
|
||||
feeRate := l.startingFeeRate + chainfee.SatPerKWeight(feeRateDelta)
|
||||
if feeRate > l.endingFeeRate {
|
||||
return l.endingFeeRate
|
||||
}
|
||||
|
||||
return feeRate
|
||||
}
|
||||
|
||||
// estimateFeeRate asks the fee estimator to estimate the fee rate based on its
|
||||
// conf target.
|
||||
func (l *LinearFeeFunction) estimateFeeRate(
|
||||
confTarget uint32) (chainfee.SatPerKWeight, error) {
|
||||
|
||||
fee := FeeEstimateInfo{
|
||||
ConfTarget: confTarget,
|
||||
}
|
||||
|
||||
// endingFeeRate comes from budget/txWeight, which means the returned
|
||||
// fee rate will always be capped by this value, hence we don't need to
|
||||
// worry about overpay.
|
||||
estimatedFeeRate, err := fee.Estimate(l.estimator, l.endingFeeRate)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return estimatedFeeRate, nil
|
||||
}
|
247
sweep/fee_function_test.go
Normal file
247
sweep/fee_function_test.go
Normal file
@ -0,0 +1,247 @@
|
||||
package sweep
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestLinearFeeFunctionNew tests the NewLinearFeeFunction function.
|
||||
func TestLinearFeeFunctionNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := require.New(t)
|
||||
|
||||
// Create a mock fee estimator.
|
||||
estimator := &chainfee.MockEstimator{}
|
||||
|
||||
// Create testing params.
|
||||
maxFeeRate := chainfee.SatPerKWeight(10000)
|
||||
estimatedFeeRate := chainfee.SatPerKWeight(500)
|
||||
confTarget := uint32(6)
|
||||
|
||||
// Assert init fee function with zero conf value returns an error.
|
||||
f, err := NewLinearFeeFunction(maxFeeRate, 0, estimator)
|
||||
rt.ErrorContains(err, "width must be greater than zero")
|
||||
rt.Nil(f)
|
||||
|
||||
// When the fee estimator returns an error, it's returned.
|
||||
//
|
||||
// Mock the fee estimator to return an error.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
chainfee.SatPerKWeight(0), errDummy).Once()
|
||||
|
||||
f, err = NewLinearFeeFunction(maxFeeRate, confTarget, estimator)
|
||||
rt.ErrorIs(err, errDummy)
|
||||
rt.Nil(f)
|
||||
|
||||
// When the starting feerate is greater than the ending feerate, the
|
||||
// starting feerate is capped.
|
||||
//
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
smallConf := uint32(1)
|
||||
estimator.On("EstimateFeePerKW", smallConf).Return(
|
||||
// The fee rate is greater than the max fee rate.
|
||||
maxFeeRate+1, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
|
||||
f, err = NewLinearFeeFunction(maxFeeRate, smallConf, estimator)
|
||||
rt.NoError(err)
|
||||
rt.NotNil(f)
|
||||
|
||||
// When the calculated fee rate delta is 0, an error should be returned.
|
||||
//
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
// The starting fee rate is 1 sat/kw less than the max fee rate.
|
||||
maxFeeRate-1, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
|
||||
f, err = NewLinearFeeFunction(maxFeeRate, confTarget, estimator)
|
||||
rt.ErrorContains(err, "fee rate delta is zero")
|
||||
rt.Nil(f)
|
||||
|
||||
// Check a successfully created fee function.
|
||||
//
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
estimatedFeeRate, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
|
||||
f, err = NewLinearFeeFunction(maxFeeRate, confTarget, estimator)
|
||||
rt.NoError(err)
|
||||
rt.NotNil(f)
|
||||
|
||||
// Assert the internal state.
|
||||
rt.Equal(estimatedFeeRate, f.startingFeeRate)
|
||||
rt.Equal(maxFeeRate, f.endingFeeRate)
|
||||
rt.Equal(estimatedFeeRate, f.currentFeeRate)
|
||||
rt.NotZero(f.deltaFeeRate)
|
||||
rt.Equal(confTarget, f.width)
|
||||
}
|
||||
|
||||
// TestLinearFeeFunctionFeeRateAtPosition checks the expected feerate is
|
||||
// calculated and returned.
|
||||
func TestLinearFeeFunctionFeeRateAtPosition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := require.New(t)
|
||||
|
||||
// Create a fee func which has three positions:
|
||||
// - position 0: 1000
|
||||
// - position 1: 2000
|
||||
// - position 2: 3000
|
||||
f := &LinearFeeFunction{
|
||||
startingFeeRate: 1000,
|
||||
endingFeeRate: 3000,
|
||||
position: 0,
|
||||
deltaFeeRate: 1000,
|
||||
width: 3,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
pos uint32
|
||||
expectedFeerate chainfee.SatPerKWeight
|
||||
}{
|
||||
{
|
||||
name: "position 0",
|
||||
pos: 0,
|
||||
expectedFeerate: 1000,
|
||||
},
|
||||
{
|
||||
name: "position 1",
|
||||
pos: 1,
|
||||
expectedFeerate: 2000,
|
||||
},
|
||||
{
|
||||
name: "position 2",
|
||||
pos: 2,
|
||||
expectedFeerate: 3000,
|
||||
},
|
||||
{
|
||||
name: "position 3",
|
||||
pos: 3,
|
||||
expectedFeerate: 3000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := f.feeRateAtPosition(tc.pos)
|
||||
rt.Equal(tc.expectedFeerate, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLinearFeeFunctionIncrement checks the internal state is updated
|
||||
// correctly when the fee rate is incremented.
|
||||
func TestLinearFeeFunctionIncrement(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := require.New(t)
|
||||
|
||||
// Create a mock fee estimator.
|
||||
estimator := &chainfee.MockEstimator{}
|
||||
|
||||
// Create testing params. These params are chosen so the delta value is
|
||||
// 100.
|
||||
maxFeeRate := chainfee.SatPerKWeight(1000)
|
||||
estimatedFeeRate := chainfee.SatPerKWeight(100)
|
||||
confTarget := uint32(9)
|
||||
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
estimatedFeeRate, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
|
||||
f, err := NewLinearFeeFunction(maxFeeRate, confTarget, estimator)
|
||||
rt.NoError(err)
|
||||
|
||||
// We now increase the position from 1 to 9.
|
||||
for i := uint32(1); i <= confTarget; i++ {
|
||||
// Increase the fee rate.
|
||||
increased, err := f.Increment()
|
||||
rt.NoError(err)
|
||||
rt.True(increased)
|
||||
|
||||
// Assert the internal state.
|
||||
rt.Equal(i, f.position)
|
||||
|
||||
delta := chainfee.SatPerKWeight(i * 100)
|
||||
rt.Equal(estimatedFeeRate+delta, f.currentFeeRate)
|
||||
|
||||
// Check public method returns the expected fee rate.
|
||||
rt.Equal(estimatedFeeRate+delta, f.FeeRate())
|
||||
}
|
||||
|
||||
// Now the position is at 9th, increase it again should give us an
|
||||
// error.
|
||||
increased, err := f.Increment()
|
||||
rt.ErrorIs(err, ErrMaxPosition)
|
||||
rt.False(increased)
|
||||
}
|
||||
|
||||
// TestLinearFeeFunctionIncreaseFeeRate checks the internal state is updated
|
||||
// correctly when the fee rate is increased using conf targets.
|
||||
func TestLinearFeeFunctionIncreaseFeeRate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := require.New(t)
|
||||
|
||||
// Create a mock fee estimator.
|
||||
estimator := &chainfee.MockEstimator{}
|
||||
|
||||
// Create testing params. These params are chosen so the delta value is
|
||||
// 100.
|
||||
maxFeeRate := chainfee.SatPerKWeight(1000)
|
||||
estimatedFeeRate := chainfee.SatPerKWeight(100)
|
||||
confTarget := uint32(9)
|
||||
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
estimatedFeeRate, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
|
||||
f, err := NewLinearFeeFunction(maxFeeRate, confTarget, estimator)
|
||||
rt.NoError(err)
|
||||
|
||||
// If we are increasing the fee rate using the initial conf target, we
|
||||
// should get a nil error and false.
|
||||
increased, err := f.IncreaseFeeRate(confTarget)
|
||||
rt.NoError(err)
|
||||
rt.False(increased)
|
||||
|
||||
// Test that we are allowed to use a larger conf target.
|
||||
increased, err = f.IncreaseFeeRate(confTarget + 1)
|
||||
rt.NoError(err)
|
||||
rt.False(increased)
|
||||
|
||||
// Test that when we use a conf target of 0, we get an error.
|
||||
increased, err = f.IncreaseFeeRate(0)
|
||||
rt.ErrorIs(err, ErrMaxPosition)
|
||||
rt.False(increased)
|
||||
|
||||
// We now increase the fee rate from conf target 8 to 1 and assert we
|
||||
// get no error and true.
|
||||
for i := uint32(1); i < confTarget; i++ {
|
||||
// Increase the fee rate.
|
||||
increased, err := f.IncreaseFeeRate(confTarget - i)
|
||||
rt.NoError(err)
|
||||
rt.True(increased)
|
||||
|
||||
// Assert the internal state.
|
||||
rt.Equal(i, f.position)
|
||||
|
||||
delta := chainfee.SatPerKWeight(i * 100)
|
||||
rt.Equal(estimatedFeeRate+delta, f.currentFeeRate)
|
||||
|
||||
// Check public method returns the expected fee rate.
|
||||
rt.Equal(estimatedFeeRate+delta, f.FeeRate())
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user