Skip to content

Commit

Permalink
chainfee: allow specifying min relay feerate from API source
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yyforyongyu committed Jul 4, 2024
1 parent 451cedc commit b108b37
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 28 deletions.
65 changes: 48 additions & 17 deletions lnwallet/chainfee/estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -839,6 +847,7 @@ func (w *WebAPIEstimator) Start() error {
go w.feeUpdateManager()

})

return err
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}

Expand Down
22 changes: 14 additions & 8 deletions lnwallet/chainfee/estimator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
})
}

Expand Down
16 changes: 16 additions & 0 deletions lnwallet/chainfee/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 2 additions & 2 deletions lnwallet/chainfee/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lnwallet/chainfee/rates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit b108b37

Please sign in to comment.