Skip to content

Commit 7e60d41

Browse files
committed
chainfee: make sure web API has been started before estimating
Previously we may get a floor feerate when calling `EstimateFeePerKW` due to the fee estimator not finishing its startup process, which gives us an empty fee map. This is now fixed to return an error if the estimator is not started.
1 parent 0a0d51c commit 7e60d41

File tree

2 files changed

+44
-26
lines changed

2 files changed

+44
-26
lines changed

lnwallet/chainfee/estimator.go

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net"
1111
"net/http"
1212
"sync"
13+
"sync/atomic"
1314
"time"
1415

1516
"github.com/btcsuite/btcd/btcutil"
@@ -725,8 +726,8 @@ var _ WebAPIFeeSource = (*SparseConfFeeSource)(nil)
725726
// WebAPIEstimator is an implementation of the Estimator interface that
726727
// queries an HTTP-based fee estimation from an existing web API.
727728
type WebAPIEstimator struct {
728-
started sync.Once
729-
stopped sync.Once
729+
started atomic.Bool
730+
stopped atomic.Bool
730731

731732
// apiSource is the backing web API source we'll use for our queries.
732733
apiSource WebAPIFeeSource
@@ -792,6 +793,12 @@ func NewWebAPIEstimator(api WebAPIFeeSource, noCache bool,
792793
func (w *WebAPIEstimator) EstimateFeePerKW(numBlocks uint32) (
793794
SatPerKWeight, error) {
794795

796+
// If the estimator hasn't been started yet, we'll return an error as
797+
// we can't provide a fee estimate.
798+
if !w.started.Load() {
799+
return 0, fmt.Errorf("estimator not started")
800+
}
801+
795802
if numBlocks > MaxBlockTarget {
796803
numBlocks = MaxBlockTarget
797804
} else if numBlocks < minBlockTarget {
@@ -831,49 +838,56 @@ func (w *WebAPIEstimator) EstimateFeePerKW(numBlocks uint32) (
831838
//
832839
// NOTE: This method is part of the Estimator interface.
833840
func (w *WebAPIEstimator) Start() error {
841+
log.Infof("Starting Web API fee estimator...")
842+
843+
// Return an error if it's already been started.
844+
if w.started.Load() {
845+
return fmt.Errorf("web API fee estimator already started")
846+
}
847+
defer w.started.Store(true)
848+
849+
// During startup we'll query the API to initialize the fee map.
850+
w.updateFeeEstimates()
851+
834852
// No update loop is needed when we don't cache.
835853
if w.noCache {
836854
return nil
837855
}
838856

839-
var err error
840-
w.started.Do(func() {
841-
log.Infof("Starting web API fee estimator")
857+
feeUpdateTimeout := w.randomFeeUpdateTimeout()
842858

843-
feeUpdateTimeout := w.randomFeeUpdateTimeout()
859+
log.Infof("Web API fee estimator using update timeout of %v",
860+
feeUpdateTimeout)
844861

845-
log.Infof("Web API fee estimator using update timeout of %v",
846-
feeUpdateTimeout)
847-
848-
w.updateFeeTicker = time.NewTicker(feeUpdateTimeout)
849-
w.updateFeeEstimates()
862+
w.updateFeeTicker = time.NewTicker(feeUpdateTimeout)
850863

851-
w.wg.Add(1)
852-
go w.feeUpdateManager()
864+
w.wg.Add(1)
865+
go w.feeUpdateManager()
853866

854-
})
855-
856-
return err
867+
return nil
857868
}
858869

859870
// Stop stops any spawned goroutines and cleans up the resources used by the
860871
// fee estimator.
861872
//
862873
// NOTE: This method is part of the Estimator interface.
863874
func (w *WebAPIEstimator) Stop() error {
875+
log.Infof("Stopping web API fee estimator")
876+
877+
if w.stopped.Swap(true) {
878+
return fmt.Errorf("web API fee estimator already stopped")
879+
}
880+
864881
// Update loop is not running when we don't cache.
865882
if w.noCache {
866883
return nil
867884
}
868885

869-
w.stopped.Do(func() {
870-
log.Infof("Stopping web API fee estimator")
886+
w.updateFeeTicker.Stop()
871887

872-
w.updateFeeTicker.Stop()
888+
close(w.quit)
889+
w.wg.Wait()
873890

874-
close(w.quit)
875-
w.wg.Wait()
876-
})
877891
return nil
878892
}
879893

@@ -882,6 +896,10 @@ func (w *WebAPIEstimator) Stop() error {
882896
//
883897
// NOTE: This method is part of the Estimator interface.
884898
func (w *WebAPIEstimator) RelayFeePerKW() SatPerKWeight {
899+
if !w.started.Load() {
900+
log.Error("WebAPIEstimator not started")
901+
}
902+
885903
// Get fee estimates now if we don't refresh periodically.
886904
if w.noCache {
887905
w.updateFeeEstimates()

lnwallet/chainfee/estimator_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,12 @@ func TestWebAPIFeeEstimator(t *testing.T) {
252252
feeSource, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
253253
)
254254

255-
// Test that requesting a fee when no fees have been cached won't fail.
255+
// Test that when the estimator is not started, an error is returned.
256256
feeRate, err := estimator.EstimateFeePerKW(5)
257-
require.NoErrorf(t, err, "expected no error")
258-
require.Equalf(t, FeePerKwFloor, feeRate, "expected fee rate floor "+
259-
"returned when no cached fee rate found")
257+
require.Error(t, err, "expected an error")
258+
require.Zero(t, feeRate, "expected zero fee rate")
260259

260+
// Start the estimator.
261261
require.NoError(t, estimator.Start(), "unable to start fee estimator")
262262

263263
for _, tc := range testCases {

0 commit comments

Comments
 (0)