diff --git a/chainregistry.go b/chainregistry.go index 80a6dde3..71137e6a 100644 --- a/chainregistry.go +++ b/chainregistry.go @@ -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 ( diff --git a/config.go b/config.go index eedcf8ff..27e7f2ef 100644 --- a/config.go +++ b/config.go @@ -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 { diff --git a/lnd.go b/lnd.go index 56ab23c5..c140c15a 100644 --- a/lnd.go +++ b/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 diff --git a/lnwallet/btcwallet/config.go b/lnwallet/btcwallet/config.go index b52b966f..ea948e8a 100644 --- a/lnwallet/btcwallet/config.go +++ b/lnwallet/btcwallet/config.go @@ -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 diff --git a/lnwallet/fee_estimator.go b/lnwallet/fee_estimator.go index 76acc63a..65cbef04 100644 --- a/lnwallet/fee_estimator.go +++ b/lnwallet/fee_estimator.go @@ -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) diff --git a/lnwallet/fee_estimator_test.go b/lnwallet/fee_estimator_test.go index 77d60916..c2cfeb52 100644 --- a/lnwallet/fee_estimator_test.go +++ b/lnwallet/fee_estimator_test.go @@ -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) + } + } + }) + } +} diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index de496d66..4d30d07f 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -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 { @@ -2819,13 +2803,12 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, aliceSeedBytes := aliceSeed.Sum(nil) aliceWalletConfig := &btcwallet.Config{ - PrivatePass: []byte("alice-pass"), - HdSeed: aliceSeedBytes, - DataDir: tempTestDirAlice, - NetParams: netParams, - ChainSource: aliceClient, - FeeEstimator: feeEstimator, - CoinType: keychain.CoinTypeTestnet, + PrivatePass: []byte("alice-pass"), + HdSeed: aliceSeedBytes, + DataDir: tempTestDirAlice, + NetParams: netParams, + ChainSource: aliceClient, + CoinType: keychain.CoinTypeTestnet, } aliceWalletController, err = walletDriver.New(aliceWalletConfig) if err != nil { @@ -2843,13 +2826,12 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, bobSeedBytes := bobSeed.Sum(nil) bobWalletConfig := &btcwallet.Config{ - PrivatePass: []byte("bob-pass"), - HdSeed: bobSeedBytes, - DataDir: tempTestDirBob, - NetParams: netParams, - ChainSource: bobClient, - FeeEstimator: feeEstimator, - CoinType: keychain.CoinTypeTestnet, + PrivatePass: []byte("bob-pass"), + HdSeed: bobSeedBytes, + DataDir: tempTestDirBob, + NetParams: netParams, + ChainSource: bobClient, + CoinType: keychain.CoinTypeTestnet, } bobWalletController, err = walletDriver.New(bobWalletConfig) if err != nil { diff --git a/sample-lnd.conf b/sample-lnd.conf index d49fc1cd..f4dce3ec 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -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]