diff --git a/lnwallet/chainfee/estimator.go b/lnwallet/chainfee/estimator.go index 24552af562..5e9a6999d3 100644 --- a/lnwallet/chainfee/estimator.go +++ b/lnwallet/chainfee/estimator.go @@ -628,9 +628,9 @@ var _ Estimator = (*BitcoindEstimator)(nil) // implementation of this interface in order to allow the WebAPIEstimator to // be fully generic in its logic. type WebAPIFeeSource interface { - // GetFeeMap will query the web API, parse the response and return a + // GetResponse will query the web API, parse the response and return a // map of confirmation targets to sat/kw fees. - GetFeeMap() (map[uint32]uint32, error) + GetResponse() (*WebAPIResponse, error) } // SparseConfFeeSource is an implementation of the WebAPIFeeSource that utilizes @@ -642,30 +642,37 @@ type SparseConfFeeSource struct { URL string } +// WebAPIResponse is the response returned by the fee estimation API. +type WebAPIResponse struct { + // FeeByBlockTarget is a map of confirmation targets to sat/kvb fees. + FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"` + + // MinRelayFeerate is the minimum relay fee in sat/kvb. + MinRelayFeerate SatPerKVByte `json:"min_relay_feerate"` +} + // 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. func (s SparseConfFeeSource) parseResponse(r io.Reader) ( - map[uint32]uint32, error) { - - type jsonResp struct { - FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"` - } + *WebAPIResponse, error) { - resp := jsonResp{ + resp := &WebAPIResponse{ FeeByBlockTarget: make(map[uint32]uint32), + MinRelayFeerate: 0, } jsonReader := json.NewDecoder(r) if err := jsonReader.Decode(&resp); err != nil { return nil, err } - return resp.FeeByBlockTarget, nil + return resp, nil } -// GetFeeMap will query the web API, parse the response and return a map of -// confirmation targets to sat/kw fees. -func (s SparseConfFeeSource) GetFeeMap() (map[uint32]uint32, error) { +// GetResponse will query the web API, parse the response and return a map of +// confirmation targets to sat/kw fees and min relay feerate in a parsed +// response. +func (s SparseConfFeeSource) GetResponse() (*WebAPIResponse, error) { // 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 @@ -694,14 +701,14 @@ func (s SparseConfFeeSource) GetFeeMap() (map[uint32]uint32, error) { // Once we've obtained the response, we'll instruct the WebAPIFeeSource // to parse out the body to obtain our final result. - feesByBlockTarget, err := s.parseResponse(resp.Body) + parsedResp, err := s.parseResponse(resp.Body) if err != nil { log.Errorf("unable to parse fee api response: %v", err) return nil, err } - return feesByBlockTarget, nil + return parsedResp, nil } // A compile-time assertion to ensure that SparseConfFeeSource implements the @@ -726,6 +733,7 @@ type WebAPIEstimator struct { // rather than re-querying the API, to prevent an inadvertent DoS attack. feesMtx sync.Mutex feeByBlockTarget map[uint32]uint32 + minRelayFeerate SatPerKVByte // noCache determines whether the web estimator should cache fee // estimates. @@ -839,6 +847,7 @@ func (w *WebAPIEstimator) Start() error { go w.feeUpdateManager() }) + return err } @@ -868,7 +877,22 @@ func (w *WebAPIEstimator) Stop() error { // // NOTE: This method is part of the Estimator interface. func (w *WebAPIEstimator) RelayFeePerKW() SatPerKWeight { - return FeePerKwFloor + // Get fee estimates now if we don't refresh periodically. + if w.noCache { + w.updateFeeEstimates() + } + + if w.minRelayFeerate == 0 { + log.Errorf("No min relay fee rate available, using default %v", + FeePerKwFloor) + + return FeePerKwFloor + } + + log.Infof("Web API returning %v for min relay feerate", + w.minRelayFeerate) + + return w.minRelayFeerate.FeePerKWeight() } // randomFeeUpdateTimeout returns a random timeout between minFeeUpdateTimeout @@ -958,14 +982,21 @@ func (w *WebAPIEstimator) getCachedFee(numBlocks uint32) (uint32, error) { func (w *WebAPIEstimator) updateFeeEstimates() { // 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.GetFeeMap() + resp, err := w.apiSource.GetResponse() if err != nil { log.Errorf("unable to get fee response: %v", err) return } + log.Debugf("Received response from source: %s", newLogClosure( + func() string { + resp, _ := json.Marshal(resp) + return fmt.Sprintf("%s", resp) + })) + w.feesMtx.Lock() - w.feeByBlockTarget = feesByBlockTarget + w.feeByBlockTarget = resp.FeeByBlockTarget + w.minRelayFeerate = resp.MinRelayFeerate w.feesMtx.Unlock() } diff --git a/lnwallet/chainfee/estimator_test.go b/lnwallet/chainfee/estimator_test.go index 51ab5bf29b..aeefb09f24 100644 --- a/lnwallet/chainfee/estimator_test.go +++ b/lnwallet/chainfee/estimator_test.go @@ -107,17 +107,20 @@ func TestSparseConfFeeSource(t *testing.T) { 2: 42, 3: 54321, } - testJSON := map[string]map[uint32]uint32{ - "fee_by_block_target": testFees, + testMinRelayFee := SatPerKVByte(1000) + testResp := &WebAPIResponse{ + MinRelayFeerate: testMinRelayFee, + FeeByBlockTarget: testFees, } - jsonResp, err := json.Marshal(testJSON) + + jsonResp, err := json.Marshal(testResp) require.NoError(t, err, "unable to marshal JSON API response") reader := bytes.NewReader(jsonResp) // Finally, ensure the expected map is returned without error. - fees, err := feeSource.parseResponse(reader) + resp, err := feeSource.parseResponse(reader) require.NoError(t, err, "unable to parse API response") - require.Equal(t, testFees, fees, "unexpected fee map returned") + require.Equal(t, testResp, resp, "unexpected resp returned") // Test parsing an improperly formatted JSON API response. badFees := map[string]uint32{"hi": 12345, "hello": 42, "satoshi": 54321} @@ -194,14 +197,17 @@ func TestWebAPIFeeEstimator(t *testing.T) { // This will create a `feeByBlockTarget` map with the following values, // - 2: 4000 sat/kb // - 6: 2000 sat/kb. - feeRateResp := map[uint32]uint32{ + feeRates := map[uint32]uint32{ minTarget: maxFeeRate, maxTarget: minFeeRate, } + resp := &WebAPIResponse{ + FeeByBlockTarget: feeRates, + } // Create a mock fee source and mock its returned map. feeSource := &mockFeeSource{} - feeSource.On("GetFeeMap").Return(feeRateResp, nil) + feeSource.On("GetResponse").Return(resp, nil) estimator, _ := NewWebAPIEstimator( feeSource, false, minFeeUpdateTimeout, maxFeeUpdateTimeout, @@ -234,7 +240,7 @@ func TestWebAPIFeeEstimator(t *testing.T) { exp := SatPerKVByte(tc.expectedFeeRate).FeePerKWeight() require.Equalf(t, exp, est, "target %v failed, fee "+ - "map is %v", tc.target, feeRateResp) + "map is %v", tc.target, feeRate) }) } diff --git a/lnwallet/chainfee/log.go b/lnwallet/chainfee/log.go index d5d0405782..9145830330 100644 --- a/lnwallet/chainfee/log.go +++ b/lnwallet/chainfee/log.go @@ -27,3 +27,19 @@ func DisableLog() { func UseLogger(logger btclog.Logger) { log = logger } + +// logClosure is used to provide a closure over expensive logging operations so +// don't have to be performed when the logging level doesn't warrant it. +type logClosure func() string + +// String invokes the underlying function and returns the result. +func (c logClosure) String() string { + return c() +} + +// newLogClosure returns a new closure over a function that returns a string +// which itself provides a Stringer interface so that it can be used with the +// logging system. +func newLogClosure(c func() string) logClosure { + return logClosure(c) +} diff --git a/lnwallet/chainfee/mocks.go b/lnwallet/chainfee/mocks.go index e14340d91f..9f9571a473 100644 --- a/lnwallet/chainfee/mocks.go +++ b/lnwallet/chainfee/mocks.go @@ -12,10 +12,10 @@ type mockFeeSource struct { // WebAPIFeeSource interface. var _ WebAPIFeeSource = (*mockFeeSource)(nil) -func (m *mockFeeSource) GetFeeMap() (map[uint32]uint32, error) { +func (m *mockFeeSource) GetResponse() (*WebAPIResponse, error) { args := m.Called() - return args.Get(0).(map[uint32]uint32), args.Error(1) + return args.Get(0).(*WebAPIResponse), args.Error(1) } // MockEstimator implements the `Estimator` interface and is used by diff --git a/lnwallet/chainfee/rates.go b/lnwallet/chainfee/rates.go index f5294e4d1c..20f4f1e1d2 100644 --- a/lnwallet/chainfee/rates.go +++ b/lnwallet/chainfee/rates.go @@ -53,7 +53,7 @@ func (s SatPerKVByte) FeePerKWeight() SatPerKWeight { // String returns a human-readable string of the fee rate. func (s SatPerKVByte) String() string { - return fmt.Sprintf("%v sat/kb", int64(s)) + return fmt.Sprintf("%v sat/kvb", int64(s)) } // SatPerKWeight represents a fee rate in sat/kw.