Merge pull request #2900 from valentinewallace/neutrino-fee-est
Accept external URL for neutrino fee estimation.
This commit is contained in:
commit
e396b557fd
@ -132,7 +132,7 @@ type chainControl struct {
|
||||
func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
privateWalletPw, publicWalletPw []byte, birthday time.Time,
|
||||
recoveryWindow uint32, wallet *wallet.Wallet,
|
||||
neutrinoCS *neutrino.ChainService) (*chainControl, func(), error) {
|
||||
neutrinoCS *neutrino.ChainService) (*chainControl, error) {
|
||||
|
||||
// Set the RPC config from the "home" chain. Multi-chain isn't yet
|
||||
// active, so we'll restrict usage to a particular chain for now.
|
||||
@ -167,8 +167,8 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
defaultLitecoinStaticFeePerKW, 0,
|
||||
)
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("Default routing policy for "+
|
||||
"chain %v is unknown", registeredChains.PrimaryChain())
|
||||
return nil, fmt.Errorf("Default routing policy for chain %v is "+
|
||||
"unknown", registeredChains.PrimaryChain())
|
||||
}
|
||||
|
||||
walletConfig := &btcwallet.Config{
|
||||
@ -178,20 +178,16 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
RecoveryWindow: recoveryWindow,
|
||||
DataDir: homeChainConfig.ChainDir,
|
||||
NetParams: activeNetParams.Params,
|
||||
FeeEstimator: cc.feeEstimator,
|
||||
CoinType: activeNetParams.CoinType,
|
||||
Wallet: wallet,
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
cleanUp func()
|
||||
)
|
||||
var err error
|
||||
|
||||
// Initialize the height hint cache within the chain directory.
|
||||
hintCache, err := chainntnfs.NewHeightHintCache(chanDB)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to initialize height hint "+
|
||||
return nil, fmt.Errorf("unable to initialize height hint "+
|
||||
"cache: %v", err)
|
||||
}
|
||||
|
||||
@ -208,9 +204,26 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
)
|
||||
cc.chainView, err = chainview.NewCfFilteredChainView(neutrinoCS)
|
||||
if err != nil {
|
||||
cleanUp()
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the user provided an API for fee estimation, activate it now.
|
||||
if cfg.NeutrinoMode.FeeURL != "" {
|
||||
ltndLog.Infof("Using API fee estimator!")
|
||||
|
||||
estimator := lnwallet.NewWebAPIFeeEstimator(
|
||||
lnwallet.SparseConfFeeSource{
|
||||
URL: cfg.NeutrinoMode.FeeURL,
|
||||
},
|
||||
defaultBitcoinStaticFeePerKW,
|
||||
)
|
||||
|
||||
if err := estimator.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc.feeEstimator = estimator
|
||||
}
|
||||
|
||||
walletConfig.ChainSource = chain.NewNeutrinoClient(
|
||||
activeNetParams.Params, neutrinoCS,
|
||||
)
|
||||
@ -238,7 +251,7 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
// this back to the btcwallet/bitcoind port.
|
||||
rpcPort, err := strconv.Atoi(activeNetParams.rpcPort)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
rpcPort -= 2
|
||||
bitcoindHost = fmt.Sprintf("%v:%d",
|
||||
@ -265,12 +278,12 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
100*time.Millisecond,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bitcoindConn.Start(); err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to connect to "+
|
||||
"bitcoind: %v", err)
|
||||
return nil, fmt.Errorf("unable to connect to bitcoind: "+
|
||||
"%v", err)
|
||||
}
|
||||
|
||||
cc.chainNotifier = bitcoindnotify.New(
|
||||
@ -302,10 +315,10 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
*rpcConfig, fallBackFeeRate.FeePerKWeight(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if err := cc.feeEstimator.Start(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
} else if cfg.Litecoin.Active {
|
||||
ltndLog.Infof("Initializing litecoind backed fee estimator")
|
||||
@ -319,10 +332,10 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
*rpcConfig, fallBackFeeRate.FeePerKWeight(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if err := cc.feeEstimator.Start(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
case "btcd", "ltcd":
|
||||
@ -343,19 +356,19 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
if btcdMode.RawRPCCert != "" {
|
||||
rpcCert, err = hex.DecodeString(btcdMode.RawRPCCert)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
certFile, err := os.Open(btcdMode.RPCCert)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
rpcCert, err = ioutil.ReadAll(certFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if err := certFile.Close(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -387,7 +400,7 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
rpcConfig, activeNetParams.Params, hintCache, hintCache,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Finally, we'll create an instance of the default chain view to be
|
||||
@ -395,7 +408,7 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
cc.chainView, err = chainview.NewBtcdFilteredChainView(*rpcConfig)
|
||||
if err != nil {
|
||||
srvrLog.Errorf("unable to create chain view: %v", err)
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a special websockets rpc client for btcd which will be used
|
||||
@ -403,7 +416,7 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
chainRPC, err := chain.NewRPCClient(activeNetParams.Params, btcdHost,
|
||||
btcdUser, btcdPass, rpcCert, false, 20)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
walletConfig.ChainSource = chainRPC
|
||||
@ -424,24 +437,21 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
*rpcConfig, fallBackFeeRate.FeePerKWeight(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if err := cc.feeEstimator.Start(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown node type: %s",
|
||||
return nil, fmt.Errorf("unknown node type: %s",
|
||||
homeChainConfig.Node)
|
||||
}
|
||||
|
||||
wc, err := btcwallet.New(*walletConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("unable to create wallet controller: %v\n", err)
|
||||
if cleanUp != nil {
|
||||
cleanUp()
|
||||
}
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cc.msgSigner = wc
|
||||
@ -476,24 +486,18 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
||||
lnWallet, err := lnwallet.NewLightningWallet(walletCfg)
|
||||
if err != nil {
|
||||
fmt.Printf("unable to create wallet: %v\n", err)
|
||||
if cleanUp != nil {
|
||||
cleanUp()
|
||||
}
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if err := lnWallet.Startup(); err != nil {
|
||||
fmt.Printf("unable to start wallet: %v\n", err)
|
||||
if cleanUp != nil {
|
||||
cleanUp()
|
||||
}
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ltndLog.Info("LightningWallet opened")
|
||||
|
||||
cc.wallet = lnWallet
|
||||
|
||||
return cc, cleanUp, nil
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -130,6 +130,7 @@ type neutrinoConfig struct {
|
||||
MaxPeers int `long:"maxpeers" description:"Max number of inbound and outbound peers"`
|
||||
BanDuration time.Duration `long:"banduration" description:"How long to ban misbehaving peers. Valid time units are {s, m, h}. Minimum 1 second"`
|
||||
BanThreshold uint32 `long:"banthreshold" description:"Maximum allowed ban score before disconnecting and banning misbehaving peers."`
|
||||
FeeURL string `long:"feeurl" description:"Optional URL for fee estimation. If a URL is not specified, static fees will be used for estimation."`
|
||||
}
|
||||
|
||||
type btcdConfig struct {
|
||||
|
5
lnd.go
5
lnd.go
@ -278,7 +278,7 @@ func lndMain() error {
|
||||
// With the information parsed from the configuration, create valid
|
||||
// instances of the pertinent interfaces required to operate the
|
||||
// Lightning Network Daemon.
|
||||
activeChainControl, chainCleanUp, err := newChainControlFromConfig(
|
||||
activeChainControl, err := newChainControlFromConfig(
|
||||
cfg, chanDB, privateWalletPw, publicWalletPw,
|
||||
walletInitParams.Birthday, walletInitParams.RecoveryWindow,
|
||||
walletInitParams.Wallet, neutrinoCS,
|
||||
@ -287,9 +287,6 @@ func lndMain() error {
|
||||
fmt.Printf("unable to create chain control: %v\n", err)
|
||||
return err
|
||||
}
|
||||
if chainCleanUp != nil {
|
||||
defer chainCleanUp()
|
||||
}
|
||||
|
||||
// Finally before we start the server, we'll register the "holy
|
||||
// trinity" of interface for our current "home chain" with the active
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
|
||||
"github.com/btcsuite/btcwallet/chain"
|
||||
"github.com/btcsuite/btcwallet/wallet"
|
||||
@ -79,11 +78,6 @@ type Config struct {
|
||||
// notifications for received funds, etc.
|
||||
ChainSource chain.Interface
|
||||
|
||||
// FeeEstimator is an instance of the fee estimator interface which
|
||||
// will be used by the wallet to dynamically set transaction fees when
|
||||
// crafting transactions.
|
||||
FeeEstimator lnwallet.FeeEstimator
|
||||
|
||||
// NetParams is the net parameters for the target chain.
|
||||
NetParams *chaincfg.Params
|
||||
|
||||
|
@ -2,6 +2,13 @@ package lnwallet
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
prand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/blockchain"
|
||||
"github.com/btcsuite/btcd/rpcclient"
|
||||
@ -12,6 +19,25 @@ const (
|
||||
// FeePerKwFloor is the lowest fee rate in sat/kw that we should use for
|
||||
// determining transaction fees.
|
||||
FeePerKwFloor SatPerKWeight = 253
|
||||
|
||||
// maxBlockTarget is the highest number of blocks confirmations that
|
||||
// a WebAPIFeeEstimator 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 WebAPIFeeEstimator 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
|
||||
// WebAPIFeeEstimator will request fresh fees from its API.
|
||||
minFeeUpdateTimeout = 5 * time.Minute
|
||||
|
||||
// maxFeeUpdateTimeout represents the maximum interval in which a
|
||||
// WebAPIFeeEstimator will request fresh fees from its API.
|
||||
maxFeeUpdateTimeout = 20 * time.Minute
|
||||
)
|
||||
|
||||
// SatPerKVByte represents a fee rate in sat/kb.
|
||||
@ -458,3 +484,271 @@ func (b *BitcoindFeeEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight,
|
||||
// A compile-time assertion to ensure that BitcoindFeeEstimator implements the
|
||||
// FeeEstimator interface.
|
||||
var _ FeeEstimator = (*BitcoindFeeEstimator)(nil)
|
||||
|
||||
// WebAPIFeeSource is an interface allows the WebAPIFeeEstimator to query an
|
||||
// arbitrary HTTP-based fee estimator. Each new set/network will gain an
|
||||
// implementation of this interface in order to allow the WebAPIFeeEstimator 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)
|
||||
|
||||
// WebAPIFeeEstimator is an implementation of the FeeEstimator interface that
|
||||
// queries an HTTP-based fee estimation from an existing web API.
|
||||
type WebAPIFeeEstimator 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
|
||||
}
|
||||
|
||||
// NewWebAPIFeeEstimator creates a new WebAPIFeeEstimator from a given URL and a
|
||||
// fallback default fee. The fees are updated whenever a new block is mined.
|
||||
func NewWebAPIFeeEstimator(
|
||||
api WebAPIFeeSource, defaultFee SatPerKWeight) *WebAPIFeeEstimator {
|
||||
|
||||
return &WebAPIFeeEstimator{
|
||||
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 FeeEstimator interface.
|
||||
func (w *WebAPIFeeEstimator) 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
|
||||
}
|
||||
|
||||
walletLog.Debugf("Web API returning %v sat/kw for conf target of %v",
|
||||
int64(satPerKw), numBlocks)
|
||||
|
||||
return satPerKw, nil
|
||||
}
|
||||
|
||||
// Start signals the FeeEstimator to start any processes or goroutines it needs
|
||||
// to perform its duty.
|
||||
//
|
||||
// NOTE: This method is part of the FeeEstimator interface.
|
||||
func (w *WebAPIFeeEstimator) Start() error {
|
||||
var err error
|
||||
w.started.Do(func() {
|
||||
walletLog.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 FeeEstimator interface.
|
||||
func (w *WebAPIFeeEstimator) Stop() error {
|
||||
w.stopped.Do(func() {
|
||||
walletLog.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 FeeEstimator interface.
|
||||
func (w *WebAPIFeeEstimator) 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 *WebAPIFeeEstimator) 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 *WebAPIFeeEstimator) 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 *WebAPIFeeEstimator) 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 {
|
||||
walletLog.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 {
|
||||
walletLog.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 *WebAPIFeeEstimator) feeUpdateManager() {
|
||||
defer w.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.updateFeeTicker.C:
|
||||
w.updateFeeEstimates()
|
||||
case <-w.quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A compile-time assertion to ensure that WebAPIFeeEstimator implements the
|
||||
// FeeEstimator interface.
|
||||
var _ FeeEstimator = (*WebAPIFeeEstimator)(nil)
|
||||
|
@ -1,12 +1,31 @@
|
||||
package lnwallet_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
)
|
||||
|
||||
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.
|
||||
@ -67,8 +86,8 @@ func TestFeeRateTypes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestStaticFeeEstimator checks that the StaticFeeEstimator
|
||||
// returns the expected fee rate.
|
||||
// TestStaticFeeEstimator checks that the StaticFeeEstimator returns the
|
||||
// expected fee rate.
|
||||
func TestStaticFeeEstimator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -89,3 +108,129 @@ func TestStaticFeeEstimator(t *testing.T) {
|
||||
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 := lnwallet.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(lnwallet.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 := lnwallet.NewWebAPIFeeEstimator(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 {
|
||||
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 := lnwallet.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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -2646,8 +2646,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||
|
||||
aliceWalletController lnwallet.WalletController
|
||||
bobWalletController lnwallet.WalletController
|
||||
|
||||
feeEstimator lnwallet.FeeEstimator
|
||||
)
|
||||
|
||||
tempTestDirAlice, err := ioutil.TempDir("", "lnwallet")
|
||||
@ -2668,12 +2666,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||
var aliceClient, bobClient chain.Interface
|
||||
switch backEnd {
|
||||
case "btcd":
|
||||
feeEstimator, err = lnwallet.NewBtcdFeeEstimator(
|
||||
rpcConfig, 250)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create btcd fee estimator: %v",
|
||||
err)
|
||||
}
|
||||
aliceClient, err = chain.NewRPCClient(netParams,
|
||||
rpcConfig.Host, rpcConfig.User, rpcConfig.Pass,
|
||||
rpcConfig.Certificates, false, 20)
|
||||
@ -2688,8 +2680,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||
}
|
||||
|
||||
case "neutrino":
|
||||
feeEstimator = lnwallet.NewStaticFeeEstimator(62500, 0)
|
||||
|
||||
// Set some package-level variable to speed up
|
||||
// operation for tests.
|
||||
neutrino.BanDuration = time.Millisecond * 100
|
||||
@ -2751,12 +2741,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||
)
|
||||
|
||||
case "bitcoind":
|
||||
feeEstimator, err = lnwallet.NewBitcoindFeeEstimator(
|
||||
rpcConfig, 250)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create bitcoind fee estimator: %v",
|
||||
err)
|
||||
}
|
||||
// Start a bitcoind instance.
|
||||
tempBitcoindDir, err := ioutil.TempDir("", "bitcoind")
|
||||
if err != nil {
|
||||
@ -2824,7 +2808,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||
DataDir: tempTestDirAlice,
|
||||
NetParams: netParams,
|
||||
ChainSource: aliceClient,
|
||||
FeeEstimator: feeEstimator,
|
||||
CoinType: keychain.CoinTypeTestnet,
|
||||
}
|
||||
aliceWalletController, err = walletDriver.New(aliceWalletConfig)
|
||||
@ -2848,7 +2831,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||
DataDir: tempTestDirBob,
|
||||
NetParams: netParams,
|
||||
ChainSource: bobClient,
|
||||
FeeEstimator: feeEstimator,
|
||||
CoinType: keychain.CoinTypeTestnet,
|
||||
}
|
||||
bobWalletController, err = walletDriver.New(bobWalletConfig)
|
||||
|
@ -233,6 +233,9 @@ bitcoin.node=btcd
|
||||
; Add a peer to connect with at startup.
|
||||
; neutrino.addpeer=
|
||||
|
||||
; Set a URL source for fee estimates.
|
||||
; neutrino.feeurl=
|
||||
|
||||
|
||||
[Litecoin]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user