• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 13725358077

07 Mar 2025 04:51PM UTC coverage: 58.224% (-10.4%) from 68.615%
13725358077

Pull #9458

github

web-flow
Merge bf4c6625f into ab2dc09eb
Pull Request #9458: multi+server.go: add initial permissions for some peers

346 of 549 new or added lines in 10 files covered. (63.02%)

27466 existing lines in 443 files now uncovered.

94609 of 162492 relevant lines covered (58.22%)

1.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

29.08
/lnwallet/chainfee/estimator.go
1
package chainfee
2

3
import (
4
        "encoding/json"
5
        "errors"
6
        "fmt"
7
        "io"
8
        "math"
9
        prand "math/rand"
10
        "net"
11
        "net/http"
12
        "sync"
13
        "sync/atomic"
14
        "time"
15

16
        "github.com/btcsuite/btcd/btcutil"
17
        "github.com/btcsuite/btcd/rpcclient"
18
        "github.com/lightningnetwork/lnd/lnutils"
19
)
20

21
const (
22
        // MaxBlockTarget is the highest number of blocks confirmations that
23
        // a WebAPIEstimator will cache fees for. This number is chosen
24
        // because it's the highest number of confs bitcoind will return a fee
25
        // estimate for.
26
        MaxBlockTarget uint32 = 1008
27

28
        // minBlockTarget is the lowest number of blocks confirmations that
29
        // a WebAPIEstimator will cache fees for. Requesting an estimate for
30
        // less than this will result in an error.
31
        minBlockTarget uint32 = 1
32

33
        // WebAPIConnectionTimeout specifies the timeout value for connecting
34
        // to the api source.
35
        WebAPIConnectionTimeout = 5 * time.Second
36

37
        // WebAPIResponseTimeout specifies the timeout value for receiving a
38
        // fee response from the api source.
39
        WebAPIResponseTimeout = 10 * time.Second
40

41
        // economicalFeeMode is a mode that bitcoind uses to serve
42
        // non-conservative fee estimates. These fee estimates are less
43
        // resistant to shocks.
44
        economicalFeeMode = "ECONOMICAL"
45

46
        // filterCapConfTarget is the conf target that will be used to cap our
47
        // minimum feerate if we used the median of our peers' feefilter
48
        // values.
49
        filterCapConfTarget = uint32(1)
50
)
51

52
var (
53
        // errNoFeeRateFound is used when a given conf target cannot be found
54
        // from the fee estimator.
55
        errNoFeeRateFound = errors.New("no fee estimation for block target")
56

57
        // errEmptyCache is used when the fee rate cache is empty.
58
        errEmptyCache = errors.New("fee rate cache is empty")
59
)
60

61
// Estimator provides the ability to estimate on-chain transaction fees for
62
// various combinations of transaction sizes and desired confirmation time
63
// (measured by number of blocks).
64
type Estimator interface {
65
        // EstimateFeePerKW takes in a target for the number of blocks until an
66
        // initial confirmation and returns the estimated fee expressed in
67
        // sat/kw.
68
        EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error)
69

70
        // Start signals the Estimator to start any processes or goroutines
71
        // it needs to perform its duty.
72
        Start() error
73

74
        // Stop stops any spawned goroutines and cleans up the resources used
75
        // by the fee estimator.
76
        Stop() error
77

78
        // RelayFeePerKW returns the minimum fee rate required for transactions
79
        // to be relayed. This is also the basis for calculation of the dust
80
        // limit.
81
        RelayFeePerKW() SatPerKWeight
82
}
83

84
// StaticEstimator will return a static value for all fee calculation requests.
85
// It is designed to be replaced by a proper fee calculation implementation.
86
// The fees are not accessible directly, because changing them would not be
87
// thread safe.
88
type StaticEstimator struct {
89
        // feePerKW is the static fee rate in satoshis-per-vbyte that will be
90
        // returned by this fee estimator.
91
        feePerKW SatPerKWeight
92

93
        // relayFee is the minimum fee rate required for transactions to be
94
        // relayed.
95
        relayFee SatPerKWeight
96
}
97

98
// NewStaticEstimator returns a new static fee estimator instance.
99
func NewStaticEstimator(feePerKW, relayFee SatPerKWeight) *StaticEstimator {
3✔
100
        return &StaticEstimator{
3✔
101
                feePerKW: feePerKW,
3✔
102
                relayFee: relayFee,
3✔
103
        }
3✔
104
}
3✔
105

106
// EstimateFeePerKW will return a static value for fee calculations.
107
//
108
// NOTE: This method is part of the Estimator interface.
UNCOV
109
func (e StaticEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) {
×
UNCOV
110
        return e.feePerKW, nil
×
UNCOV
111
}
×
112

113
// RelayFeePerKW returns the minimum fee rate required for transactions to be
114
// relayed.
115
//
116
// NOTE: This method is part of the Estimator interface.
117
func (e StaticEstimator) RelayFeePerKW() SatPerKWeight {
×
118
        return e.relayFee
×
119
}
×
120

121
// Start signals the Estimator to start any processes or goroutines
122
// it needs to perform its duty.
123
//
124
// NOTE: This method is part of the Estimator interface.
UNCOV
125
func (e StaticEstimator) Start() error {
×
UNCOV
126
        return nil
×
UNCOV
127
}
×
128

129
// Stop stops any spawned goroutines and cleans up the resources used
130
// by the fee estimator.
131
//
132
// NOTE: This method is part of the Estimator interface.
UNCOV
133
func (e StaticEstimator) Stop() error {
×
UNCOV
134
        return nil
×
UNCOV
135
}
×
136

137
// A compile-time assertion to ensure that StaticFeeEstimator implements the
138
// Estimator interface.
139
var _ Estimator = (*StaticEstimator)(nil)
140

141
// BtcdEstimator is an implementation of the Estimator interface backed
142
// by the RPC interface of an active btcd node. This implementation will proxy
143
// any fee estimation requests to btcd's RPC interface.
144
type BtcdEstimator struct {
145
        // fallbackFeePerKW is the fall back fee rate in sat/kw that is returned
146
        // if the fee estimator does not yet have enough data to actually
147
        // produce fee estimates.
148
        fallbackFeePerKW SatPerKWeight
149

150
        // minFeeManager is used to query the current minimum fee, in sat/kw,
151
        // that we should enforce. This will be used to determine fee rate for
152
        // a transaction when the estimated fee rate is too low to allow the
153
        // transaction to propagate through the network.
154
        minFeeManager *minFeeManager
155

156
        btcdConn *rpcclient.Client
157

158
        // filterManager uses our peer's feefilter values to determine a
159
        // suitable feerate to use that will allow successful transaction
160
        // propagation.
161
        filterManager *filterManager
162
}
163

164
// NewBtcdEstimator creates a new BtcdEstimator given a fully populated
165
// rpc config that is able to successfully connect and authenticate with the
166
// btcd node, and also a fall back fee rate. The fallback fee rate is used in
167
// the occasion that the estimator has insufficient data, or returns zero for a
168
// fee estimate.
169
func NewBtcdEstimator(rpcConfig rpcclient.ConnConfig,
170
        fallBackFeeRate SatPerKWeight) (*BtcdEstimator, error) {
×
171

×
172
        rpcConfig.DisableConnectOnNew = true
×
173
        rpcConfig.DisableAutoReconnect = false
×
174
        chainConn, err := rpcclient.New(&rpcConfig, nil)
×
175
        if err != nil {
×
176
                return nil, err
×
177
        }
×
178

179
        fetchCb := func() ([]SatPerKWeight, error) {
×
180
                return fetchBtcdFilters(chainConn)
×
181
        }
×
182

183
        return &BtcdEstimator{
×
184
                fallbackFeePerKW: fallBackFeeRate,
×
185
                btcdConn:         chainConn,
×
186
                filterManager:    newFilterManager(fetchCb),
×
187
        }, nil
×
188
}
189

190
// Start signals the Estimator to start any processes or goroutines
191
// it needs to perform its duty.
192
//
193
// NOTE: This method is part of the Estimator interface.
194
func (b *BtcdEstimator) Start() error {
×
195
        if err := b.btcdConn.Connect(20); err != nil {
×
196
                return err
×
197
        }
×
198

199
        // Once the connection to the backend node has been established, we
200
        // can initialise the minimum relay fee manager which queries the
201
        // chain backend for the minimum relay fee on construction.
202
        minRelayFeeManager, err := newMinFeeManager(
×
203
                defaultUpdateInterval, b.fetchMinRelayFee,
×
204
        )
×
205
        if err != nil {
×
206
                return err
×
207
        }
×
208
        b.minFeeManager = minRelayFeeManager
×
209

×
210
        b.filterManager.Start()
×
211

×
212
        return nil
×
213
}
214

215
// fetchMinRelayFee fetches and returns the minimum relay fee in sat/kb from
216
// the btcd backend.
217
func (b *BtcdEstimator) fetchMinRelayFee() (SatPerKWeight, error) {
×
218
        info, err := b.btcdConn.GetInfo()
×
219
        if err != nil {
×
220
                return 0, err
×
221
        }
×
222

223
        relayFee, err := btcutil.NewAmount(info.RelayFee)
×
224
        if err != nil {
×
225
                return 0, err
×
226
        }
×
227

228
        // The fee rate is expressed in sat/kb, so we'll manually convert it to
229
        // our desired sat/kw rate.
230
        return SatPerKVByte(relayFee).FeePerKWeight(), nil
×
231
}
232

233
// Stop stops any spawned goroutines and cleans up the resources used
234
// by the fee estimator.
235
//
236
// NOTE: This method is part of the Estimator interface.
237
func (b *BtcdEstimator) Stop() error {
×
238
        b.filterManager.Stop()
×
239

×
240
        b.btcdConn.Shutdown()
×
241
        b.btcdConn.WaitForShutdown()
×
242

×
243
        return nil
×
244
}
×
245

246
// EstimateFeePerKW takes in a target for the number of blocks until an initial
247
// confirmation and returns the estimated fee expressed in sat/kw.
248
//
249
// NOTE: This method is part of the Estimator interface.
250
func (b *BtcdEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) {
×
251
        feeEstimate, err := b.fetchEstimate(numBlocks)
×
252
        switch {
×
253
        // If the estimator doesn't have enough data, or returns an error, then
254
        // to return a proper value, then we'll return the default fall back
255
        // fee rate.
256
        case err != nil:
×
257
                log.Errorf("unable to query estimator: %v", err)
×
258
                fallthrough
×
259

260
        case feeEstimate == 0:
×
261
                return b.fallbackFeePerKW, nil
×
262
        }
263

264
        return feeEstimate, nil
×
265
}
266

267
// RelayFeePerKW returns the minimum fee rate required for transactions to be
268
// relayed.
269
//
270
// NOTE: This method is part of the Estimator interface.
271
func (b *BtcdEstimator) RelayFeePerKW() SatPerKWeight {
×
272
        // Get a suitable minimum feerate to use. This may optionally use the
×
273
        // median of our peers' feefilter values.
×
274
        feeCapClosure := func() (SatPerKWeight, error) {
×
275
                return b.fetchEstimateInner(filterCapConfTarget)
×
276
        }
×
277

278
        return chooseMinFee(
×
279
                b.minFeeManager.fetchMinFee, b.filterManager.FetchMedianFilter,
×
280
                feeCapClosure,
×
281
        )
×
282
}
283

284
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
285
// confTarget blocks. The estimate is returned in sat/kw.
286
func (b *BtcdEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
×
287
        satPerKw, err := b.fetchEstimateInner(confTarget)
×
288
        if err != nil {
×
289
                return 0, err
×
290
        }
×
291

292
        // Finally, we'll enforce our fee floor by choosing the higher of the
293
        // minimum relay fee and the feerate returned by the filterManager.
294
        absoluteMinFee := b.RelayFeePerKW()
×
295

×
296
        if satPerKw < absoluteMinFee {
×
297
                log.Debugf("Estimated fee rate of %v sat/kw is too low, "+
×
298
                        "using fee floor of %v sat/kw instead", satPerKw,
×
299
                        absoluteMinFee)
×
300

×
301
                satPerKw = absoluteMinFee
×
302
        }
×
303

304
        log.Debugf("Returning %v sat/kw for conf target of %v",
×
305
                int64(satPerKw), confTarget)
×
306

×
307
        return satPerKw, nil
×
308
}
309

310
func (b *BtcdEstimator) fetchEstimateInner(confTarget uint32) (SatPerKWeight,
311
        error) {
×
312

×
313
        // First, we'll fetch the estimate for our confirmation target.
×
314
        btcPerKB, err := b.btcdConn.EstimateFee(int64(confTarget))
×
315
        if err != nil {
×
316
                return 0, err
×
317
        }
×
318

319
        // Next, we'll convert the returned value to satoshis, as it's
320
        // currently returned in BTC.
321
        satPerKB, err := btcutil.NewAmount(btcPerKB)
×
322
        if err != nil {
×
323
                return 0, err
×
324
        }
×
325

326
        // Since we use fee rates in sat/kw internally, we'll convert the
327
        // estimated fee rate from its sat/kb representation to sat/kw.
328
        return SatPerKVByte(satPerKB).FeePerKWeight(), nil
×
329
}
330

331
// A compile-time assertion to ensure that BtcdEstimator implements the
332
// Estimator interface.
333
var _ Estimator = (*BtcdEstimator)(nil)
334

335
// BitcoindEstimator is an implementation of the Estimator interface backed by
336
// the RPC interface of an active bitcoind node. This implementation will proxy
337
// any fee estimation requests to bitcoind's RPC interface.
338
type BitcoindEstimator struct {
339
        // fallbackFeePerKW is the fallback fee rate in sat/kw that is returned
340
        // if the fee estimator does not yet have enough data to actually
341
        // produce fee estimates.
342
        fallbackFeePerKW SatPerKWeight
343

344
        // minFeeManager is used to keep track of the minimum fee, in sat/kw,
345
        // that we should enforce. This will be used as the default fee rate
346
        // for a transaction when the estimated fee rate is too low to allow
347
        // the transaction to propagate through the network.
348
        minFeeManager *minFeeManager
349

350
        // feeMode is the estimate_mode to use when calling "estimatesmartfee".
351
        // It can be either "ECONOMICAL" or "CONSERVATIVE", and it's default
352
        // to "CONSERVATIVE".
353
        feeMode string
354

355
        // TODO(ziggie): introduce an interface for the client to enhance
356
        // testability of the estimator.
357
        bitcoindConn *rpcclient.Client
358

359
        // filterManager uses our peer's feefilter values to determine a
360
        // suitable feerate to use that will allow successful transaction
361
        // propagation.
362
        filterManager *filterManager
363
}
364

365
// NewBitcoindEstimator creates a new BitcoindEstimator given a fully populated
366
// rpc config that is able to successfully connect and authenticate with the
367
// bitcoind node, and also a fall back fee rate. The fallback fee rate is used
368
// in the occasion that the estimator has insufficient data, or returns zero
369
// for a fee estimate.
370
func NewBitcoindEstimator(rpcConfig rpcclient.ConnConfig, feeMode string,
371
        fallBackFeeRate SatPerKWeight) (*BitcoindEstimator, error) {
×
372

×
373
        rpcConfig.DisableConnectOnNew = true
×
374
        rpcConfig.DisableAutoReconnect = false
×
375
        rpcConfig.DisableTLS = true
×
376
        rpcConfig.HTTPPostMode = true
×
377
        chainConn, err := rpcclient.New(&rpcConfig, nil)
×
378
        if err != nil {
×
379
                return nil, err
×
380
        }
×
381

382
        fetchCb := func() ([]SatPerKWeight, error) {
×
383
                return fetchBitcoindFilters(chainConn)
×
384
        }
×
385

386
        return &BitcoindEstimator{
×
387
                fallbackFeePerKW: fallBackFeeRate,
×
388
                bitcoindConn:     chainConn,
×
389
                feeMode:          feeMode,
×
390
                filterManager:    newFilterManager(fetchCb),
×
391
        }, nil
×
392
}
393

394
// Start signals the Estimator to start any processes or goroutines
395
// it needs to perform its duty.
396
//
397
// NOTE: This method is part of the Estimator interface.
398
func (b *BitcoindEstimator) Start() error {
×
399
        // Once the connection to the backend node has been established, we'll
×
400
        // initialise the minimum relay fee manager which will query
×
401
        // the backend node for its minimum mempool fee.
×
402
        relayFeeManager, err := newMinFeeManager(
×
403
                defaultUpdateInterval,
×
404
                b.fetchMinMempoolFee,
×
405
        )
×
406
        if err != nil {
×
407
                return err
×
408
        }
×
409
        b.minFeeManager = relayFeeManager
×
410

×
411
        b.filterManager.Start()
×
412

×
413
        return nil
×
414
}
415

416
// fetchMinMempoolFee is used to fetch the minimum fee that the backend node
417
// requires for a tx to enter its mempool. The returned fee will be the
418
// maximum of the minimum relay fee and the minimum mempool fee.
419
func (b *BitcoindEstimator) fetchMinMempoolFee() (SatPerKWeight, error) {
×
420
        resp, err := b.bitcoindConn.RawRequest("getmempoolinfo", nil)
×
421
        if err != nil {
×
422
                return 0, err
×
423
        }
×
424

425
        // Parse the response to retrieve the min mempool fee in sat/KB.
426
        // mempoolminfee is the maximum of minrelaytxfee and
427
        // minimum mempool fee
428
        info := struct {
×
429
                MempoolMinFee float64 `json:"mempoolminfee"`
×
430
        }{}
×
431
        if err := json.Unmarshal(resp, &info); err != nil {
×
432
                return 0, err
×
433
        }
×
434

435
        minMempoolFee, err := btcutil.NewAmount(info.MempoolMinFee)
×
436
        if err != nil {
×
437
                return 0, err
×
438
        }
×
439

440
        // The fee rate is expressed in sat/kb, so we'll manually convert it to
441
        // our desired sat/kw rate.
442
        return SatPerKVByte(minMempoolFee).FeePerKWeight(), nil
×
443
}
444

445
// Stop stops any spawned goroutines and cleans up the resources used
446
// by the fee estimator.
447
//
448
// NOTE: This method is part of the Estimator interface.
449
func (b *BitcoindEstimator) Stop() error {
×
450
        b.filterManager.Stop()
×
451
        return nil
×
452
}
×
453

454
// EstimateFeePerKW takes in a target for the number of blocks until an initial
455
// confirmation and returns the estimated fee expressed in sat/kw.
456
//
457
// NOTE: This method is part of the Estimator interface.
458
func (b *BitcoindEstimator) EstimateFeePerKW(
459
        numBlocks uint32) (SatPerKWeight, error) {
×
460

×
461
        if numBlocks > MaxBlockTarget {
×
462
                log.Debugf("conf target %d exceeds the max value, "+
×
463
                        "use %d instead.", numBlocks, MaxBlockTarget,
×
464
                )
×
465
                numBlocks = MaxBlockTarget
×
466
        }
×
467

468
        feeEstimate, err := b.fetchEstimate(numBlocks, b.feeMode)
×
469
        switch {
×
470
        // If the estimator doesn't have enough data, or returns an error, then
471
        // to return a proper value, then we'll return the default fall back
472
        // fee rate.
473
        case err != nil:
×
474
                log.Errorf("unable to query estimator: %v", err)
×
475
                fallthrough
×
476

477
        case feeEstimate == 0:
×
478
                return b.fallbackFeePerKW, nil
×
479
        }
480

481
        return feeEstimate, nil
×
482
}
483

484
// RelayFeePerKW returns the minimum fee rate required for transactions to be
485
// relayed.
486
//
487
// NOTE: This method is part of the Estimator interface.
488
func (b *BitcoindEstimator) RelayFeePerKW() SatPerKWeight {
×
489
        // Get a suitable minimum feerate to use. This may optionally use the
×
490
        // median of our peers' feefilter values.
×
491
        feeCapClosure := func() (SatPerKWeight, error) {
×
492
                return b.fetchEstimateInner(
×
493
                        filterCapConfTarget, economicalFeeMode,
×
494
                )
×
495
        }
×
496

497
        return chooseMinFee(
×
498
                b.minFeeManager.fetchMinFee, b.filterManager.FetchMedianFilter,
×
499
                feeCapClosure,
×
500
        )
×
501
}
502

503
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
504
// confTarget blocks. The estimate is returned in sat/kw.
505
func (b *BitcoindEstimator) fetchEstimate(confTarget uint32, feeMode string) (
506
        SatPerKWeight, error) {
×
507

×
508
        satPerKw, err := b.fetchEstimateInner(confTarget, feeMode)
×
509
        if err != nil {
×
510
                return 0, err
×
511
        }
×
512

513
        // Finally, we'll enforce our fee floor by choosing the higher of the
514
        // minimum relay fee and the feerate returned by the filterManager.
515
        absoluteMinFee := b.RelayFeePerKW()
×
516

×
517
        if satPerKw < absoluteMinFee {
×
518
                log.Debugf("Estimated fee rate of %v sat/kw is too low, "+
×
519
                        "using fee floor of %v sat/kw instead", satPerKw,
×
520
                        absoluteMinFee)
×
521

×
522
                satPerKw = absoluteMinFee
×
523
        }
×
524

525
        log.Debugf("Returning %v sat/kw for conf target of %v",
×
526
                int64(satPerKw), confTarget)
×
527

×
528
        return satPerKw, nil
×
529
}
530

531
func (b *BitcoindEstimator) fetchEstimateInner(confTarget uint32,
532
        feeMode string) (SatPerKWeight, error) {
×
533

×
534
        // First, we'll send an "estimatesmartfee" command as a raw request,
×
535
        // since it isn't supported by btcd but is available in bitcoind.
×
536
        target, err := json.Marshal(uint64(confTarget))
×
537
        if err != nil {
×
538
                return 0, err
×
539
        }
×
540

541
        // The mode must be either ECONOMICAL or CONSERVATIVE.
542
        mode, err := json.Marshal(feeMode)
×
543
        if err != nil {
×
544
                return 0, err
×
545
        }
×
546

547
        resp, err := b.bitcoindConn.RawRequest(
×
548
                "estimatesmartfee", []json.RawMessage{target, mode},
×
549
        )
×
550
        if err != nil {
×
551
                return 0, err
×
552
        }
×
553

554
        // Next, we'll parse the response to get the BTC per KB.
555
        feeEstimate := struct {
×
556
                FeeRate float64 `json:"feerate"`
×
557
        }{}
×
558
        err = json.Unmarshal(resp, &feeEstimate)
×
559
        if err != nil {
×
560
                return 0, err
×
561
        }
×
562

563
        // Next, we'll convert the returned value to satoshis, as it's currently
564
        // returned in BTC.
565
        satPerKB, err := btcutil.NewAmount(feeEstimate.FeeRate)
×
566
        if err != nil {
×
567
                return 0, err
×
568
        }
×
569

570
        // Bitcoind will not report any fee estimation if it has not enough
571
        // data available hence the fee will remain zero. We return an error
572
        // here to make sure that we do not use the min relay fee instead.
573
        if satPerKB == 0 {
×
574
                return 0, fmt.Errorf("fee estimation data not available yet")
×
575
        }
×
576

577
        // Since we use fee rates in sat/kw internally, we'll convert the
578
        // estimated fee rate from its sat/kb representation to sat/kw.
579
        return SatPerKVByte(satPerKB).FeePerKWeight(), nil
×
580
}
581

582
// chooseMinFee takes the minimum relay fee and the median of our peers'
583
// feefilter values and takes the higher of the two. It then compares the value
584
// against a maximum fee and caps it if the value is higher than the maximum
585
// fee. This function is only called if we have data for our peers' feefilter.
586
// The returned value will be used as the fee floor for calls to
587
// RelayFeePerKW.
588
func chooseMinFee(minRelayFeeFunc func() SatPerKWeight,
589
        medianFilterFunc func() (SatPerKWeight, error),
590
        feeCapFunc func() (SatPerKWeight, error)) SatPerKWeight {
×
591

×
592
        minRelayFee := minRelayFeeFunc()
×
593

×
594
        medianFilter, err := medianFilterFunc()
×
595
        if err != nil {
×
596
                // If we don't have feefilter data, we fallback to using our
×
597
                // minimum relay fee.
×
598
                return minRelayFee
×
599
        }
×
600

601
        feeCap, err := feeCapFunc()
×
602
        if err != nil {
×
603
                // If we encountered an error, don't use the medianFilter and
×
604
                // instead fallback to using our minimum relay fee.
×
605
                return minRelayFee
×
606
        }
×
607

608
        // If the median feefilter is higher than our minimum relay fee, use it
609
        // instead.
610
        if medianFilter > minRelayFee {
×
611
                // Only apply the cap if the median filter was used. This is
×
612
                // to prevent an adversary from taking up the majority of our
×
613
                // outbound peer slots and forcing us to use a high median
×
614
                // filter value.
×
615
                if medianFilter > feeCap {
×
616
                        return feeCap
×
617
                }
×
618

619
                return medianFilter
×
620
        }
621

622
        return minRelayFee
×
623
}
624

625
// A compile-time assertion to ensure that BitcoindEstimator implements the
626
// Estimator interface.
627
var _ Estimator = (*BitcoindEstimator)(nil)
628

629
// WebAPIFeeSource is an interface allows the WebAPIEstimator to query an
630
// arbitrary HTTP-based fee estimator. Each new set/network will gain an
631
// implementation of this interface in order to allow the WebAPIEstimator to
632
// be fully generic in its logic.
633
type WebAPIFeeSource interface {
634
        // GetFeeInfo will query the web API, parse the response into a
635
        // WebAPIResponse which contains a map of confirmation targets to
636
        // sat/kw fees and min relay feerate.
637
        GetFeeInfo() (WebAPIResponse, error)
638
}
639

640
// SparseConfFeeSource is an implementation of the WebAPIFeeSource that utilizes
641
// a user-specified fee estimation API for Bitcoin. It expects the response
642
// to be in the JSON format: `fee_by_block_target: { ... }` where the value maps
643
// block targets to fee estimates (in sat per kilovbyte).
644
type SparseConfFeeSource struct {
645
        // URL is the fee estimation API specified by the user.
646
        URL string
647
}
648

649
// WebAPIResponse is the response returned by the fee estimation API.
650
type WebAPIResponse struct {
651
        // FeeByBlockTarget is a map of confirmation targets to sat/kvb fees.
652
        FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"`
653

654
        // MinRelayFeerate is the minimum relay fee in sat/kvb.
655
        MinRelayFeerate SatPerKVByte `json:"min_relay_feerate"`
656
}
657

658
// parseResponse attempts to parse the body of the response generated by the
659
// above query URL. Typically this will be JSON, but the specifics are left to
660
// the WebAPIFeeSource implementation.
661
func (s SparseConfFeeSource) parseResponse(r io.Reader) (
662
        WebAPIResponse, error) {
3✔
663

3✔
664
        resp := WebAPIResponse{
3✔
665
                FeeByBlockTarget: make(map[uint32]uint32),
3✔
666
                MinRelayFeerate:  0,
3✔
667
        }
3✔
668
        jsonReader := json.NewDecoder(r)
3✔
669
        if err := jsonReader.Decode(&resp); err != nil {
3✔
UNCOV
670
                return WebAPIResponse{}, err
×
UNCOV
671
        }
×
672

673
        if resp.MinRelayFeerate == 0 {
3✔
UNCOV
674
                log.Errorf("No min relay fee rate available, using default %v",
×
UNCOV
675
                        FeePerKwFloor)
×
UNCOV
676
                resp.MinRelayFeerate = FeePerKwFloor.FeePerKVByte()
×
UNCOV
677
        }
×
678

679
        return resp, nil
3✔
680
}
681

682
// GetFeeInfo will query the web API, parse the response and return a map of
683
// confirmation targets to sat/kw fees and min relay feerate in a parsed
684
// response.
685
func (s SparseConfFeeSource) GetFeeInfo() (WebAPIResponse, error) {
3✔
686
        // Rather than use the default http.Client, we'll make a custom one
3✔
687
        // which will allow us to control how long we'll wait to read the
3✔
688
        // response from the service. This way, if the service is down or
3✔
689
        // overloaded, we can exit early and use our default fee.
3✔
690
        netTransport := &http.Transport{
3✔
691
                Dial: (&net.Dialer{
3✔
692
                        Timeout: WebAPIConnectionTimeout,
3✔
693
                }).Dial,
3✔
694
                TLSHandshakeTimeout: WebAPIConnectionTimeout,
3✔
695
        }
3✔
696
        netClient := &http.Client{
3✔
697
                Timeout:   WebAPIResponseTimeout,
3✔
698
                Transport: netTransport,
3✔
699
        }
3✔
700

3✔
701
        // With the client created, we'll query the API source to fetch the URL
3✔
702
        // that we should use to query for the fee estimation.
3✔
703
        targetURL := s.URL
3✔
704
        resp, err := netClient.Get(targetURL)
3✔
705
        if err != nil {
3✔
706
                log.Errorf("unable to query web api for fee response: %v",
×
707
                        err)
×
708
                return WebAPIResponse{}, err
×
709
        }
×
710
        defer resp.Body.Close()
3✔
711

3✔
712
        // Once we've obtained the response, we'll instruct the WebAPIFeeSource
3✔
713
        // to parse out the body to obtain our final result.
3✔
714
        parsedResp, err := s.parseResponse(resp.Body)
3✔
715
        if err != nil {
3✔
716
                log.Errorf("unable to parse fee api response: %v", err)
×
717

×
718
                return WebAPIResponse{}, err
×
719
        }
×
720

721
        return parsedResp, nil
3✔
722
}
723

724
// A compile-time assertion to ensure that SparseConfFeeSource implements the
725
// WebAPIFeeSource interface.
726
var _ WebAPIFeeSource = (*SparseConfFeeSource)(nil)
727

728
// WebAPIEstimator is an implementation of the Estimator interface that
729
// queries an HTTP-based fee estimation from an existing web API.
730
type WebAPIEstimator struct {
731
        started atomic.Bool
732
        stopped atomic.Bool
733

734
        // apiSource is the backing web API source we'll use for our queries.
735
        apiSource WebAPIFeeSource
736

737
        // updateFeeTicker is the ticker responsible for updating the Estimator's
738
        // fee estimates every time it fires.
739
        updateFeeTicker *time.Ticker
740

741
        // feeByBlockTarget is our cache for fees pulled from the API. When a
742
        // fee estimate request comes in, we pull the estimate from this array
743
        // rather than re-querying the API, to prevent an inadvertent DoS attack.
744
        feesMtx          sync.Mutex
745
        feeByBlockTarget map[uint32]uint32
746
        minRelayFeerate  SatPerKVByte
747

748
        // noCache determines whether the web estimator should cache fee
749
        // estimates.
750
        noCache bool
751

752
        // minFeeUpdateTimeout represents the minimum interval in which the
753
        // web estimator will request fresh fees from its API.
754
        minFeeUpdateTimeout time.Duration
755

756
        // minFeeUpdateTimeout represents the maximum interval in which the
757
        // web estimator will request fresh fees from its API.
758
        maxFeeUpdateTimeout time.Duration
759

760
        quit chan struct{}
761
        wg   sync.WaitGroup
762
}
763

764
// NewWebAPIEstimator creates a new WebAPIEstimator from a given URL and a
765
// fallback default fee. The fees are updated whenever a new block is mined.
766
func NewWebAPIEstimator(api WebAPIFeeSource, noCache bool,
767
        minFeeUpdateTimeout time.Duration,
768
        maxFeeUpdateTimeout time.Duration) (*WebAPIEstimator, error) {
3✔
769

3✔
770
        if minFeeUpdateTimeout == 0 || maxFeeUpdateTimeout == 0 {
3✔
771
                return nil, fmt.Errorf("minFeeUpdateTimeout and " +
×
772
                        "maxFeeUpdateTimeout must be greater than 0")
×
773
        }
×
774

775
        if minFeeUpdateTimeout >= maxFeeUpdateTimeout {
3✔
UNCOV
776
                return nil, fmt.Errorf("minFeeUpdateTimeout target of %v "+
×
UNCOV
777
                        "cannot be greater than maxFeeUpdateTimeout of %v",
×
UNCOV
778
                        minFeeUpdateTimeout, maxFeeUpdateTimeout)
×
UNCOV
779
        }
×
780

781
        return &WebAPIEstimator{
3✔
782
                apiSource:           api,
3✔
783
                feeByBlockTarget:    make(map[uint32]uint32),
3✔
784
                noCache:             noCache,
3✔
785
                quit:                make(chan struct{}),
3✔
786
                minFeeUpdateTimeout: minFeeUpdateTimeout,
3✔
787
                maxFeeUpdateTimeout: maxFeeUpdateTimeout,
3✔
788
        }, nil
3✔
789
}
790

791
// EstimateFeePerKW takes in a target for the number of blocks until an initial
792
// confirmation and returns the estimated fee expressed in sat/kw.
793
//
794
// NOTE: This method is part of the Estimator interface.
795
func (w *WebAPIEstimator) EstimateFeePerKW(numBlocks uint32) (
796
        SatPerKWeight, error) {
3✔
797

3✔
798
        // If the estimator hasn't been started yet, we'll return an error as
3✔
799
        // we can't provide a fee estimate.
3✔
800
        if !w.started.Load() {
3✔
UNCOV
801
                return 0, fmt.Errorf("estimator not started")
×
UNCOV
802
        }
×
803

804
        if numBlocks > MaxBlockTarget {
3✔
805
                numBlocks = MaxBlockTarget
×
806
        } else if numBlocks < minBlockTarget {
3✔
UNCOV
807
                return 0, fmt.Errorf("conf target of %v is too low, minimum "+
×
UNCOV
808
                        "accepted is %v", numBlocks, minBlockTarget)
×
UNCOV
809
        }
×
810

811
        // Get fee estimates now if we don't refresh periodically.
812
        if w.noCache {
6✔
813
                w.updateFeeEstimates()
3✔
814
        }
3✔
815

816
        feePerKb, err := w.getCachedFee(numBlocks)
3✔
817

3✔
818
        // If the estimator returns an error, a zero value fee rate will be
3✔
819
        // returned. We will log the error and return the fall back fee rate
3✔
820
        // instead.
3✔
821
        if err != nil {
3✔
822
                log.Errorf("Unable to query estimator: %v", err)
×
823
        }
×
824

825
        // If the result is too low, then we'll clamp it to our current fee
826
        // floor.
827
        satPerKw := SatPerKVByte(feePerKb).FeePerKWeight()
3✔
828
        if satPerKw < FeePerKwFloor {
6✔
829
                satPerKw = FeePerKwFloor
3✔
830
        }
3✔
831

832
        log.Debugf("Web API returning %v sat/kw for conf target of %v",
3✔
833
                int64(satPerKw), numBlocks)
3✔
834

3✔
835
        return satPerKw, nil
3✔
836
}
837

838
// Start signals the Estimator to start any processes or goroutines it needs
839
// to perform its duty.
840
//
841
// NOTE: This method is part of the Estimator interface.
842
func (w *WebAPIEstimator) Start() error {
3✔
843
        log.Infof("Starting Web API fee estimator...")
3✔
844

3✔
845
        // Return an error if it's already been started.
3✔
846
        if w.started.Load() {
3✔
847
                return fmt.Errorf("web API fee estimator already started")
×
848
        }
×
849
        defer w.started.Store(true)
3✔
850

3✔
851
        // During startup we'll query the API to initialize the fee map.
3✔
852
        w.updateFeeEstimates()
3✔
853

3✔
854
        // No update loop is needed when we don't cache.
3✔
855
        if w.noCache {
6✔
856
                return nil
3✔
857
        }
3✔
858

UNCOV
859
        feeUpdateTimeout := w.randomFeeUpdateTimeout()
×
UNCOV
860

×
UNCOV
861
        log.Infof("Web API fee estimator using update timeout of %v",
×
UNCOV
862
                feeUpdateTimeout)
×
UNCOV
863

×
UNCOV
864
        w.updateFeeTicker = time.NewTicker(feeUpdateTimeout)
×
UNCOV
865

×
UNCOV
866
        w.wg.Add(1)
×
UNCOV
867
        go w.feeUpdateManager()
×
UNCOV
868

×
UNCOV
869
        return nil
×
870
}
871

872
// Stop stops any spawned goroutines and cleans up the resources used by the
873
// fee estimator.
874
//
875
// NOTE: This method is part of the Estimator interface.
876
func (w *WebAPIEstimator) Stop() error {
3✔
877
        log.Infof("Stopping web API fee estimator")
3✔
878

3✔
879
        if w.stopped.Swap(true) {
3✔
880
                return fmt.Errorf("web API fee estimator already stopped")
×
881
        }
×
882

883
        // Update loop is not running when we don't cache.
884
        if w.noCache {
6✔
885
                return nil
3✔
886
        }
3✔
887

UNCOV
888
        if w.updateFeeTicker != nil {
×
UNCOV
889
                w.updateFeeTicker.Stop()
×
UNCOV
890
        }
×
891

UNCOV
892
        close(w.quit)
×
UNCOV
893
        w.wg.Wait()
×
UNCOV
894

×
UNCOV
895
        return nil
×
896
}
897

898
// RelayFeePerKW returns the minimum fee rate required for transactions to be
899
// relayed.
900
//
901
// NOTE: This method is part of the Estimator interface.
902
func (w *WebAPIEstimator) RelayFeePerKW() SatPerKWeight {
3✔
903
        if !w.started.Load() {
3✔
904
                log.Error("WebAPIEstimator not started")
×
905
        }
×
906

907
        // Get fee estimates now if we don't refresh periodically.
908
        if w.noCache {
6✔
909
                w.updateFeeEstimates()
3✔
910
        }
3✔
911

912
        log.Infof("Web API returning %v for min relay feerate",
3✔
913
                w.minRelayFeerate)
3✔
914

3✔
915
        return w.minRelayFeerate.FeePerKWeight()
3✔
916
}
917

918
// randomFeeUpdateTimeout returns a random timeout between minFeeUpdateTimeout
919
// and maxFeeUpdateTimeout that will be used to determine how often the Estimator
920
// should retrieve fresh fees from its API.
UNCOV
921
func (w *WebAPIEstimator) randomFeeUpdateTimeout() time.Duration {
×
UNCOV
922
        lower := int64(w.minFeeUpdateTimeout)
×
UNCOV
923
        upper := int64(w.maxFeeUpdateTimeout)
×
UNCOV
924
        return time.Duration(
×
UNCOV
925
                prand.Int63n(upper-lower) + lower, //nolint:gosec
×
UNCOV
926
        ).Round(time.Second)
×
UNCOV
927
}
×
928

929
// getCachedFee takes a conf target and returns the cached fee rate. When the
930
// fee rate cannot be found, it will search the cache by decrementing the conf
931
// target until a fee rate is found. If still not found, it will return the fee
932
// rate of the minimum conf target cached, in other words, the most expensive
933
// fee rate it knows of.
934
func (w *WebAPIEstimator) getCachedFee(numBlocks uint32) (uint32, error) {
3✔
935
        w.feesMtx.Lock()
3✔
936
        defer w.feesMtx.Unlock()
3✔
937

3✔
938
        // If the cache is empty, return an error.
3✔
939
        if len(w.feeByBlockTarget) == 0 {
3✔
UNCOV
940
                return 0, fmt.Errorf("web API error: %w", errEmptyCache)
×
UNCOV
941
        }
×
942

943
        // Search the conf target from the cache. We expect a query to the web
944
        // API has been made and the result has been cached at this point.
945
        fee, ok := w.feeByBlockTarget[numBlocks]
3✔
946

3✔
947
        // If the conf target can be found, exit early.
3✔
948
        if ok {
6✔
949
                return fee, nil
3✔
950
        }
3✔
951

952
        // The conf target cannot be found. We will first search the cache
953
        // using a lower conf target. This is a conservative approach as the
954
        // fee rate returned will be larger than what's requested.
955
        for target := numBlocks; target >= minBlockTarget; target-- {
6✔
956
                fee, ok := w.feeByBlockTarget[target]
3✔
957
                if !ok {
6✔
958
                        continue
3✔
959
                }
960

961
                log.Warnf("Web API does not have a fee rate for target=%d, "+
3✔
962
                        "using the fee rate for target=%d instead",
3✔
963
                        numBlocks, target)
3✔
964

3✔
965
                // Return the fee rate found, which will be more expensive than
3✔
966
                // requested. We will not cache the fee rate here in the hope
3✔
967
                // that the web API will later populate this value.
3✔
968
                return fee, nil
3✔
969
        }
970

971
        // There are no lower conf targets cached, which is likely when the
972
        // requested conf target is 1. We will search the cache using a higher
973
        // conf target, which gives a fee rate that's cheaper than requested.
974
        //
975
        // NOTE: we can only get here iff the requested conf target is smaller
976
        // than the minimum conf target cached, so we return the minimum conf
977
        // target from the cache.
UNCOV
978
        minTargetCached := uint32(math.MaxUint32)
×
UNCOV
979
        for target := range w.feeByBlockTarget {
×
UNCOV
980
                if target < minTargetCached {
×
UNCOV
981
                        minTargetCached = target
×
UNCOV
982
                }
×
983
        }
984

UNCOV
985
        fee, ok = w.feeByBlockTarget[minTargetCached]
×
UNCOV
986
        if !ok {
×
987
                // We should never get here, just a vanity check.
×
988
                return 0, fmt.Errorf("web API error: %w, conf target: %d",
×
989
                        errNoFeeRateFound, numBlocks)
×
990
        }
×
991

992
        // Log an error instead of a warning as a cheaper fee rate may delay
993
        // the confirmation for some important transactions.
UNCOV
994
        log.Errorf("Web API does not have a fee rate for target=%d, "+
×
UNCOV
995
                "using the fee rate for target=%d instead",
×
UNCOV
996
                numBlocks, minTargetCached)
×
UNCOV
997

×
UNCOV
998
        return fee, nil
×
999
}
1000

1001
// updateFeeEstimates re-queries the API for fresh fees and caches them.
1002
func (w *WebAPIEstimator) updateFeeEstimates() {
3✔
1003
        // Once we've obtained the response, we'll instruct the WebAPIFeeSource
3✔
1004
        // to parse out the body to obtain our final result.
3✔
1005
        resp, err := w.apiSource.GetFeeInfo()
3✔
1006
        if err != nil {
3✔
1007
                log.Errorf("unable to get fee response: %v", err)
×
1008
                return
×
1009
        }
×
1010

1011
        log.Debugf("Received response from source: %s", lnutils.NewLogClosure(
3✔
1012
                func() string {
6✔
1013
                        resp, _ := json.Marshal(resp)
3✔
1014
                        return string(resp)
3✔
1015
                }))
3✔
1016

1017
        w.feesMtx.Lock()
3✔
1018
        w.feeByBlockTarget = resp.FeeByBlockTarget
3✔
1019
        w.minRelayFeerate = resp.MinRelayFeerate
3✔
1020
        w.feesMtx.Unlock()
3✔
1021
}
1022

1023
// feeUpdateManager updates the fee estimates whenever a new block comes in.
UNCOV
1024
func (w *WebAPIEstimator) feeUpdateManager() {
×
UNCOV
1025
        defer w.wg.Done()
×
UNCOV
1026

×
UNCOV
1027
        for {
×
UNCOV
1028
                select {
×
1029
                case <-w.updateFeeTicker.C:
×
1030
                        w.updateFeeEstimates()
×
UNCOV
1031
                case <-w.quit:
×
UNCOV
1032
                        return
×
1033
                }
1034
        }
1035
}
1036

1037
// A compile-time assertion to ensure that WebAPIEstimator implements the
1038
// Estimator interface.
1039
var _ Estimator = (*WebAPIEstimator)(nil)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc