mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-08 14:57:38 +02:00
chainfee: create new chainfee package extracting fees from lnwallet
In this commit, we create a new chainfee package, that houses all fee related functionality used within the codebase. The creation of this new package furthers our long-term goal of extracting functionality from the bloated `lnwallet` package into new distinct packages. Additionally, this new packages resolves a class of import cycle that could arise if a new package that was imported by something in `lnwallet` wanted to use the existing fee related functions in the prior `lnwallet` package.
This commit is contained in:
719
lnwallet/chainfee/estimator.go
Normal file
719
lnwallet/chainfee/estimator.go
Normal file
@@ -0,0 +1,719 @@
|
||||
package chainfee
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
prand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/rpcclient"
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxBlockTarget is the highest number of blocks confirmations that
|
||||
// a WebAPIEstimator will cache fees for. This number is chosen
|
||||
// because it's the highest number of confs bitcoind will return a fee
|
||||
// estimate for.
|
||||
maxBlockTarget uint32 = 1009
|
||||
|
||||
// minBlockTarget is the lowest number of blocks confirmations that
|
||||
// a WebAPIEstimator will cache fees for. Requesting an estimate for
|
||||
// less than this will result in an error.
|
||||
minBlockTarget uint32 = 2
|
||||
|
||||
// minFeeUpdateTimeout represents the minimum interval in which a
|
||||
// WebAPIEstimator will request fresh fees from its API.
|
||||
minFeeUpdateTimeout = 5 * time.Minute
|
||||
|
||||
// maxFeeUpdateTimeout represents the maximum interval in which a
|
||||
// WebAPIEstimator will request fresh fees from its API.
|
||||
maxFeeUpdateTimeout = 20 * time.Minute
|
||||
)
|
||||
|
||||
// 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).
|
||||
type Estimator interface {
|
||||
// EstimateFeePerKW takes in a target for the number of blocks until an
|
||||
// initial confirmation and returns the estimated fee expressed in
|
||||
// sat/kw.
|
||||
EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error)
|
||||
|
||||
// Start signals the Estimator to start any processes or goroutines
|
||||
// it needs to perform its duty.
|
||||
Start() error
|
||||
|
||||
// Stop stops any spawned goroutines and cleans up the resources used
|
||||
// by the fee estimator.
|
||||
Stop() error
|
||||
|
||||
// RelayFeePerKW returns the minimum fee rate required for transactions
|
||||
// to be relayed. This is also the basis for calculation of the dust
|
||||
// limit.
|
||||
RelayFeePerKW() SatPerKWeight
|
||||
}
|
||||
|
||||
// StaticEstimator will return a static value for all fee calculation requests.
|
||||
// It is designed to be replaced by a proper fee calculation implementation.
|
||||
// The fees are not accessible directly, because changing them would not be
|
||||
// thread safe.
|
||||
type StaticEstimator struct {
|
||||
// feePerKW is the static fee rate in satoshis-per-vbyte that will be
|
||||
// returned by this fee estimator.
|
||||
feePerKW SatPerKWeight
|
||||
|
||||
// relayFee is the minimum fee rate required for transactions to be
|
||||
// relayed.
|
||||
relayFee SatPerKWeight
|
||||
}
|
||||
|
||||
// NewStaticEstimator returns a new static fee estimator instance.
|
||||
func NewStaticEstimator(feePerKW, relayFee SatPerKWeight) *StaticEstimator {
|
||||
|
||||
return &StaticEstimator{
|
||||
feePerKW: feePerKW,
|
||||
relayFee: relayFee,
|
||||
}
|
||||
}
|
||||
|
||||
// EstimateFeePerKW will return a static value for fee calculations.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (e StaticEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) {
|
||||
return e.feePerKW, nil
|
||||
}
|
||||
|
||||
// RelayFeePerKW returns the minimum fee rate required for transactions to be
|
||||
// relayed.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (e StaticEstimator) RelayFeePerKW() SatPerKWeight {
|
||||
return e.relayFee
|
||||
}
|
||||
|
||||
// Start signals the Estimator to start any processes or goroutines
|
||||
// it needs to perform its duty.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (e StaticEstimator) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops any spawned goroutines and cleans up the resources used
|
||||
// by the fee estimator.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (e StaticEstimator) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// A compile-time assertion to ensure that StaticFeeEstimator implements the
|
||||
// Estimator interface.
|
||||
var _ Estimator = (*StaticEstimator)(nil)
|
||||
|
||||
// BtcdEstimator is an implementation of the Estimator interface backed
|
||||
// by the RPC interface of an active btcd node. This implementation will proxy
|
||||
// any fee estimation requests to btcd's RPC interface.
|
||||
type BtcdEstimator struct {
|
||||
// fallbackFeePerKW is the fall back fee rate in sat/kw that is returned
|
||||
// if the fee estimator does not yet have enough data to actually
|
||||
// produce fee estimates.
|
||||
fallbackFeePerKW SatPerKWeight
|
||||
|
||||
// minFeePerKW is 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 the transaction to propagate
|
||||
// through the network.
|
||||
minFeePerKW SatPerKWeight
|
||||
|
||||
btcdConn *rpcclient.Client
|
||||
}
|
||||
|
||||
// NewBtcdEstimator creates a new BtcdEstimator given a fully populated
|
||||
// rpc config that is able to successfully connect and authenticate with the
|
||||
// btcd node, and also a fall back fee rate. The fallback fee rate is used in
|
||||
// the occasion that the estimator has insufficient data, or returns zero for a
|
||||
// fee estimate.
|
||||
func NewBtcdEstimator(rpcConfig rpcclient.ConnConfig,
|
||||
fallBackFeeRate SatPerKWeight) (*BtcdEstimator, error) {
|
||||
|
||||
rpcConfig.DisableConnectOnNew = true
|
||||
rpcConfig.DisableAutoReconnect = false
|
||||
chainConn, err := rpcclient.New(&rpcConfig, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BtcdEstimator{
|
||||
fallbackFeePerKW: fallBackFeeRate,
|
||||
btcdConn: chainConn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start signals the Estimator to start any processes or goroutines
|
||||
// it needs to perform its duty.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (b *BtcdEstimator) Start() error {
|
||||
if err := b.btcdConn.Connect(20); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Once the connection to the backend node has been established, we'll
|
||||
// query it for its minimum relay fee.
|
||||
info, err := b.btcdConn.GetInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relayFee, err := btcutil.NewAmount(info.RelayFee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The fee rate is expressed in sat/kb, so we'll manually convert it to
|
||||
// our desired sat/kw rate.
|
||||
minRelayFeePerKw := SatPerKVByte(relayFee).FeePerKWeight()
|
||||
|
||||
// By default, we'll use the backend node's minimum relay fee as the
|
||||
// minimum fee rate we'll propose for transacations. However, if this
|
||||
// happens to be lower than our fee floor, we'll enforce that instead.
|
||||
b.minFeePerKW = minRelayFeePerKw
|
||||
if b.minFeePerKW < FeePerKwFloor {
|
||||
b.minFeePerKW = FeePerKwFloor
|
||||
}
|
||||
|
||||
log.Debugf("Using minimum fee rate of %v sat/kw",
|
||||
int64(b.minFeePerKW))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops any spawned goroutines and cleans up the resources used
|
||||
// by the fee estimator.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (b *BtcdEstimator) Stop() error {
|
||||
b.btcdConn.Shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EstimateFeePerKW takes in a target for the number of blocks until an initial
|
||||
// confirmation and returns the estimated fee expressed in sat/kw.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (b *BtcdEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) {
|
||||
feeEstimate, err := b.fetchEstimate(numBlocks)
|
||||
switch {
|
||||
// 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
|
||||
// fee rate.
|
||||
case err != nil:
|
||||
log.Errorf("unable to query estimator: %v", err)
|
||||
fallthrough
|
||||
|
||||
case feeEstimate == 0:
|
||||
return b.fallbackFeePerKW, nil
|
||||
}
|
||||
|
||||
return feeEstimate, nil
|
||||
}
|
||||
|
||||
// RelayFeePerKW returns the minimum fee rate required for transactions to be
|
||||
// relayed.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (b *BtcdEstimator) RelayFeePerKW() SatPerKWeight {
|
||||
return b.minFeePerKW
|
||||
}
|
||||
|
||||
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
|
||||
// confTarget blocks. The estimate is returned in sat/kw.
|
||||
func (b *BtcdEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
|
||||
// First, we'll fetch the estimate for our confirmation target.
|
||||
btcPerKB, err := b.btcdConn.EstimateFee(int64(confTarget))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Next, we'll convert the returned value to satoshis, as it's
|
||||
// currently returned in BTC.
|
||||
satPerKB, err := btcutil.NewAmount(btcPerKB)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Since we use fee rates in sat/kw internally, we'll convert the
|
||||
// estimated fee rate from its sat/kb representation to sat/kw.
|
||||
satPerKw := SatPerKVByte(satPerKB).FeePerKWeight()
|
||||
|
||||
// Finally, we'll enforce our fee floor.
|
||||
if satPerKw < b.minFeePerKW {
|
||||
log.Debugf("Estimated fee rate of %v sat/kw is too low, "+
|
||||
"using fee floor of %v sat/kw instead", satPerKw,
|
||||
b.minFeePerKW)
|
||||
satPerKw = b.minFeePerKW
|
||||
}
|
||||
|
||||
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
|
||||
// Estimator interface.
|
||||
var _ Estimator = (*BtcdEstimator)(nil)
|
||||
|
||||
// BitcoindEstimator is an implementation of the Estimator interface backed by
|
||||
// the RPC interface of an active bitcoind node. This implementation will proxy
|
||||
// any fee estimation requests to bitcoind's RPC interface.
|
||||
type BitcoindEstimator struct {
|
||||
// fallbackFeePerKW is the fallback fee rate in sat/kw that is returned
|
||||
// if the fee estimator does not yet have enough data to actually
|
||||
// produce fee estimates.
|
||||
fallbackFeePerKW SatPerKWeight
|
||||
|
||||
// minFeePerKW is 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 the transaction to propagate
|
||||
// through the network.
|
||||
minFeePerKW SatPerKWeight
|
||||
|
||||
bitcoindConn *rpcclient.Client
|
||||
}
|
||||
|
||||
// NewBitcoindEstimator creates a new BitcoindEstimator given a fully populated
|
||||
// rpc config that is able to successfully connect and authenticate with the
|
||||
// bitcoind node, and also a fall back fee rate. The fallback fee rate is used
|
||||
// in the occasion that the estimator has insufficient data, or returns zero
|
||||
// for a fee estimate.
|
||||
func NewBitcoindEstimator(rpcConfig rpcclient.ConnConfig,
|
||||
fallBackFeeRate SatPerKWeight) (*BitcoindEstimator, error) {
|
||||
|
||||
rpcConfig.DisableConnectOnNew = true
|
||||
rpcConfig.DisableAutoReconnect = false
|
||||
rpcConfig.DisableTLS = true
|
||||
rpcConfig.HTTPPostMode = true
|
||||
chainConn, err := rpcclient.New(&rpcConfig, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BitcoindEstimator{
|
||||
fallbackFeePerKW: fallBackFeeRate,
|
||||
bitcoindConn: chainConn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start signals the Estimator to start any processes or goroutines
|
||||
// it needs to perform its duty.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (b *BitcoindEstimator) Start() error {
|
||||
// Once the connection to the backend node has been established, we'll
|
||||
// query it for its minimum relay fee. Since the `getinfo` RPC has been
|
||||
// deprecated for `bitcoind`, we'll need to send a `getnetworkinfo`
|
||||
// command as a raw request.
|
||||
resp, err := b.bitcoindConn.RawRequest("getnetworkinfo", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the response to retrieve the relay fee in sat/KB.
|
||||
info := struct {
|
||||
RelayFee float64 `json:"relayfee"`
|
||||
}{}
|
||||
if err := json.Unmarshal(resp, &info); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relayFee, err := btcutil.NewAmount(info.RelayFee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The fee rate is expressed in sat/kb, so we'll manually convert it to
|
||||
// our desired sat/kw rate.
|
||||
minRelayFeePerKw := SatPerKVByte(relayFee).FeePerKWeight()
|
||||
|
||||
// By default, we'll use the backend node's minimum relay fee as the
|
||||
// minimum fee rate we'll propose for transacations. However, if this
|
||||
// happens to be lower than our fee floor, we'll enforce that instead.
|
||||
b.minFeePerKW = minRelayFeePerKw
|
||||
if b.minFeePerKW < FeePerKwFloor {
|
||||
b.minFeePerKW = FeePerKwFloor
|
||||
}
|
||||
|
||||
log.Debugf("Using minimum fee rate of %v sat/kw",
|
||||
int64(b.minFeePerKW))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops any spawned goroutines and cleans up the resources used
|
||||
// by the fee estimator.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (b *BitcoindEstimator) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EstimateFeePerKW takes in a target for the number of blocks until an initial
|
||||
// confirmation and returns the estimated fee expressed in sat/kw.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (b *BitcoindEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) {
|
||||
feeEstimate, err := b.fetchEstimate(numBlocks)
|
||||
switch {
|
||||
// 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
|
||||
// fee rate.
|
||||
case err != nil:
|
||||
log.Errorf("unable to query estimator: %v", err)
|
||||
fallthrough
|
||||
|
||||
case feeEstimate == 0:
|
||||
return b.fallbackFeePerKW, nil
|
||||
}
|
||||
|
||||
return feeEstimate, nil
|
||||
}
|
||||
|
||||
// RelayFeePerKW returns the minimum fee rate required for transactions to be
|
||||
// relayed.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (b *BitcoindEstimator) RelayFeePerKW() SatPerKWeight {
|
||||
return b.minFeePerKW
|
||||
}
|
||||
|
||||
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
|
||||
// confTarget blocks. The estimate is returned in sat/kw.
|
||||
func (b *BitcoindEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
|
||||
// First, we'll send an "estimatesmartfee" command as a raw request,
|
||||
// since it isn't supported by btcd but is available in bitcoind.
|
||||
target, err := json.Marshal(uint64(confTarget))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// TODO: Allow selection of economical/conservative modifiers.
|
||||
resp, err := b.bitcoindConn.RawRequest(
|
||||
"estimatesmartfee", []json.RawMessage{target},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Next, we'll parse the response to get the BTC per KB.
|
||||
feeEstimate := struct {
|
||||
FeeRate float64 `json:"feerate"`
|
||||
}{}
|
||||
err = json.Unmarshal(resp, &feeEstimate)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Next, we'll convert the returned value to satoshis, as it's currently
|
||||
// returned in BTC.
|
||||
satPerKB, err := btcutil.NewAmount(feeEstimate.FeeRate)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Since we use fee rates in sat/kw internally, we'll convert the
|
||||
// estimated fee rate from its sat/kb representation to sat/kw.
|
||||
satPerKw := SatPerKVByte(satPerKB).FeePerKWeight()
|
||||
|
||||
// Finally, we'll enforce our fee floor.
|
||||
if satPerKw < b.minFeePerKW {
|
||||
log.Debugf("Estimated fee rate of %v sat/kw is too low, "+
|
||||
"using fee floor of %v sat/kw instead", satPerKw,
|
||||
b.minFeePerKW)
|
||||
|
||||
satPerKw = b.minFeePerKW
|
||||
}
|
||||
|
||||
log.Debugf("Returning %v sat/kw for conf target of %v",
|
||||
int64(satPerKw), confTarget)
|
||||
|
||||
return satPerKw, nil
|
||||
}
|
||||
|
||||
// A compile-time assertion to ensure that BitcoindEstimator implements the
|
||||
// Estimator interface.
|
||||
var _ Estimator = (*BitcoindEstimator)(nil)
|
||||
|
||||
// WebAPIFeeSource is an interface allows the WebAPIEstimator to query an
|
||||
// arbitrary HTTP-based fee estimator. Each new set/network will gain an
|
||||
// implementation of this interface in order to allow the WebAPIEstimator to
|
||||
// be fully generic in its logic.
|
||||
type WebAPIFeeSource interface {
|
||||
// GenQueryURL generates the full query URL. The value returned by this
|
||||
// method should be able to be used directly as a path for an HTTP GET
|
||||
// request.
|
||||
GenQueryURL() string
|
||||
|
||||
// ParseResponse attempts to parse the body of the response generated
|
||||
// by the above query URL. Typically this will be JSON, but the
|
||||
// specifics are left to the WebAPIFeeSource implementation.
|
||||
ParseResponse(r io.Reader) (map[uint32]uint32, error)
|
||||
}
|
||||
|
||||
// SparseConfFeeSource is an implementation of the WebAPIFeeSource that utilizes
|
||||
// a user-specified fee estimation API for Bitcoin. It expects the response
|
||||
// to be in the JSON format: `fee_by_block_target: { ... }` where the value maps
|
||||
// block targets to fee estimates (in sat per kilovbyte).
|
||||
type SparseConfFeeSource struct {
|
||||
// URL is the fee estimation API specified by the user.
|
||||
URL string
|
||||
}
|
||||
|
||||
// GenQueryURL generates the full query URL. The value returned by this
|
||||
// method should be able to be used directly as a path for an HTTP GET
|
||||
// request.
|
||||
//
|
||||
// NOTE: Part of the WebAPIFeeSource interface.
|
||||
func (s SparseConfFeeSource) GenQueryURL() string {
|
||||
return s.URL
|
||||
}
|
||||
|
||||
// ParseResponse attempts to parse the body of the response generated by the
|
||||
// above query URL. Typically this will be JSON, but the specifics are left to
|
||||
// the WebAPIFeeSource implementation.
|
||||
//
|
||||
// NOTE: Part of the WebAPIFeeSource interface.
|
||||
func (s SparseConfFeeSource) ParseResponse(r io.Reader) (map[uint32]uint32, error) {
|
||||
type jsonResp struct {
|
||||
FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"`
|
||||
}
|
||||
|
||||
resp := jsonResp{
|
||||
FeeByBlockTarget: make(map[uint32]uint32),
|
||||
}
|
||||
jsonReader := json.NewDecoder(r)
|
||||
if err := jsonReader.Decode(&resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.FeeByBlockTarget, nil
|
||||
}
|
||||
|
||||
// A compile-time assertion to ensure that SparseConfFeeSource implements the
|
||||
// WebAPIFeeSource interface.
|
||||
var _ WebAPIFeeSource = (*SparseConfFeeSource)(nil)
|
||||
|
||||
// WebAPIEstimator is an implementation of the Estimator interface that
|
||||
// queries an HTTP-based fee estimation from an existing web API.
|
||||
type WebAPIEstimator struct {
|
||||
started sync.Once
|
||||
stopped sync.Once
|
||||
|
||||
// apiSource is the backing web API source we'll use for our queries.
|
||||
apiSource WebAPIFeeSource
|
||||
|
||||
// updateFeeTicker is the ticker responsible for updating the Estimator's
|
||||
// fee estimates every time it fires.
|
||||
updateFeeTicker *time.Ticker
|
||||
|
||||
// feeByBlockTarget is our cache for fees pulled from the API. When a
|
||||
// fee estimate request comes in, we pull the estimate from this array
|
||||
// rather than re-querying the API, to prevent an inadvertent DoS attack.
|
||||
feesMtx sync.Mutex
|
||||
feeByBlockTarget map[uint32]uint32
|
||||
|
||||
// defaultFeePerKw is a fallback value that we'll use if we're unable
|
||||
// to query the API for any reason.
|
||||
defaultFeePerKw 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, defaultFee SatPerKWeight) *WebAPIEstimator {
|
||||
|
||||
return &WebAPIEstimator{
|
||||
apiSource: api,
|
||||
feeByBlockTarget: make(map[uint32]uint32),
|
||||
defaultFeePerKw: defaultFee,
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// EstimateFeePerKW takes in a target for the number of blocks until an initial
|
||||
// confirmation and returns the estimated fee expressed in sat/kw.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (w *WebAPIEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) {
|
||||
if numBlocks > maxBlockTarget {
|
||||
numBlocks = maxBlockTarget
|
||||
} else if numBlocks < minBlockTarget {
|
||||
return 0, fmt.Errorf("conf target of %v is too low, minimum "+
|
||||
"accepted is %v", numBlocks, minBlockTarget)
|
||||
}
|
||||
|
||||
feePerKb, err := w.getCachedFee(numBlocks)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
log.Debugf("Web API returning %v sat/kw for conf target of %v",
|
||||
int64(satPerKw), numBlocks)
|
||||
|
||||
return satPerKw, nil
|
||||
}
|
||||
|
||||
// Start signals the Estimator to start any processes or goroutines it needs
|
||||
// to perform its duty.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (w *WebAPIEstimator) Start() error {
|
||||
var err error
|
||||
w.started.Do(func() {
|
||||
log.Infof("Starting web API fee estimator")
|
||||
|
||||
w.updateFeeTicker = time.NewTicker(w.randomFeeUpdateTimeout())
|
||||
w.updateFeeEstimates()
|
||||
|
||||
w.wg.Add(1)
|
||||
go w.feeUpdateManager()
|
||||
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Stop stops any spawned goroutines and cleans up the resources used by the
|
||||
// fee estimator.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (w *WebAPIEstimator) Stop() error {
|
||||
w.stopped.Do(func() {
|
||||
log.Infof("Stopping web API fee estimator")
|
||||
|
||||
w.updateFeeTicker.Stop()
|
||||
|
||||
close(w.quit)
|
||||
w.wg.Wait()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// RelayFeePerKW returns the minimum fee rate required for transactions to be
|
||||
// relayed.
|
||||
//
|
||||
// NOTE: This method is part of the Estimator interface.
|
||||
func (w *WebAPIEstimator) RelayFeePerKW() SatPerKWeight {
|
||||
return FeePerKwFloor
|
||||
}
|
||||
|
||||
// randomFeeUpdateTimeout returns a random timeout between minFeeUpdateTimeout
|
||||
// and maxFeeUpdateTimeout that will be used to determine how often the Estimator
|
||||
// should retrieve fresh fees from its API.
|
||||
func (w *WebAPIEstimator) randomFeeUpdateTimeout() time.Duration {
|
||||
lower := int64(minFeeUpdateTimeout)
|
||||
upper := int64(maxFeeUpdateTimeout)
|
||||
return time.Duration(prand.Int63n(upper-lower) + lower)
|
||||
}
|
||||
|
||||
// getCachedFee takes in a target for the number of blocks until an initial
|
||||
// confirmation and returns an estimated fee (if one was returned by the API). If
|
||||
// the fee was not previously cached, we cache it here.
|
||||
func (w *WebAPIEstimator) getCachedFee(numBlocks uint32) (uint32, error) {
|
||||
w.feesMtx.Lock()
|
||||
defer w.feesMtx.Unlock()
|
||||
|
||||
// Search our cached fees for the desired block target. If the target is
|
||||
// not cached, then attempt to extrapolate it from the next lowest target
|
||||
// that *is* cached. If we successfully extrapolate, then cache the
|
||||
// target's fee.
|
||||
for target := numBlocks; target >= minBlockTarget; target-- {
|
||||
fee, ok := w.feeByBlockTarget[target]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
_, ok = w.feeByBlockTarget[numBlocks]
|
||||
if !ok {
|
||||
w.feeByBlockTarget[numBlocks] = fee
|
||||
}
|
||||
return fee, nil
|
||||
}
|
||||
return 0, fmt.Errorf("web API does not include a fee estimation for "+
|
||||
"block target of %v", numBlocks)
|
||||
}
|
||||
|
||||
// updateFeeEstimates re-queries the API for fresh fees and caches them.
|
||||
func (w *WebAPIEstimator) updateFeeEstimates() {
|
||||
// Rather than use the default http.Client, we'll make a custom one
|
||||
// which will allow us to control how long we'll wait to read the
|
||||
// response from the service. This way, if the service is down or
|
||||
// overloaded, we can exit early and use our default fee.
|
||||
netTransport := &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
netClient := &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
// With the client created, we'll query the API source to fetch the URL
|
||||
// that we should use to query for the fee estimation.
|
||||
targetURL := w.apiSource.GenQueryURL()
|
||||
resp, err := netClient.Get(targetURL)
|
||||
if err != nil {
|
||||
log.Errorf("unable to query web api for fee response: %v",
|
||||
err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Once we've obtained the response, we'll instruct the WebAPIFeeSource
|
||||
// to parse out the body to obtain our final result.
|
||||
feesByBlockTarget, err := w.apiSource.ParseResponse(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("unable to query web api for fee response: %v",
|
||||
err)
|
||||
return
|
||||
}
|
||||
|
||||
w.feesMtx.Lock()
|
||||
w.feeByBlockTarget = feesByBlockTarget
|
||||
w.feesMtx.Unlock()
|
||||
}
|
||||
|
||||
// feeUpdateManager updates the fee estimates whenever a new block comes in.
|
||||
func (w *WebAPIEstimator) feeUpdateManager() {
|
||||
defer w.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.updateFeeTicker.C:
|
||||
w.updateFeeEstimates()
|
||||
case <-w.quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A compile-time assertion to ensure that WebAPIEstimator implements the
|
||||
// Estimator interface.
|
||||
var _ Estimator = (*WebAPIEstimator)(nil)
|
235
lnwallet/chainfee/estimator_test.go
Normal file
235
lnwallet/chainfee/estimator_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package chainfee
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
||||
type mockSparseConfFeeSource struct {
|
||||
url string
|
||||
fees map[uint32]uint32
|
||||
}
|
||||
|
||||
func (e mockSparseConfFeeSource) GenQueryURL() string {
|
||||
return e.url
|
||||
}
|
||||
|
||||
func (e mockSparseConfFeeSource) ParseResponse(r io.Reader) (map[uint32]uint32, error) {
|
||||
return e.fees, nil
|
||||
}
|
||||
|
||||
// TestFeeRateTypes checks that converting fee rates between the
|
||||
// different types that represent fee rates and calculating fees
|
||||
// work as expected.
|
||||
func TestFeeRateTypes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// We'll be calculating the transaction fees for the given measurements
|
||||
// using different fee rates and expecting them to match.
|
||||
const vsize = 300
|
||||
const weight = vsize * 4
|
||||
|
||||
// Test the conversion from sat/kw to sat/kb.
|
||||
for feePerKw := SatPerKWeight(250); feePerKw < 10000; feePerKw += 50 {
|
||||
feePerKB := feePerKw.FeePerKVByte()
|
||||
if feePerKB != SatPerKVByte(feePerKw*4) {
|
||||
t.Fatalf("expected %d sat/kb, got %d sat/kb when "+
|
||||
"converting from %d sat/kw", feePerKw*4,
|
||||
feePerKB, feePerKw)
|
||||
}
|
||||
|
||||
// The resulting transaction fee should be the same when using
|
||||
// both rates.
|
||||
expectedFee := btcutil.Amount(feePerKw * weight / 1000)
|
||||
fee1 := feePerKw.FeeForWeight(weight)
|
||||
if fee1 != expectedFee {
|
||||
t.Fatalf("expected fee of %d sats, got %d sats",
|
||||
expectedFee, fee1)
|
||||
}
|
||||
fee2 := feePerKB.FeeForVSize(vsize)
|
||||
if fee2 != expectedFee {
|
||||
t.Fatalf("expected fee of %d sats, got %d sats",
|
||||
expectedFee, fee2)
|
||||
}
|
||||
}
|
||||
|
||||
// Test the conversion from sat/kb to sat/kw.
|
||||
for feePerKB := SatPerKVByte(1000); feePerKB < 40000; feePerKB += 1000 {
|
||||
feePerKw := feePerKB.FeePerKWeight()
|
||||
if feePerKw != SatPerKWeight(feePerKB/4) {
|
||||
t.Fatalf("expected %d sat/kw, got %d sat/kw when "+
|
||||
"converting from %d sat/kb", feePerKB/4,
|
||||
feePerKw, feePerKB)
|
||||
}
|
||||
|
||||
// The resulting transaction fee should be the same when using
|
||||
// both rates.
|
||||
expectedFee := btcutil.Amount(feePerKB * vsize / 1000)
|
||||
fee1 := feePerKB.FeeForVSize(vsize)
|
||||
if fee1 != expectedFee {
|
||||
t.Fatalf("expected fee of %d sats, got %d sats",
|
||||
expectedFee, fee1)
|
||||
}
|
||||
fee2 := feePerKw.FeeForWeight(weight)
|
||||
if fee2 != expectedFee {
|
||||
t.Fatalf("expected fee of %d sats, got %d sats",
|
||||
expectedFee, fee2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStaticFeeEstimator checks that the StaticFeeEstimator returns the
|
||||
// expected fee rate.
|
||||
func TestStaticFeeEstimator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const feePerKw = FeePerKwFloor
|
||||
|
||||
feeEstimator := NewStaticEstimator(feePerKw, 0)
|
||||
if err := feeEstimator.Start(); err != nil {
|
||||
t.Fatalf("unable to start fee estimator: %v", err)
|
||||
}
|
||||
defer feeEstimator.Stop()
|
||||
|
||||
feeRate, err := feeEstimator.EstimateFeePerKW(6)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get fee rate: %v", err)
|
||||
}
|
||||
|
||||
if feeRate != feePerKw {
|
||||
t.Fatalf("expected fee rate %v, got %v", feePerKw, feeRate)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSparseConfFeeSource checks that SparseConfFeeSource generates URLs and
|
||||
// parses API responses as expected.
|
||||
func TestSparseConfFeeSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test that GenQueryURL returns the URL as is.
|
||||
url := "test"
|
||||
feeSource := SparseConfFeeSource{URL: url}
|
||||
queryURL := feeSource.GenQueryURL()
|
||||
if queryURL != url {
|
||||
t.Fatalf("expected query URL of %v, got %v", url, queryURL)
|
||||
}
|
||||
|
||||
// Test parsing a properly formatted JSON API response.
|
||||
// First, create the response as a bytes.Reader.
|
||||
testFees := map[uint32]uint32{
|
||||
1: 12345,
|
||||
2: 42,
|
||||
3: 54321,
|
||||
}
|
||||
testJSON := map[string]map[uint32]uint32{"fee_by_block_target": testFees}
|
||||
jsonResp, err := json.Marshal(testJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to marshal JSON API response: %v", err)
|
||||
}
|
||||
reader := bytes.NewReader(jsonResp)
|
||||
|
||||
// Finally, ensure the expected map is returned without error.
|
||||
fees, err := feeSource.ParseResponse(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to parse API response: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(fees, testFees) {
|
||||
t.Fatalf("expected %v, got %v", testFees, fees)
|
||||
}
|
||||
|
||||
// Test parsing an improperly formatted JSON API response.
|
||||
badFees := map[string]uint32{"hi": 12345, "hello": 42, "satoshi": 54321}
|
||||
badJSON := map[string]map[string]uint32{"fee_by_block_target": badFees}
|
||||
jsonResp, err = json.Marshal(badJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to marshal JSON API response: %v", err)
|
||||
}
|
||||
reader = bytes.NewReader(jsonResp)
|
||||
|
||||
// Finally, ensure the improperly formatted fees error.
|
||||
_, err = feeSource.ParseResponse(reader)
|
||||
if err == nil {
|
||||
t.Fatalf("expected ParseResponse to fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWebAPIFeeEstimator checks that the WebAPIFeeEstimator returns fee rates
|
||||
// as expected.
|
||||
func TestWebAPIFeeEstimator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
feeFloor := uint32(FeePerKwFloor.FeePerKVByte())
|
||||
testCases := []struct {
|
||||
name string
|
||||
target uint32
|
||||
apiEst uint32
|
||||
est uint32
|
||||
err string
|
||||
}{
|
||||
{"target_below_min", 1, 12345, 12345, "too low, minimum"},
|
||||
{"target_w_too-low_fee", 10, 42, feeFloor, ""},
|
||||
{"API-omitted_target", 2, 0, 0, "web API does not include"},
|
||||
{"valid_target", 20, 54321, 54321, ""},
|
||||
{"valid_target_extrapolated_fee", 25, 0, 54321, ""},
|
||||
}
|
||||
|
||||
// Construct mock fee source for the Estimator to pull fees from.
|
||||
testFees := make(map[uint32]uint32)
|
||||
for _, tc := range testCases {
|
||||
if tc.apiEst != 0 {
|
||||
testFees[tc.target] = tc.apiEst
|
||||
}
|
||||
}
|
||||
|
||||
feeSource := mockSparseConfFeeSource{
|
||||
url: "https://www.github.com",
|
||||
fees: testFees,
|
||||
}
|
||||
|
||||
estimator := NewWebAPIEstimator(feeSource, 10)
|
||||
|
||||
// Test that requesting a fee when no fees have been cached fails.
|
||||
_, err := estimator.EstimateFeePerKW(5)
|
||||
if err == nil ||
|
||||
!strings.Contains(err.Error(), "web API does not include") {
|
||||
|
||||
t.Fatalf("expected fee estimation to fail, instead got: %v", err)
|
||||
}
|
||||
|
||||
if err := estimator.Start(); err != nil {
|
||||
t.Fatalf("unable to start fee estimator, got: %v", err)
|
||||
}
|
||||
defer estimator.Stop()
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
est, err := estimator.EstimateFeePerKW(tc.target)
|
||||
if tc.err != "" {
|
||||
if err == nil ||
|
||||
!strings.Contains(err.Error(), tc.err) {
|
||||
|
||||
t.Fatalf("expected fee estimation to "+
|
||||
"fail, instead got: %v", err)
|
||||
}
|
||||
} else {
|
||||
exp := SatPerKVByte(tc.est).FeePerKWeight()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to estimate fee for "+
|
||||
"%v block target, got: %v",
|
||||
tc.target, err)
|
||||
}
|
||||
if est != exp {
|
||||
t.Fatalf("expected fee estimate of "+
|
||||
"%v, got %v", exp, est)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
29
lnwallet/chainfee/log.go
Normal file
29
lnwallet/chainfee/log.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package chainfee
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
)
|
||||
|
||||
// log is a logger that is initialized with no output filters. This means the
|
||||
// package will not perform any logging by default until the caller requests
|
||||
// it.
|
||||
var log btclog.Logger
|
||||
|
||||
// The default amount of logging is none.
|
||||
func init() {
|
||||
UseLogger(build.NewSubLogger("CFEE", nil))
|
||||
}
|
||||
|
||||
// DisableLog disables all library log output. Logging output is disabled by
|
||||
// default until UseLogger is called.
|
||||
func DisableLog() {
|
||||
UseLogger(btclog.Disabled)
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info. This
|
||||
// should be used in preference to SetLogWriter if the caller is also using
|
||||
// btclog.
|
||||
func UseLogger(logger btclog.Logger) {
|
||||
log = logger
|
||||
}
|
53
lnwallet/chainfee/rates.go
Normal file
53
lnwallet/chainfee/rates.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package chainfee
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/blockchain"
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// FeePerKwFloor is the lowest fee rate in sat/kw that we should use for
|
||||
// determining transaction fees.
|
||||
FeePerKwFloor SatPerKWeight = 253
|
||||
)
|
||||
|
||||
// SatPerKVByte represents a fee rate in sat/kb.
|
||||
type SatPerKVByte btcutil.Amount
|
||||
|
||||
// FeeForVSize calculates the fee resulting from this fee rate and the given
|
||||
// vsize in vbytes.
|
||||
func (s SatPerKVByte) FeeForVSize(vbytes int64) btcutil.Amount {
|
||||
return btcutil.Amount(s) * btcutil.Amount(vbytes) / 1000
|
||||
}
|
||||
|
||||
// FeePerKWeight converts the current fee rate from sat/kb to sat/kw.
|
||||
func (s SatPerKVByte) FeePerKWeight() SatPerKWeight {
|
||||
return SatPerKWeight(s / blockchain.WitnessScaleFactor)
|
||||
}
|
||||
|
||||
// String returns a human-readable string of the fee rate.
|
||||
func (s SatPerKVByte) String() string {
|
||||
return fmt.Sprintf("%v sat/kb", int64(s))
|
||||
}
|
||||
|
||||
// SatPerKWeight represents a fee rate in sat/kw.
|
||||
type SatPerKWeight btcutil.Amount
|
||||
|
||||
// FeeForWeight calculates the fee resulting from this fee rate and the given
|
||||
// weight in weight units (wu).
|
||||
func (s SatPerKWeight) FeeForWeight(wu int64) btcutil.Amount {
|
||||
// The resulting fee is rounded down, as specified in BOLT#03.
|
||||
return btcutil.Amount(s) * btcutil.Amount(wu) / 1000
|
||||
}
|
||||
|
||||
// FeePerKVByte converts the current fee rate from sat/kw to sat/kb.
|
||||
func (s SatPerKWeight) FeePerKVByte() SatPerKVByte {
|
||||
return SatPerKVByte(s * blockchain.WitnessScaleFactor)
|
||||
}
|
||||
|
||||
// String returns a human-readable string of the fee rate.
|
||||
func (s SatPerKWeight) String() string {
|
||||
return fmt.Sprintf("%v sat/kw", int64(s))
|
||||
}
|
Reference in New Issue
Block a user