Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion chainreg/chainregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
MinHtlcIn: cfg.Bitcoin.MinHTLCIn,
FeeEstimator: chainfee.NewStaticEstimator(
DefaultBitcoinStaticFeePerKW,
DefaultBitcoinStaticMinRelayFeeRate,
cfg.Fee.FeeFloorKW(),
),
}

Expand Down Expand Up @@ -409,6 +409,7 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
cc.FeeEstimator, err = chainfee.NewBitcoindEstimator(
*rpcConfig, bitcoindMode.EstimateMode,
fallBackFeeRate.FeePerKWeight(),
cfg.Fee.FeeFloorKW(),
)
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -672,6 +673,7 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
fallBackFeeRate := chainfee.SatPerKVByte(25 * 1000)
cc.FeeEstimator, err = chainfee.NewBtcdEstimator(
*rpcConfig, fallBackFeeRate.FeePerKWeight(),
cfg.Fee.FeeFloorKW(),
)
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -729,6 +731,7 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
!cacheFees,
cfg.Fee.MinUpdateTimeout,
cfg.Fee.MaxUpdateTimeout,
cfg.Fee.MinRelayFeeRate.FeePerKVByte(),
)
if err != nil {
return nil, nil, err
Expand Down
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ func DefaultConfig() Config {
Fee: &lncfg.Fee{
MinUpdateTimeout: lncfg.DefaultMinUpdateTimeout,
MaxUpdateTimeout: lncfg.DefaultMaxUpdateTimeout,
MinRelayFeeRate: lncfg.DefaultMinRelayFeeRate,
},

SubRPCServers: &subRPCServerConfigs{
Expand Down
34 changes: 30 additions & 4 deletions lncfg/fee.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package lncfg

import "time"
import (
"fmt"
"time"

"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)

// DefaultMinUpdateTimeout represents the minimum interval in which a
// WebAPIEstimator will request fresh fees from its API.
Expand All @@ -10,11 +15,32 @@ const DefaultMinUpdateTimeout = 5 * time.Minute
// WebAPIEstimator will request fresh fees from its API.
const DefaultMaxUpdateTimeout = 20 * time.Minute

// DefaultMinRelayFeeRate is the default minimum relay fee rate floor in
// sat/vb. 1 sat/vb equals 250 sat/kw; the relay floor is set to 1 sat/vb
// so that the effective floor remains FeePerKwFloor (253 sat/kw) after
// the minFeeManager rounds up from the backend's reported value.
const DefaultMinRelayFeeRate chainfee.SatPerVByte = 1

// Fee holds the configuration options for fee estimation.
//
//nolint:ll
type Fee struct {
URL string `long:"url" description:"Optional URL for external fee estimation. If no URL is specified, the method for fee estimation will depend on the chosen backend and network. Must be set for neutrino on mainnet."`
MinUpdateTimeout time.Duration `long:"min-update-timeout" description:"The minimum interval in which fees will be updated from the specified fee URL."`
MaxUpdateTimeout time.Duration `long:"max-update-timeout" description:"The maximum interval in which fees will be updated from the specified fee URL."`
URL string `long:"url" description:"Optional URL for external fee estimation. If no URL is specified, the method for fee estimation will depend on the chosen backend and network. Must be set for neutrino on mainnet."`
MinUpdateTimeout time.Duration `long:"min-update-timeout" description:"The minimum interval in which fees will be updated from the specified fee URL."`
MaxUpdateTimeout time.Duration `long:"max-update-timeout" description:"The maximum interval in which fees will be updated from the specified fee URL."`
MinRelayFeeRate chainfee.SatPerVByte `long:"min-relay-feerate" description:"Minimum fee rate floor in sat/vb used when estimating and enforcing on-chain fees. Lowering this below 1 sat/vb is only safe when your Bitcoin backend is configured with a matching minrelaytxfee and you control the path to miners. Default: 1 sat/vb (250 sat/kw)."`
}

// Validate checks the fee configuration for invalid values.
func (f *Fee) Validate() error {
if f.MinRelayFeeRate < 0 {
return fmt.Errorf("fee.min-relay-feerate must be >= 0")
}

return nil
}

// FeeFloorKW returns the configured minimum relay fee rate as sat/kw.
func (f *Fee) FeeFloorKW() chainfee.SatPerKWeight {
return f.MinRelayFeeRate.FeePerKWeight()
}
Comment thread
Kilombino marked this conversation as resolved.
47 changes: 34 additions & 13 deletions lnwallet/chainfee/estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ type BtcdEstimator struct {
// produce fee estimates.
fallbackFeePerKW SatPerKWeight

// feeFloor is the minimum fee rate the minFeeManager will enforce.
// When zero, FeePerKwFloor is used as the default.
feeFloor SatPerKWeight

// minFeeManager is used to query the current minimum fee, in sat/kw,
// that we should enforce. This will be used to determine fee rate for
// a transaction when the estimated fee rate is too low to allow the
Expand All @@ -167,7 +171,8 @@ type BtcdEstimator struct {
// the occasion that the estimator has insufficient data, or returns zero for a
// fee estimate.
func NewBtcdEstimator(rpcConfig rpcclient.ConnConfig,
fallBackFeeRate SatPerKWeight) (*BtcdEstimator, error) {
fallBackFeeRate SatPerKWeight,
feeFloor SatPerKWeight) (*BtcdEstimator, error) {

rpcConfig.DisableConnectOnNew = true
rpcConfig.DisableAutoReconnect = false
Expand All @@ -182,6 +187,7 @@ func NewBtcdEstimator(rpcConfig rpcclient.ConnConfig,

return &BtcdEstimator{
fallbackFeePerKW: fallBackFeeRate,
feeFloor: feeFloor,
btcdConn: chainConn,
filterManager: newFilterManager(fetchCb),
}, nil
Expand All @@ -200,7 +206,7 @@ func (b *BtcdEstimator) Start() error {
// can initialise the minimum relay fee manager which queries the
// chain backend for the minimum relay fee on construction.
minRelayFeeManager, err := newMinFeeManager(
defaultUpdateInterval, b.fetchMinRelayFee,
defaultUpdateInterval, b.fetchMinRelayFee, b.feeFloor,
)
if err != nil {
return err
Expand Down Expand Up @@ -341,6 +347,10 @@ type BitcoindEstimator struct {
// produce fee estimates.
fallbackFeePerKW SatPerKWeight

// feeFloor is the minimum fee rate the minFeeManager will enforce.
// When zero, FeePerKwFloor is used as the default.
feeFloor SatPerKWeight

// minFeeManager is used to keep track of the minimum fee, in sat/kw,
// that we should enforce. This will be used as the default fee rate
// for a transaction when the estimated fee rate is too low to allow
Expand Down Expand Up @@ -368,7 +378,8 @@ type BitcoindEstimator struct {
// in the occasion that the estimator has insufficient data, or returns zero
// for a fee estimate.
func NewBitcoindEstimator(rpcConfig rpcclient.ConnConfig, feeMode string,
fallBackFeeRate SatPerKWeight) (*BitcoindEstimator, error) {
fallBackFeeRate SatPerKWeight,
feeFloor SatPerKWeight) (*BitcoindEstimator, error) {

rpcConfig.DisableConnectOnNew = true
rpcConfig.DisableAutoReconnect = false
Expand All @@ -385,6 +396,7 @@ func NewBitcoindEstimator(rpcConfig rpcclient.ConnConfig, feeMode string,

return &BitcoindEstimator{
fallbackFeePerKW: fallBackFeeRate,
feeFloor: feeFloor,
bitcoindConn: chainConn,
feeMode: feeMode,
filterManager: newFilterManager(fetchCb),
Expand All @@ -402,6 +414,7 @@ func (b *BitcoindEstimator) Start() error {
relayFeeManager, err := newMinFeeManager(
defaultUpdateInterval,
b.fetchMinMempoolFee,
b.feeFloor,
)
if err != nil {
return err
Expand Down Expand Up @@ -670,12 +683,6 @@ func (s SparseConfFeeSource) parseResponse(r io.Reader) (
return WebAPIResponse{}, err
}

if resp.MinRelayFeerate == 0 {
log.Errorf("No min relay fee rate available, using default %v",
FeePerKwFloor)
resp.MinRelayFeerate = FeePerKwFloor.FeePerKVByte()
}

return resp, nil
}

Expand Down Expand Up @@ -745,6 +752,11 @@ type WebAPIEstimator struct {
feeByBlockTarget map[uint32]uint32
minRelayFeerate SatPerKVByte

// feeFloor is the minimum fee floor in sat/kvb. When the API reports a
// relay fee rate below this value (or reports none at all), feeFloor is
// used instead. Defaults to FeePerKwFloor.FeePerKVByte() when zero.
feeFloor SatPerKVByte

// noCache determines whether the web estimator should cache fee
// estimates.
noCache bool
Expand All @@ -765,7 +777,8 @@ type WebAPIEstimator struct {
// fallback default fee. The fees are updated whenever a new block is mined.
func NewWebAPIEstimator(api WebAPIFeeSource, noCache bool,
minFeeUpdateTimeout time.Duration,
maxFeeUpdateTimeout time.Duration) (*WebAPIEstimator, error) {
maxFeeUpdateTimeout time.Duration,
feeFloor SatPerKVByte) (*WebAPIEstimator, error) {

if minFeeUpdateTimeout == 0 || maxFeeUpdateTimeout == 0 {
return nil, fmt.Errorf("minFeeUpdateTimeout and " +
Expand All @@ -781,6 +794,7 @@ func NewWebAPIEstimator(api WebAPIFeeSource, noCache bool,
return &WebAPIEstimator{
apiSource: api,
feeByBlockTarget: make(map[uint32]uint32),
feeFloor: feeFloor,
noCache: noCache,
quit: make(chan struct{}),
minFeeUpdateTimeout: minFeeUpdateTimeout,
Expand Down Expand Up @@ -825,8 +839,8 @@ func (w *WebAPIEstimator) EstimateFeePerKW(numBlocks uint32) (
// If the result is too low, then we'll clamp it to our current fee
// floor.
satPerKw := SatPerKVByte(feePerKb).FeePerKWeight()
if satPerKw < FeePerKwFloor {
satPerKw = FeePerKwFloor
if satPerKw < w.feeFloor.FeePerKWeight() {
satPerKw = w.feeFloor.FeePerKWeight()
}

log.Debugf("Web API returning %v sat/kw for conf target of %v",
Expand Down Expand Up @@ -1014,9 +1028,16 @@ func (w *WebAPIEstimator) updateFeeEstimates() {
return string(resp)
}))

minRelayFeerate := resp.MinRelayFeerate
if minRelayFeerate == 0 || minRelayFeerate < w.feeFloor {
log.Debugf("API relay fee rate %v below configured floor %v, "+
"using floor", minRelayFeerate, w.feeFloor)
minRelayFeerate = w.feeFloor
}

w.feesMtx.Lock()
w.feeByBlockTarget = resp.FeeByBlockTarget
w.minRelayFeerate = resp.MinRelayFeerate
w.minRelayFeerate = minRelayFeerate
w.feesMtx.Unlock()
}

Expand Down
8 changes: 4 additions & 4 deletions lnwallet/chainfee/estimator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ func TestWebAPIFeeEstimator(t *testing.T) {
feeSource.On("GetFeeInfo").Return(resp, nil)

estimator, _ := NewWebAPIEstimator(
feeSource, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
feeSource, false, minFeeUpdateTimeout, maxFeeUpdateTimeout, 0,
)

// Test that when the estimator is not started, an error is returned.
Expand Down Expand Up @@ -305,7 +305,7 @@ func TestGetCachedFee(t *testing.T) {

// Create a dummy estimator without WebAPIFeeSource.
estimator, _ := NewWebAPIEstimator(
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout, 0,
)

// When the cache is empty, an error should be returned.
Expand Down Expand Up @@ -381,7 +381,7 @@ func TestRandomFeeUpdateTimeout(t *testing.T) {
)

estimator, _ := NewWebAPIEstimator(
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout, 0,
)

for i := 0; i < 1000; i++ {
Expand All @@ -401,7 +401,7 @@ func TestInvalidFeeUpdateTimeout(t *testing.T) {
)

_, err := NewWebAPIEstimator(
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout, 0,
)
require.Error(t, err, "NewWebAPIEstimator should return an error "+
"when minFeeUpdateTimeout > maxFeeUpdateTimeout")
Expand Down
15 changes: 10 additions & 5 deletions lnwallet/chainfee/minfeemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const defaultUpdateInterval = 10 * time.Minute
type minFeeManager struct {
mu sync.Mutex
minFeePerKW SatPerKWeight
feeFloor SatPerKWeight
lastUpdatedTime time.Time
minUpdateInterval time.Duration
fetchFeeFunc fetchFee
Expand All @@ -24,8 +25,11 @@ type fetchFee func() (SatPerKWeight, error)
// newMinFeeManager creates a new minFeeManager and uses the
// given fetchMinFee function to set the minFeePerKW of the minFeeManager.
// This function requires the fetchMinFee function to succeed.
//
// feeFloor sets the minimum fee rate the manager will ever return. It defaults
// to FeePerKwFloor when zero is passed in.
func newMinFeeManager(minUpdateInterval time.Duration,
fetchMinFee fetchFee) (*minFeeManager, error) {
fetchMinFee fetchFee, feeFloor SatPerKWeight) (*minFeeManager, error) {

minFee, err := fetchMinFee()
if err != nil {
Expand All @@ -34,12 +38,13 @@ func newMinFeeManager(minUpdateInterval time.Duration,

// Ensure that the minimum fee we use is always clamped by our fee
// floor.
if minFee < FeePerKwFloor {
minFee = FeePerKwFloor
if minFee < feeFloor {
minFee = feeFloor
}

return &minFeeManager{
minFeePerKW: minFee,
feeFloor: feeFloor,
lastUpdatedTime: time.Now(),
minUpdateInterval: minUpdateInterval,
fetchFeeFunc: fetchMinFee,
Expand Down Expand Up @@ -70,8 +75,8 @@ func (m *minFeeManager) fetchMinFee() SatPerKWeight {
// minimum fee rate we'll propose for transactions. However, if this
// happens to be lower than our fee floor, we'll enforce that instead.
m.minFeePerKW = newMinFee
if m.minFeePerKW < FeePerKwFloor {
m.minFeePerKW = FeePerKwFloor
if m.minFeePerKW < m.feeFloor {
m.minFeePerKW = m.feeFloor
}
m.lastUpdatedTime = time.Now()

Expand Down
62 changes: 60 additions & 2 deletions lnwallet/chainfee/minfeemanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ func TestMinFeeManager(t *testing.T) {
minFee: FeePerKwFloor - 1,
}

// Initialise the min fee manager. This should call the chain backend
// once.
// Initialise the min fee manager with the standard floor. This should
// call the chain backend once.
feeManager, err := newMinFeeManager(
100*time.Millisecond,
chainBackend.fetchFee,
FeePerKwFloor,
)
require.NoError(t, err)
require.Equal(t, 1, chainBackend.callCount)
Expand All @@ -57,3 +58,60 @@ func TestMinFeeManager(t *testing.T) {
require.Equal(t, SatPerKWeight(2000), minFee)
require.Equal(t, 2, chainBackend.callCount)
}

// TestMinFeeManagerCustomFloor verifies that a custom feeFloor is respected,
// allowing fee rates below FeePerKwFloor when explicitly configured.
func TestMinFeeManagerCustomFloor(t *testing.T) {
t.Parallel()

// A custom floor of 1 sat/kw — effectively no floor.
customFloor := SatPerKWeight(1)

// Backend returns a fee rate well below the standard FeePerKwFloor.
backendFee := SatPerKWeight(100) // ~0.4 sat/vb
chainBackend := &mockChainBackend{minFee: backendFee}

feeManager, err := newMinFeeManager(
100*time.Millisecond,
chainBackend.fetchFee,
customFloor,
)
require.NoError(t, err)

// The manager should store and return the backend fee since it is
// above the custom floor, not clamped to FeePerKwFloor.
require.Equal(t, backendFee, feeManager.minFeePerKW)

minFee := feeManager.fetchMinFee()
require.Equal(t, backendFee, minFee)
}

// TestMinFeeManagerFloorClampsBackend verifies that when the backend returns a
// fee below the configured feeFloor, the floor is enforced.
func TestMinFeeManagerFloorClampsBackend(t *testing.T) {
t.Parallel()

customFloor := SatPerKWeight(500) // custom floor: ~2 sat/vb

// Backend returns something below the custom floor.
chainBackend := &mockChainBackend{minFee: SatPerKWeight(100)}

feeManager, err := newMinFeeManager(
100*time.Millisecond,
chainBackend.fetchFee,
customFloor,
)
require.NoError(t, err)

// The fee should be clamped to our custom floor.
require.Equal(t, customFloor, feeManager.minFeePerKW)

// Fake time passing and have the backend return a fee above the floor.
feeManager.lastUpdatedTime = time.Now().Add(-200 * time.Millisecond)
feeManager.fetchFeeFunc = (&mockChainBackend{
minFee: SatPerKWeight(800),
}).fetchFee

minFee := feeManager.fetchMinFee()
require.Equal(t, SatPerKWeight(800), minFee)
}
Loading
Loading