mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-27 22:36:16 +02:00
chainfee: introduce filterManager and use it for fee floor
This commit introduces a filterManager that uses our outbound peers' feefilter values to determine an acceptable minimum feerate that ensures successful transaction propagation. Under the hood, a moving median is used as it is more resistant to shocks than a moving average.
This commit is contained in:
@@ -43,6 +43,16 @@ const (
|
|||||||
// WebAPIResponseTimeout specifies the timeout value for receiving a
|
// WebAPIResponseTimeout specifies the timeout value for receiving a
|
||||||
// fee response from the api source.
|
// fee response from the api source.
|
||||||
WebAPIResponseTimeout = 10 * time.Second
|
WebAPIResponseTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
// economicalFeeMode is a mode that bitcoind uses to serve
|
||||||
|
// non-conservative fee estimates. These fee estimates are less
|
||||||
|
// resistant to shocks.
|
||||||
|
economicalFeeMode = "ECONOMICAL"
|
||||||
|
|
||||||
|
// filterCapConfTarget is the conf target that will be used to cap our
|
||||||
|
// minimum feerate if we used the median of our peers' feefilter
|
||||||
|
// values.
|
||||||
|
filterCapConfTarget = uint32(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -150,6 +160,11 @@ type BtcdEstimator struct {
|
|||||||
minFeeManager *minFeeManager
|
minFeeManager *minFeeManager
|
||||||
|
|
||||||
btcdConn *rpcclient.Client
|
btcdConn *rpcclient.Client
|
||||||
|
|
||||||
|
// filterManager uses our peer's feefilter values to determine a
|
||||||
|
// suitable feerate to use that will allow successful transaction
|
||||||
|
// propagation.
|
||||||
|
filterManager *filterManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBtcdEstimator creates a new BtcdEstimator given a fully populated
|
// NewBtcdEstimator creates a new BtcdEstimator given a fully populated
|
||||||
@@ -167,9 +182,14 @@ func NewBtcdEstimator(rpcConfig rpcclient.ConnConfig,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchCb := func() ([]SatPerKWeight, error) {
|
||||||
|
return fetchBtcdFilters(chainConn)
|
||||||
|
}
|
||||||
|
|
||||||
return &BtcdEstimator{
|
return &BtcdEstimator{
|
||||||
fallbackFeePerKW: fallBackFeeRate,
|
fallbackFeePerKW: fallBackFeeRate,
|
||||||
btcdConn: chainConn,
|
btcdConn: chainConn,
|
||||||
|
filterManager: newFilterManager(fetchCb),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +213,8 @@ func (b *BtcdEstimator) Start() error {
|
|||||||
}
|
}
|
||||||
b.minFeeManager = minRelayFeeManager
|
b.minFeeManager = minRelayFeeManager
|
||||||
|
|
||||||
|
b.filterManager.Start()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +241,8 @@ func (b *BtcdEstimator) fetchMinRelayFee() (SatPerKWeight, error) {
|
|||||||
//
|
//
|
||||||
// NOTE: This method is part of the Estimator interface.
|
// NOTE: This method is part of the Estimator interface.
|
||||||
func (b *BtcdEstimator) Stop() error {
|
func (b *BtcdEstimator) Stop() error {
|
||||||
|
b.filterManager.Stop()
|
||||||
|
|
||||||
b.btcdConn.Shutdown()
|
b.btcdConn.Shutdown()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -250,12 +274,47 @@ func (b *BtcdEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error
|
|||||||
//
|
//
|
||||||
// NOTE: This method is part of the Estimator interface.
|
// NOTE: This method is part of the Estimator interface.
|
||||||
func (b *BtcdEstimator) RelayFeePerKW() SatPerKWeight {
|
func (b *BtcdEstimator) RelayFeePerKW() SatPerKWeight {
|
||||||
return b.minFeeManager.fetchMinFee()
|
// Get a suitable minimum feerate to use. This may optionally use the
|
||||||
|
// median of our peers' feefilter values.
|
||||||
|
feeCapClosure := func() (SatPerKWeight, error) {
|
||||||
|
return b.fetchEstimateInner(filterCapConfTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chooseMinFee(
|
||||||
|
b.minFeeManager.fetchMinFee, b.filterManager.FetchMedianFilter,
|
||||||
|
feeCapClosure,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
|
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
|
||||||
// confTarget blocks. The estimate is returned in sat/kw.
|
// confTarget blocks. The estimate is returned in sat/kw.
|
||||||
func (b *BtcdEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
|
func (b *BtcdEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
|
||||||
|
satPerKw, err := b.fetchEstimateInner(confTarget)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll enforce our fee floor by choosing the higher of the
|
||||||
|
// minimum relay fee and the feerate returned by the filterManager.
|
||||||
|
absoluteMinFee := b.RelayFeePerKW()
|
||||||
|
|
||||||
|
if satPerKw < absoluteMinFee {
|
||||||
|
log.Debugf("Estimated fee rate of %v sat/kw is too low, "+
|
||||||
|
"using fee floor of %v sat/kw instead", satPerKw,
|
||||||
|
absoluteMinFee)
|
||||||
|
|
||||||
|
satPerKw = absoluteMinFee
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Returning %v sat/kw for conf target of %v",
|
||||||
|
int64(satPerKw), confTarget)
|
||||||
|
|
||||||
|
return satPerKw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BtcdEstimator) fetchEstimateInner(confTarget uint32) (SatPerKWeight,
|
||||||
|
error) {
|
||||||
|
|
||||||
// First, we'll fetch the estimate for our confirmation target.
|
// First, we'll fetch the estimate for our confirmation target.
|
||||||
btcPerKB, err := b.btcdConn.EstimateFee(int64(confTarget))
|
btcPerKB, err := b.btcdConn.EstimateFee(int64(confTarget))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -271,20 +330,7 @@ func (b *BtcdEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error)
|
|||||||
|
|
||||||
// Since we use fee rates in sat/kw internally, we'll convert the
|
// Since we use fee rates in sat/kw internally, we'll convert the
|
||||||
// estimated fee rate from its sat/kb representation to sat/kw.
|
// estimated fee rate from its sat/kb representation to sat/kw.
|
||||||
satPerKw := SatPerKVByte(satPerKB).FeePerKWeight()
|
return SatPerKVByte(satPerKB).FeePerKWeight(), nil
|
||||||
|
|
||||||
// Finally, we'll enforce our fee floor.
|
|
||||||
if satPerKw < b.minFeeManager.fetchMinFee() {
|
|
||||||
log.Debugf("Estimated fee rate of %v sat/kw is too low, "+
|
|
||||||
"using fee floor of %v sat/kw instead", satPerKw,
|
|
||||||
b.minFeeManager)
|
|
||||||
satPerKw = b.minFeeManager.fetchMinFee()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Returning %v sat/kw for conf target of %v",
|
|
||||||
int64(satPerKw), confTarget)
|
|
||||||
|
|
||||||
return satPerKw, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A compile-time assertion to ensure that BtcdEstimator implements the
|
// A compile-time assertion to ensure that BtcdEstimator implements the
|
||||||
@@ -314,6 +360,11 @@ type BitcoindEstimator struct {
|
|||||||
// TODO(ziggie): introduce an interface for the client to enhance
|
// TODO(ziggie): introduce an interface for the client to enhance
|
||||||
// testability of the estimator.
|
// testability of the estimator.
|
||||||
bitcoindConn *rpcclient.Client
|
bitcoindConn *rpcclient.Client
|
||||||
|
|
||||||
|
// filterManager uses our peer's feefilter values to determine a
|
||||||
|
// suitable feerate to use that will allow successful transaction
|
||||||
|
// propagation.
|
||||||
|
filterManager *filterManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBitcoindEstimator creates a new BitcoindEstimator given a fully populated
|
// NewBitcoindEstimator creates a new BitcoindEstimator given a fully populated
|
||||||
@@ -333,10 +384,15 @@ func NewBitcoindEstimator(rpcConfig rpcclient.ConnConfig, feeMode string,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchCb := func() ([]SatPerKWeight, error) {
|
||||||
|
return fetchBitcoindFilters(chainConn)
|
||||||
|
}
|
||||||
|
|
||||||
return &BitcoindEstimator{
|
return &BitcoindEstimator{
|
||||||
fallbackFeePerKW: fallBackFeeRate,
|
fallbackFeePerKW: fallBackFeeRate,
|
||||||
bitcoindConn: chainConn,
|
bitcoindConn: chainConn,
|
||||||
feeMode: feeMode,
|
feeMode: feeMode,
|
||||||
|
filterManager: newFilterManager(fetchCb),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,6 +413,8 @@ func (b *BitcoindEstimator) Start() error {
|
|||||||
}
|
}
|
||||||
b.minFeeManager = relayFeeManager
|
b.minFeeManager = relayFeeManager
|
||||||
|
|
||||||
|
b.filterManager.Start()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,6 +452,7 @@ func (b *BitcoindEstimator) fetchMinMempoolFee() (SatPerKWeight, error) {
|
|||||||
//
|
//
|
||||||
// NOTE: This method is part of the Estimator interface.
|
// NOTE: This method is part of the Estimator interface.
|
||||||
func (b *BitcoindEstimator) Stop() error {
|
func (b *BitcoindEstimator) Stop() error {
|
||||||
|
b.filterManager.Stop()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,7 +470,7 @@ func (b *BitcoindEstimator) EstimateFeePerKW(
|
|||||||
numBlocks = maxBlockTarget
|
numBlocks = maxBlockTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
feeEstimate, err := b.fetchEstimate(numBlocks)
|
feeEstimate, err := b.fetchEstimate(numBlocks, b.feeMode)
|
||||||
switch {
|
switch {
|
||||||
// If the estimator doesn't have enough data, or returns an error, then
|
// If the estimator doesn't have enough data, or returns an error, then
|
||||||
// to return a proper value, then we'll return the default fall back
|
// to return a proper value, then we'll return the default fall back
|
||||||
@@ -432,12 +491,51 @@ func (b *BitcoindEstimator) EstimateFeePerKW(
|
|||||||
//
|
//
|
||||||
// NOTE: This method is part of the Estimator interface.
|
// NOTE: This method is part of the Estimator interface.
|
||||||
func (b *BitcoindEstimator) RelayFeePerKW() SatPerKWeight {
|
func (b *BitcoindEstimator) RelayFeePerKW() SatPerKWeight {
|
||||||
return b.minFeeManager.fetchMinFee()
|
// Get a suitable minimum feerate to use. This may optionally use the
|
||||||
|
// median of our peers' feefilter values.
|
||||||
|
feeCapClosure := func() (SatPerKWeight, error) {
|
||||||
|
return b.fetchEstimateInner(
|
||||||
|
filterCapConfTarget, economicalFeeMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chooseMinFee(
|
||||||
|
b.minFeeManager.fetchMinFee, b.filterManager.FetchMedianFilter,
|
||||||
|
feeCapClosure,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
|
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
|
||||||
// confTarget blocks. The estimate is returned in sat/kw.
|
// confTarget blocks. The estimate is returned in sat/kw.
|
||||||
func (b *BitcoindEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
|
func (b *BitcoindEstimator) fetchEstimate(confTarget uint32, feeMode string) (
|
||||||
|
SatPerKWeight, error) {
|
||||||
|
|
||||||
|
satPerKw, err := b.fetchEstimateInner(confTarget, feeMode)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll enforce our fee floor by choosing the higher of the
|
||||||
|
// minimum relay fee and the feerate returned by the filterManager.
|
||||||
|
absoluteMinFee := b.RelayFeePerKW()
|
||||||
|
|
||||||
|
if satPerKw < absoluteMinFee {
|
||||||
|
log.Debugf("Estimated fee rate of %v sat/kw is too low, "+
|
||||||
|
"using fee floor of %v sat/kw instead", satPerKw,
|
||||||
|
absoluteMinFee)
|
||||||
|
|
||||||
|
satPerKw = absoluteMinFee
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Returning %v sat/kw for conf target of %v",
|
||||||
|
int64(satPerKw), confTarget)
|
||||||
|
|
||||||
|
return satPerKw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BitcoindEstimator) fetchEstimateInner(confTarget uint32,
|
||||||
|
feeMode string) (SatPerKWeight, error) {
|
||||||
|
|
||||||
// First, we'll send an "estimatesmartfee" command as a raw request,
|
// First, we'll send an "estimatesmartfee" command as a raw request,
|
||||||
// since it isn't supported by btcd but is available in bitcoind.
|
// since it isn't supported by btcd but is available in bitcoind.
|
||||||
target, err := json.Marshal(uint64(confTarget))
|
target, err := json.Marshal(uint64(confTarget))
|
||||||
@@ -446,7 +544,7 @@ func (b *BitcoindEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The mode must be either ECONOMICAL or CONSERVATIVE.
|
// The mode must be either ECONOMICAL or CONSERVATIVE.
|
||||||
mode, err := json.Marshal(b.feeMode)
|
mode, err := json.Marshal(feeMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -483,22 +581,50 @@ func (b *BitcoindEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, err
|
|||||||
|
|
||||||
// Since we use fee rates in sat/kw internally, we'll convert the
|
// Since we use fee rates in sat/kw internally, we'll convert the
|
||||||
// estimated fee rate from its sat/kb representation to sat/kw.
|
// estimated fee rate from its sat/kb representation to sat/kw.
|
||||||
satPerKw := SatPerKVByte(satPerKB).FeePerKWeight()
|
return SatPerKVByte(satPerKB).FeePerKWeight(), nil
|
||||||
|
|
||||||
// Finally, we'll enforce our fee floor.
|
|
||||||
minRelayFee := b.minFeeManager.fetchMinFee()
|
|
||||||
if satPerKw < minRelayFee {
|
|
||||||
log.Debugf("Estimated fee rate of %v is too low, "+
|
|
||||||
"using fee floor of %v instead", satPerKw,
|
|
||||||
minRelayFee)
|
|
||||||
|
|
||||||
satPerKw = minRelayFee
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Returning %v for conf target of %v",
|
// chooseMinFee takes the minimum relay fee and the median of our peers'
|
||||||
int64(satPerKw), confTarget)
|
// feefilter values and takes the higher of the two. It then compares the value
|
||||||
|
// against a maximum fee and caps it if the value is higher than the maximum
|
||||||
|
// fee. This function is only called if we have data for our peers' feefilter.
|
||||||
|
// The returned value will be used as the fee floor for calls to
|
||||||
|
// RelayFeePerKW.
|
||||||
|
func chooseMinFee(minRelayFeeFunc func() SatPerKWeight,
|
||||||
|
medianFilterFunc func() (SatPerKWeight, error),
|
||||||
|
feeCapFunc func() (SatPerKWeight, error)) SatPerKWeight {
|
||||||
|
|
||||||
return satPerKw, nil
|
minRelayFee := minRelayFeeFunc()
|
||||||
|
|
||||||
|
medianFilter, err := medianFilterFunc()
|
||||||
|
if err != nil {
|
||||||
|
// If we don't have feefilter data, we fallback to using our
|
||||||
|
// minimum relay fee.
|
||||||
|
return minRelayFee
|
||||||
|
}
|
||||||
|
|
||||||
|
feeCap, err := feeCapFunc()
|
||||||
|
if err != nil {
|
||||||
|
// If we encountered an error, don't use the medianFilter and
|
||||||
|
// instead fallback to using our minimum relay fee.
|
||||||
|
return minRelayFee
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the median feefilter is higher than our minimum relay fee, use it
|
||||||
|
// instead.
|
||||||
|
if medianFilter > minRelayFee {
|
||||||
|
// Only apply the cap if the median filter was used. This is
|
||||||
|
// to prevent an adversary from taking up the majority of our
|
||||||
|
// outbound peer slots and forcing us to use a high median
|
||||||
|
// filter value.
|
||||||
|
if medianFilter > feeCap {
|
||||||
|
return feeCap
|
||||||
|
}
|
||||||
|
|
||||||
|
return medianFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
return minRelayFee
|
||||||
}
|
}
|
||||||
|
|
||||||
// A compile-time assertion to ensure that BitcoindEstimator implements the
|
// A compile-time assertion to ensure that BitcoindEstimator implements the
|
||||||
|
261
lnwallet/chainfee/filtermanager.go
Normal file
261
lnwallet/chainfee/filtermanager.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package chainfee
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/rpcclient"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// fetchFilterInterval is the interval between successive fetches of
|
||||||
|
// our peers' feefilters.
|
||||||
|
fetchFilterInterval = time.Minute * 5
|
||||||
|
|
||||||
|
// minNumFilters is the minimum number of feefilters we need during a
|
||||||
|
// polling interval. If we have fewer than this, we won't consider the
|
||||||
|
// data.
|
||||||
|
minNumFilters = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// errNoData is an error that's returned if fetchMedianFilter is
|
||||||
|
// called and there is no data available.
|
||||||
|
errNoData = fmt.Errorf("no data available")
|
||||||
|
)
|
||||||
|
|
||||||
|
// filterManager is responsible for determining an acceptable minimum fee to
|
||||||
|
// use based on our peers' feefilter values.
|
||||||
|
type filterManager struct {
|
||||||
|
// median stores the median of our outbound peer's feefilter values.
|
||||||
|
median fn.Option[SatPerKWeight]
|
||||||
|
medianMtx sync.RWMutex
|
||||||
|
|
||||||
|
fetchFunc func() ([]SatPerKWeight, error)
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
quit chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFilterManager constructs a filterManager with a callback that fetches the
|
||||||
|
// set of peers' feefilters.
|
||||||
|
func newFilterManager(cb func() ([]SatPerKWeight, error)) *filterManager {
|
||||||
|
f := &filterManager{
|
||||||
|
quit: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
f.fetchFunc = cb
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the filterManager.
|
||||||
|
func (f *filterManager) Start() {
|
||||||
|
f.wg.Add(1)
|
||||||
|
go f.fetchPeerFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the filterManager.
|
||||||
|
func (f *filterManager) Stop() {
|
||||||
|
close(f.quit)
|
||||||
|
f.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchPeerFilters fetches our peers' feefilter values and calculates the
|
||||||
|
// median.
|
||||||
|
func (f *filterManager) fetchPeerFilters() {
|
||||||
|
defer f.wg.Done()
|
||||||
|
|
||||||
|
filterTicker := time.NewTicker(fetchFilterInterval)
|
||||||
|
defer filterTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-filterTicker.C:
|
||||||
|
filters, err := f.fetchFunc()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Encountered err while fetching "+
|
||||||
|
"fee filters: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.updateMedian(filters)
|
||||||
|
|
||||||
|
case <-f.quit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchMedianFilter fetches the median feefilter value.
|
||||||
|
func (f *filterManager) FetchMedianFilter() (SatPerKWeight, error) {
|
||||||
|
f.medianMtx.RLock()
|
||||||
|
defer f.medianMtx.RUnlock()
|
||||||
|
|
||||||
|
// If there is no median, return errNoData so the caller knows to
|
||||||
|
// ignore the output and continue.
|
||||||
|
return f.median.UnwrapOrErr(errNoData)
|
||||||
|
}
|
||||||
|
|
||||||
|
type bitcoindPeerInfoResp struct {
|
||||||
|
Inbound bool `json:"inbound"`
|
||||||
|
MinFeeFilter float64 `json:"minfeefilter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchBitcoindFilters(client *rpcclient.Client) ([]SatPerKWeight, error) {
|
||||||
|
resp, err := client.RawRequest("getpeerinfo", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var peerResps []bitcoindPeerInfoResp
|
||||||
|
err = json.Unmarshal(resp, &peerResps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We filter for outbound peers since it is harder for an attacker to
|
||||||
|
// be our outbound peer and therefore harder for them to manipulate us
|
||||||
|
// into broadcasting high-fee or low-fee transactions.
|
||||||
|
var outboundPeerFilters []SatPerKWeight
|
||||||
|
for _, peerResp := range peerResps {
|
||||||
|
if peerResp.Inbound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// The value that bitcoind returns for the "minfeefilter" field
|
||||||
|
// is in fractions of a bitcoin that represents the satoshis
|
||||||
|
// per KvB. We need to convert this fraction to whole satoshis
|
||||||
|
// by multiplying with COIN. Then we need to convert the
|
||||||
|
// sats/KvB to sats/KW.
|
||||||
|
//
|
||||||
|
// Convert the sats/KvB from fractions of a bitcoin to whole
|
||||||
|
// satoshis.
|
||||||
|
filterKVByte := SatPerKVByte(
|
||||||
|
peerResp.MinFeeFilter * btcutil.SatoshiPerBitcoin,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !isWithinBounds(filterKVByte) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert KvB to KW and add it to outboundPeerFilters.
|
||||||
|
outboundPeerFilters = append(
|
||||||
|
outboundPeerFilters, filterKVByte.FeePerKWeight(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have enough data to use. We don't return an error as
|
||||||
|
// that would stop the querying goroutine.
|
||||||
|
if len(outboundPeerFilters) < minNumFilters {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundPeerFilters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchBtcdFilters(client *rpcclient.Client) ([]SatPerKWeight, error) {
|
||||||
|
resp, err := client.GetPeerInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var outboundPeerFilters []SatPerKWeight
|
||||||
|
for _, peerResp := range resp {
|
||||||
|
// We filter for outbound peers since it is harder for an
|
||||||
|
// attacker to be our outbound peer and therefore harder for
|
||||||
|
// them to manipulate us into broadcasting high-fee or low-fee
|
||||||
|
// transactions.
|
||||||
|
if peerResp.Inbound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// The feefilter is already in units of sat/KvB.
|
||||||
|
filter := SatPerKVByte(peerResp.FeeFilter)
|
||||||
|
|
||||||
|
if !isWithinBounds(filter) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundPeerFilters = append(
|
||||||
|
outboundPeerFilters, filter.FeePerKWeight(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have enough data to use. We don't return an error as
|
||||||
|
// that would stop the querying goroutine.
|
||||||
|
if len(outboundPeerFilters) < minNumFilters {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundPeerFilters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateMedian takes a slice of feefilter values, computes the median, and
|
||||||
|
// updates our stored median value.
|
||||||
|
func (f *filterManager) updateMedian(feeFilters []SatPerKWeight) {
|
||||||
|
// If there are no elements, don't update.
|
||||||
|
numElements := len(feeFilters)
|
||||||
|
if numElements == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.medianMtx.Lock()
|
||||||
|
defer f.medianMtx.Unlock()
|
||||||
|
|
||||||
|
// Log the new median.
|
||||||
|
median := med(feeFilters)
|
||||||
|
f.median = fn.Some(median)
|
||||||
|
log.Debugf("filterManager updated moving median to: %v",
|
||||||
|
median.FeePerKVByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
// isWithinBounds returns false if the filter is unusable and true if it is.
|
||||||
|
func isWithinBounds(filter SatPerKVByte) bool {
|
||||||
|
// Ignore values of 0 and MaxSatoshi. A value of 0 likely means that
|
||||||
|
// the peer hasn't sent over a feefilter and a value of MaxSatoshi
|
||||||
|
// means the peer is using bitcoind and is in IBD.
|
||||||
|
switch filter {
|
||||||
|
case 0:
|
||||||
|
return false
|
||||||
|
|
||||||
|
case btcutil.MaxSatoshi:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// med calculates the median of a slice.
|
||||||
|
// NOTE: Passing in an empty slice will panic!
|
||||||
|
func med(f []SatPerKWeight) SatPerKWeight {
|
||||||
|
// Copy the original slice so that sorting doesn't modify the original.
|
||||||
|
fCopy := make([]SatPerKWeight, len(f))
|
||||||
|
copy(fCopy, f)
|
||||||
|
|
||||||
|
sort.Slice(fCopy, func(i, j int) bool {
|
||||||
|
return fCopy[i] < fCopy[j]
|
||||||
|
})
|
||||||
|
|
||||||
|
var median SatPerKWeight
|
||||||
|
|
||||||
|
numElements := len(fCopy)
|
||||||
|
switch numElements % 2 {
|
||||||
|
case 0:
|
||||||
|
// There's an even number of elements, so we need to average.
|
||||||
|
middle := numElements / 2
|
||||||
|
upper := fCopy[middle]
|
||||||
|
lower := fCopy[middle-1]
|
||||||
|
median = (upper + lower) / 2
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
median = fCopy[numElements/2]
|
||||||
|
}
|
||||||
|
|
||||||
|
return median
|
||||||
|
}
|
52
lnwallet/chainfee/filtermanager_test.go
Normal file
52
lnwallet/chainfee/filtermanager_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package chainfee
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFeeFilterMedian(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fetchedFilters []SatPerKWeight
|
||||||
|
expectedMedian SatPerKWeight
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no filter data",
|
||||||
|
fetchedFilters: nil,
|
||||||
|
expectedErr: errNoData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "even number of filters",
|
||||||
|
fetchedFilters: []SatPerKWeight{
|
||||||
|
15000, 18000, 19000, 25000,
|
||||||
|
},
|
||||||
|
expectedMedian: SatPerKWeight(18500),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "odd number of filters",
|
||||||
|
fetchedFilters: []SatPerKWeight{
|
||||||
|
15000, 18000, 25000,
|
||||||
|
},
|
||||||
|
expectedMedian: SatPerKWeight(18000),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
cb := func() ([]SatPerKWeight, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
fm := newFilterManager(cb)
|
||||||
|
|
||||||
|
fm.updateMedian(test.fetchedFilters)
|
||||||
|
|
||||||
|
median, err := fm.FetchMedianFilter()
|
||||||
|
require.Equal(t, test.expectedErr, err)
|
||||||
|
require.Equal(t, test.expectedMedian, median)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user