Skip to content

Commit ce43e4b

Browse files
committed
chainfee: allow specifying min relay feerate from API source
This commit adds a new expected field, `min_relay_feerate`, in the response body returned from the API source, allowing the API to specify a min relay feerate to be used instead of the FeePerKwFloor. This change is backwards compatible as for an old API source which doesn't specify the `min_relay_feerate`, it will be interpreted as zero.
1 parent b10ebb2 commit ce43e4b

File tree

5 files changed

+124
-32
lines changed

5 files changed

+124
-32
lines changed

lnwallet/chainfee/estimator.go

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -628,9 +628,10 @@ var _ Estimator = (*BitcoindEstimator)(nil)
628628
// implementation of this interface in order to allow the WebAPIEstimator to
629629
// be fully generic in its logic.
630630
type WebAPIFeeSource interface {
631-
// GetFeeMap will query the web API, parse the response and return a
632-
// map of confirmation targets to sat/kw fees.
633-
GetFeeMap() (map[uint32]uint32, error)
631+
// GetFeeInfo will query the web API, parse the response into a
632+
// WebAPIResponse which contains a map of confirmation targets to
633+
// sat/kw fees and min relay feerate.
634+
GetFeeInfo() (WebAPIResponse, error)
634635
}
635636

636637
// SparseConfFeeSource is an implementation of the WebAPIFeeSource that utilizes
@@ -642,30 +643,43 @@ type SparseConfFeeSource struct {
642643
URL string
643644
}
644645

646+
// WebAPIResponse is the response returned by the fee estimation API.
647+
type WebAPIResponse struct {
648+
// FeeByBlockTarget is a map of confirmation targets to sat/kvb fees.
649+
FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"`
650+
651+
// MinRelayFeerate is the minimum relay fee in sat/kvb.
652+
MinRelayFeerate SatPerKVByte `json:"min_relay_feerate"`
653+
}
654+
645655
// parseResponse attempts to parse the body of the response generated by the
646656
// above query URL. Typically this will be JSON, but the specifics are left to
647657
// the WebAPIFeeSource implementation.
648658
func (s SparseConfFeeSource) parseResponse(r io.Reader) (
649-
map[uint32]uint32, error) {
650-
651-
type jsonResp struct {
652-
FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"`
653-
}
659+
WebAPIResponse, error) {
654660

655-
resp := jsonResp{
661+
resp := WebAPIResponse{
656662
FeeByBlockTarget: make(map[uint32]uint32),
663+
MinRelayFeerate: 0,
657664
}
658665
jsonReader := json.NewDecoder(r)
659666
if err := jsonReader.Decode(&resp); err != nil {
660-
return nil, err
667+
return WebAPIResponse{}, err
668+
}
669+
670+
if resp.MinRelayFeerate == 0 {
671+
log.Errorf("No min relay fee rate available, using default %v",
672+
FeePerKwFloor)
673+
resp.MinRelayFeerate = FeePerKwFloor.FeePerKVByte()
661674
}
662675

663-
return resp.FeeByBlockTarget, nil
676+
return resp, nil
664677
}
665678

666-
// GetFeeMap will query the web API, parse the response and return a map of
667-
// confirmation targets to sat/kw fees.
668-
func (s SparseConfFeeSource) GetFeeMap() (map[uint32]uint32, error) {
679+
// GetFeeInfo will query the web API, parse the response and return a map of
680+
// confirmation targets to sat/kw fees and min relay feerate in a parsed
681+
// response.
682+
func (s SparseConfFeeSource) GetFeeInfo() (WebAPIResponse, error) {
669683
// Rather than use the default http.Client, we'll make a custom one
670684
// which will allow us to control how long we'll wait to read the
671685
// response from the service. This way, if the service is down or
@@ -688,20 +702,20 @@ func (s SparseConfFeeSource) GetFeeMap() (map[uint32]uint32, error) {
688702
if err != nil {
689703
log.Errorf("unable to query web api for fee response: %v",
690704
err)
691-
return nil, err
705+
return WebAPIResponse{}, err
692706
}
693707
defer resp.Body.Close()
694708

695709
// Once we've obtained the response, we'll instruct the WebAPIFeeSource
696710
// to parse out the body to obtain our final result.
697-
feesByBlockTarget, err := s.parseResponse(resp.Body)
711+
parsedResp, err := s.parseResponse(resp.Body)
698712
if err != nil {
699713
log.Errorf("unable to parse fee api response: %v", err)
700714

701-
return nil, err
715+
return WebAPIResponse{}, err
702716
}
703717

704-
return feesByBlockTarget, nil
718+
return parsedResp, nil
705719
}
706720

707721
// A compile-time assertion to ensure that SparseConfFeeSource implements the
@@ -726,6 +740,7 @@ type WebAPIEstimator struct {
726740
// rather than re-querying the API, to prevent an inadvertent DoS attack.
727741
feesMtx sync.Mutex
728742
feeByBlockTarget map[uint32]uint32
743+
minRelayFeerate SatPerKVByte
729744

730745
// noCache determines whether the web estimator should cache fee
731746
// estimates.
@@ -837,6 +852,7 @@ func (w *WebAPIEstimator) Start() error {
837852
go w.feeUpdateManager()
838853

839854
})
855+
840856
return err
841857
}
842858

@@ -866,7 +882,15 @@ func (w *WebAPIEstimator) Stop() error {
866882
//
867883
// NOTE: This method is part of the Estimator interface.
868884
func (w *WebAPIEstimator) RelayFeePerKW() SatPerKWeight {
869-
return FeePerKwFloor
885+
// Get fee estimates now if we don't refresh periodically.
886+
if w.noCache {
887+
w.updateFeeEstimates()
888+
}
889+
890+
log.Infof("Web API returning %v for min relay feerate",
891+
w.minRelayFeerate)
892+
893+
return w.minRelayFeerate.FeePerKWeight()
870894
}
871895

872896
// randomFeeUpdateTimeout returns a random timeout between minFeeUpdateTimeout
@@ -956,14 +980,21 @@ func (w *WebAPIEstimator) getCachedFee(numBlocks uint32) (uint32, error) {
956980
func (w *WebAPIEstimator) updateFeeEstimates() {
957981
// Once we've obtained the response, we'll instruct the WebAPIFeeSource
958982
// to parse out the body to obtain our final result.
959-
feesByBlockTarget, err := w.apiSource.GetFeeMap()
983+
resp, err := w.apiSource.GetFeeInfo()
960984
if err != nil {
961985
log.Errorf("unable to get fee response: %v", err)
962986
return
963987
}
964988

989+
log.Debugf("Received response from source: %s", newLogClosure(
990+
func() string {
991+
resp, _ := json.Marshal(resp)
992+
return string(resp)
993+
}))
994+
965995
w.feesMtx.Lock()
966-
w.feeByBlockTarget = feesByBlockTarget
996+
w.feeByBlockTarget = resp.FeeByBlockTarget
997+
w.minRelayFeerate = resp.MinRelayFeerate
967998
w.feesMtx.Unlock()
968999
}
9691000

lnwallet/chainfee/estimator_test.go

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,20 @@ func TestSparseConfFeeSource(t *testing.T) {
107107
2: 42,
108108
3: 54321,
109109
}
110-
testJSON := map[string]map[uint32]uint32{
111-
"fee_by_block_target": testFees,
110+
testMinRelayFee := SatPerKVByte(1000)
111+
testResp := WebAPIResponse{
112+
MinRelayFeerate: testMinRelayFee,
113+
FeeByBlockTarget: testFees,
112114
}
113-
jsonResp, err := json.Marshal(testJSON)
115+
116+
jsonResp, err := json.Marshal(testResp)
114117
require.NoError(t, err, "unable to marshal JSON API response")
115118
reader := bytes.NewReader(jsonResp)
116119

117120
// Finally, ensure the expected map is returned without error.
118-
fees, err := feeSource.parseResponse(reader)
121+
resp, err := feeSource.parseResponse(reader)
119122
require.NoError(t, err, "unable to parse API response")
120-
require.Equal(t, testFees, fees, "unexpected fee map returned")
123+
require.Equal(t, testResp, resp, "unexpected resp returned")
121124

122125
// Test parsing an improperly formatted JSON API response.
123126
badFees := map[string]uint32{"hi": 12345, "hello": 42, "satoshi": 54321}
@@ -131,6 +134,45 @@ func TestSparseConfFeeSource(t *testing.T) {
131134
require.Error(t, err, "expected error when parsing bad JSON")
132135
}
133136

137+
// TestFeeSourceCompatibility checks that when a fee source doesn't return a
138+
// `min_relay_feerate` field in its response, the floor feerate is used.
139+
//
140+
// NOTE: Field `min_relay_feerate` was added in v0.18.3.
141+
func TestFeeSourceCompatibility(t *testing.T) {
142+
t.Parallel()
143+
144+
// Test that GenQueryURL returns the URL as is.
145+
url := "test"
146+
feeSource := SparseConfFeeSource{URL: url}
147+
148+
// Test parsing a properly formatted JSON API response.
149+
//
150+
// Create the resp without the `min_relay_feerate` field.
151+
testFees := map[uint32]uint32{
152+
1: 12345,
153+
}
154+
testResp := struct {
155+
// FeeByBlockTarget is a map of confirmation targets to sat/kvb
156+
// fees.
157+
FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"`
158+
}{
159+
FeeByBlockTarget: testFees,
160+
}
161+
162+
jsonResp, err := json.Marshal(testResp)
163+
require.NoError(t, err, "unable to marshal JSON API response")
164+
reader := bytes.NewReader(jsonResp)
165+
166+
// Ensure the expected map is returned without error.
167+
resp, err := feeSource.parseResponse(reader)
168+
require.NoError(t, err, "unable to parse API response")
169+
require.Equal(t, testResp.FeeByBlockTarget, resp.FeeByBlockTarget,
170+
"unexpected resp returned")
171+
172+
// Expect the floor feerate to be used.
173+
require.Equal(t, FeePerKwFloor.FeePerKVByte(), resp.MinRelayFeerate)
174+
}
175+
134176
// TestWebAPIFeeEstimator checks that the WebAPIFeeEstimator returns fee rates
135177
// as expected.
136178
func TestWebAPIFeeEstimator(t *testing.T) {
@@ -194,14 +236,17 @@ func TestWebAPIFeeEstimator(t *testing.T) {
194236
// This will create a `feeByBlockTarget` map with the following values,
195237
// - 2: 4000 sat/kb
196238
// - 6: 2000 sat/kb.
197-
feeRateResp := map[uint32]uint32{
239+
feeRates := map[uint32]uint32{
198240
minTarget: maxFeeRate,
199241
maxTarget: minFeeRate,
200242
}
243+
resp := WebAPIResponse{
244+
FeeByBlockTarget: feeRates,
245+
}
201246

202247
// Create a mock fee source and mock its returned map.
203248
feeSource := &mockFeeSource{}
204-
feeSource.On("GetFeeMap").Return(feeRateResp, nil)
249+
feeSource.On("GetFeeInfo").Return(resp, nil)
205250

206251
estimator, _ := NewWebAPIEstimator(
207252
feeSource, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
@@ -234,7 +279,7 @@ func TestWebAPIFeeEstimator(t *testing.T) {
234279

235280
exp := SatPerKVByte(tc.expectedFeeRate).FeePerKWeight()
236281
require.Equalf(t, exp, est, "target %v failed, fee "+
237-
"map is %v", tc.target, feeRateResp)
282+
"map is %v", tc.target, feeRate)
238283
})
239284
}
240285

lnwallet/chainfee/log.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,19 @@ func DisableLog() {
2727
func UseLogger(logger btclog.Logger) {
2828
log = logger
2929
}
30+
31+
// logClosure is used to provide a closure over expensive logging operations so
32+
// don't have to be performed when the logging level doesn't warrant it.
33+
type logClosure func() string
34+
35+
// String invokes the underlying function and returns the result.
36+
func (c logClosure) String() string {
37+
return c()
38+
}
39+
40+
// newLogClosure returns a new closure over a function that returns a string
41+
// which itself provides a Stringer interface so that it can be used with the
42+
// logging system.
43+
func newLogClosure(c func() string) logClosure {
44+
return logClosure(c)
45+
}

lnwallet/chainfee/mocks.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ type mockFeeSource struct {
1212
// WebAPIFeeSource interface.
1313
var _ WebAPIFeeSource = (*mockFeeSource)(nil)
1414

15-
func (m *mockFeeSource) GetFeeMap() (map[uint32]uint32, error) {
15+
func (m *mockFeeSource) GetFeeInfo() (WebAPIResponse, error) {
1616
args := m.Called()
1717

18-
return args.Get(0).(map[uint32]uint32), args.Error(1)
18+
return args.Get(0).(WebAPIResponse), args.Error(1)
1919
}
2020

2121
// MockEstimator implements the `Estimator` interface and is used by

lnwallet/chainfee/rates.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (s SatPerKVByte) FeePerKWeight() SatPerKWeight {
5353

5454
// String returns a human-readable string of the fee rate.
5555
func (s SatPerKVByte) String() string {
56-
return fmt.Sprintf("%v sat/kb", int64(s))
56+
return fmt.Sprintf("%v sat/kvb", int64(s))
5757
}
5858

5959
// SatPerKWeight represents a fee rate in sat/kw.

0 commit comments

Comments
 (0)