Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clamp minimum relay feerate #9106

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
22 changes: 18 additions & 4 deletions chainreg/chainregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,10 +400,16 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
// if we're using bitcoind as a backend, then we can
// use live fee estimates, rather than a statically
// coded value.
fallBackFeeRate := chainfee.SatPerKVByte(25 * 1000)
cc.FeeEstimator, err = chainfee.NewBitcoindEstimator(
*rpcConfig, bitcoindMode.EstimateMode,
fallBackFeeRate.FeePerKWeight(),
chainfee.EstimatorConfig{
FallbackFeePerKW: chainfee.SatPerVByte(
cfg.Fee.FallbackFeeRate,
).FeePerKWeight(),
MaxMinRelayFeePerKW: chainfee.SatPerVByte(
cfg.Fee.MaxMinRelayFeeRate,
).FeePerKWeight(),
},
)
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -665,9 +671,15 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
// if we're using btcd as a backend, then we can use
// live fee estimates, rather than a statically coded
// value.
fallBackFeeRate := chainfee.SatPerKVByte(25 * 1000)
cc.FeeEstimator, err = chainfee.NewBtcdEstimator(
*rpcConfig, fallBackFeeRate.FeePerKWeight(),
*rpcConfig, chainfee.EstimatorConfig{
FallbackFeePerKW: chainfee.SatPerVByte(
cfg.Fee.FallbackFeeRate,
).FeePerKWeight(),
MaxMinRelayFeePerKW: chainfee.SatPerVByte(
cfg.Fee.MaxMinRelayFeeRate,
).FeePerKWeight(),
},
)
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -725,6 +737,8 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
!cacheFees,
cfg.Fee.MinUpdateTimeout,
cfg.Fee.MaxUpdateTimeout,
chainfee.SatPerVByte(cfg.Fee.MaxMinRelayFeeRate).
FeePerKWeight(),
)
if err != nil {
return nil, nil, err
Expand Down
6 changes: 4 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,8 +598,10 @@ func DefaultConfig() Config {
ConnectionTimeout: tor.DefaultConnTimeout,

Fee: &lncfg.Fee{
MinUpdateTimeout: lncfg.DefaultMinUpdateTimeout,
MaxUpdateTimeout: lncfg.DefaultMaxUpdateTimeout,
MinUpdateTimeout: lncfg.DefaultMinUpdateTimeout,
MaxUpdateTimeout: lncfg.DefaultMaxUpdateTimeout,
MaxMinRelayFeeRate: lncfg.DefaultMaxMinRelayFeeRate,
FallbackFeeRate: lncfg.DefaultFallbackFeeRate,
},

SubRPCServers: &subRPCServerConfigs{
Expand Down
3 changes: 3 additions & 0 deletions docs/release-notes/release-notes-0.19.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
# Improvements
## Functional Updates

* [Clamp the min relay fee rate](https://github.com/lightningnetwork/lnd/pull/9106)
to prevent misconfigured bitcoin backends from causing too high fee estimates.

## RPC Updates

## lncli Updates
Expand Down
56 changes: 46 additions & 10 deletions lncfg/fee.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,56 @@
package lncfg

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

// DefaultMinUpdateTimeout represents the minimum interval in which a
// WebAPIEstimator will request fresh fees from its API.
const DefaultMinUpdateTimeout = 5 * time.Minute
const (
// DefaultMinUpdateTimeout represents the minimum interval in which a
// WebAPIEstimator will request fresh fees from its API.
DefaultMinUpdateTimeout = 5 * time.Minute

// DefaultMaxUpdateTimeout represents the maximum interval in which a
// WebAPIEstimator will request fresh fees from its API.
const DefaultMaxUpdateTimeout = 20 * time.Minute
// DefaultMaxUpdateTimeout represents the maximum interval in which a
// WebAPIEstimator will request fresh fees from its API.
DefaultMaxUpdateTimeout = 20 * time.Minute

// DefaultMaxMinRelayFeeRate is the default maximum minimum relay fee
// rate in sat/vbyte.
DefaultMaxMinRelayFeeRate = 200

// DefaultFallbackFeeRate is the default fallback fee rate in sat/vbyte
// that will be used if the fee estimation method fails.
DefaultFallbackFeeRate = 25
)

// Fee holds the configuration options for fee estimation.
//
//nolint:lll
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."`
MaxMinRelayFeeRate uint64 `long:"max-min-relay-feerate" description:"The maximum relay fee rate in sat/vbyte that will be used although the backend would return a higher fee rate. Must be large enough to ensure transaction propagation in a croweded mempool environment."`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think we can call this MaxRelayFeeRate

FallbackFeeRate uint64 `long:"fallback-feerate" description:"The fee rate in sat/vbyte that will be used if the fee estimation method fails."`
}

// Validate checks the values configured for the fee estimator.
func (f *Fee) Validate() error {
if f.MinUpdateTimeout < 0 {
return fmt.Errorf("min-update-timeout must be positive")
}

if f.MaxUpdateTimeout < 0 {
return fmt.Errorf("max-update-timeout must be positive")
}

if f.MaxMinRelayFeeRate < 10 {
return fmt.Errorf("max-min-relay-feerate must be at least 10")
}

if f.FallbackFeeRate < 10 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm think this may be too large

return fmt.Errorf("fallback-feerate must be at least 10")
}

return nil
}
57 changes: 43 additions & 14 deletions lnwallet/chainfee/estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ var (
errEmptyCache = errors.New("fee rate cache is empty")
)

// EstimatorConfig is the configuration for the fee estimator.
type EstimatorConfig struct {
MaxMinRelayFeePerKW SatPerKWeight
FallbackFeePerKW SatPerKWeight
}

// Estimator provides the ability to estimate on-chain transaction fees for
// various combinations of transaction sizes and desired confirmation time
// (measured by number of blocks).
Expand Down Expand Up @@ -147,6 +153,10 @@ type BtcdEstimator struct {
// produce fee estimates.
fallbackFeePerKW SatPerKWeight

// maxMinRelayFeePerKW is the maximum minimum relay fee rate in sat/kw
// that we will use.
maxMinRelayFeePerKW 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 +177,7 @@ 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) {
cfg EstimatorConfig) (*BtcdEstimator, error) {

rpcConfig.DisableConnectOnNew = true
rpcConfig.DisableAutoReconnect = false
Expand All @@ -181,9 +191,10 @@ func NewBtcdEstimator(rpcConfig rpcclient.ConnConfig,
}

return &BtcdEstimator{
fallbackFeePerKW: fallBackFeeRate,
btcdConn: chainConn,
filterManager: newFilterManager(fetchCb),
fallbackFeePerKW: cfg.FallbackFeePerKW,
maxMinRelayFeePerKW: cfg.MaxMinRelayFeePerKW,
btcdConn: chainConn,
filterManager: newFilterManager(fetchCb),
}, nil
}

Expand All @@ -200,7 +211,8 @@ 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.maxMinRelayFeePerKW,
b.fetchMinRelayFee,
)
if err != nil {
return err
Expand Down Expand Up @@ -340,6 +352,10 @@ type BitcoindEstimator struct {
// produce fee estimates.
fallbackFeePerKW SatPerKWeight

// maxMinRelayFeePerKW is the maximum minimum relay fee rate in sat/kw
// that we will use.
maxMinRelayFeePerKW 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 @@ -367,7 +383,7 @@ 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) {
cfg EstimatorConfig) (*BitcoindEstimator, error) {

rpcConfig.DisableConnectOnNew = true
rpcConfig.DisableAutoReconnect = false
Expand All @@ -383,10 +399,11 @@ func NewBitcoindEstimator(rpcConfig rpcclient.ConnConfig, feeMode string,
}

return &BitcoindEstimator{
fallbackFeePerKW: fallBackFeeRate,
bitcoindConn: chainConn,
feeMode: feeMode,
filterManager: newFilterManager(fetchCb),
fallbackFeePerKW: cfg.FallbackFeePerKW,
maxMinRelayFeePerKW: cfg.MaxMinRelayFeePerKW,
bitcoindConn: chainConn,
feeMode: feeMode,
filterManager: newFilterManager(fetchCb),
}, nil
}

Expand All @@ -399,7 +416,7 @@ func (b *BitcoindEstimator) Start() error {
// initialise the minimum relay fee manager which will query
// the backend node for its minimum mempool fee.
relayFeeManager, err := newMinFeeManager(
defaultUpdateInterval,
defaultUpdateInterval, b.maxMinRelayFeePerKW,
b.fetchMinMempoolFee,
)
if err != nil {
Expand Down Expand Up @@ -756,15 +773,19 @@ type WebAPIEstimator struct {
// web estimator will request fresh fees from its API.
maxFeeUpdateTimeout time.Duration

// maxMinRelayFeeRate is the maximum minimum relay fee rate which will
// be used.
maxMinRelayFeeRate SatPerKWeight

quit chan struct{}
wg sync.WaitGroup
}

// NewWebAPIEstimator creates a new WebAPIEstimator from a given URL and a
// 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) {
minFeeUpdateTimeout time.Duration, maxFeeUpdateTimeout time.Duration,
maxMinRelayFeeRate SatPerKWeight) (*WebAPIEstimator, error) {

if minFeeUpdateTimeout == 0 || maxFeeUpdateTimeout == 0 {
return nil, fmt.Errorf("minFeeUpdateTimeout and " +
Expand All @@ -784,6 +805,7 @@ func NewWebAPIEstimator(api WebAPIFeeSource, noCache bool,
quit: make(chan struct{}),
minFeeUpdateTimeout: minFeeUpdateTimeout,
maxFeeUpdateTimeout: maxFeeUpdateTimeout,
maxMinRelayFeeRate: maxMinRelayFeeRate,
}, nil
}

Expand Down Expand Up @@ -911,7 +933,14 @@ func (w *WebAPIEstimator) RelayFeePerKW() SatPerKWeight {
log.Infof("Web API returning %v for min relay feerate",
w.minRelayFeerate)

return w.minRelayFeerate.FeePerKWeight()
minRelayFee := w.minRelayFeerate.FeePerKWeight()

// Clamp the minimum relay fee rate.
if minRelayFee > w.maxMinRelayFeeRate {
minRelayFee = w.maxMinRelayFeeRate
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think we need a warning log here

}

return minRelayFee
}

// randomFeeUpdateTimeout returns a random timeout between minFeeUpdateTimeout
Expand Down
16 changes: 16 additions & 0 deletions lnwallet/chainfee/estimator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ func TestWebAPIFeeEstimator(t *testing.T) {

minFeeUpdateTimeout = 5 * time.Minute
maxFeeUpdateTimeout = 20 * time.Minute

// Equivalent to 200 sat/vbyte.
maxMinRelayFeeRate SatPerKWeight = 50000
)

testCases := []struct {
Expand Down Expand Up @@ -250,6 +253,7 @@ func TestWebAPIFeeEstimator(t *testing.T) {

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

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

minFeeUpdateTimeout = 5 * time.Minute
maxFeeUpdateTimeout = 20 * time.Minute

// Equivalent to 200 sat/vbyte.
maxMinRelayFeeRate SatPerKWeight = 50000
)

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

// When the cache is empty, an error should be returned.
Expand Down Expand Up @@ -378,10 +386,14 @@ func TestRandomFeeUpdateTimeout(t *testing.T) {
var (
minFeeUpdateTimeout = 1 * time.Minute
maxFeeUpdateTimeout = 2 * time.Minute

// Equivalent to 200 sat/vbyte.
maxMinRelayFeeRate SatPerKWeight = 50000
)

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

for i := 0; i < 1000; i++ {
Expand All @@ -398,10 +410,14 @@ func TestInvalidFeeUpdateTimeout(t *testing.T) {
var (
minFeeUpdateTimeout = 2 * time.Minute
maxFeeUpdateTimeout = 1 * time.Minute

// Equivalent to 200 sat/vbyte.
maxMinRelayFeeRate SatPerKWeight = 50000
)

_, err := NewWebAPIEstimator(
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
maxMinRelayFeeRate,
)
require.Error(t, err, "NewWebAPIEstimator should return an error "+
"when minFeeUpdateTimeout > maxFeeUpdateTimeout")
Expand Down
Loading
Loading