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

lightningnetwork / lnd / 12986279612

27 Jan 2025 09:51AM UTC coverage: 57.652% (-1.1%) from 58.788%
12986279612

Pull #9447

github

yyforyongyu
sweep: rename methods for clarity

We now rename "third party" to "unknown" as the inputs can be spent via
an older sweeping tx, a third party (anchor), or a remote party (pin).
In fee bumper we don't have the info to distinguish the above cases, and
leave them to be further handled by the sweeper as it has more context.
Pull Request #9447: sweep: start tracking input spending status in the fee bumper

83 of 87 new or added lines in 2 files covered. (95.4%)

19578 existing lines in 256 files now uncovered.

103448 of 179434 relevant lines covered (57.65%)

24884.58 hits per line

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

33.4
/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 {
402✔
100
        return &StaticEstimator{
402✔
101
                feePerKW: feePerKW,
402✔
102
                relayFee: relayFee,
402✔
103
        }
402✔
104
}
402✔
105

106
// EstimateFeePerKW will return a static value for fee calculations.
107
//
108
// NOTE: This method is part of the Estimator interface.
109
func (e StaticEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) {
407✔
110
        return e.feePerKW, nil
407✔
111
}
407✔
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.
125
func (e StaticEstimator) Start() error {
1✔
126
        return nil
1✔
127
}
1✔
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.
133
func (e StaticEstimator) Stop() error {
1✔
134
        return nil
1✔
135
}
1✔
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 {
4✔
670
                return WebAPIResponse{}, err
1✔
671
        }
1✔
672

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

679
        return resp, nil
2✔
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.
UNCOV
685
func (s SparseConfFeeSource) GetFeeInfo() (WebAPIResponse, error) {
×
UNCOV
686
        // Rather than use the default http.Client, we'll make a custom one
×
UNCOV
687
        // which will allow us to control how long we'll wait to read the
×
UNCOV
688
        // response from the service. This way, if the service is down or
×
UNCOV
689
        // overloaded, we can exit early and use our default fee.
×
UNCOV
690
        netTransport := &http.Transport{
×
UNCOV
691
                Dial: (&net.Dialer{
×
UNCOV
692
                        Timeout: WebAPIConnectionTimeout,
×
UNCOV
693
                }).Dial,
×
UNCOV
694
                TLSHandshakeTimeout: WebAPIConnectionTimeout,
×
UNCOV
695
        }
×
UNCOV
696
        netClient := &http.Client{
×
UNCOV
697
                Timeout:   WebAPIResponseTimeout,
×
UNCOV
698
                Transport: netTransport,
×
UNCOV
699
        }
×
UNCOV
700

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

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

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

UNCOV
721
        return parsedResp, nil
×
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) {
4✔
769

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

775
        if minFeeUpdateTimeout >= maxFeeUpdateTimeout {
5✔
776
                return nil, fmt.Errorf("minFeeUpdateTimeout target of %v "+
1✔
777
                        "cannot be greater than maxFeeUpdateTimeout of %v",
1✔
778
                        minFeeUpdateTimeout, maxFeeUpdateTimeout)
1✔
779
        }
1✔
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) {
5✔
797

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

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

811
        // Get fee estimates now if we don't refresh periodically.
812
        if w.noCache {
3✔
UNCOV
813
                w.updateFeeEstimates()
×
UNCOV
814
        }
×
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 {
3✔
UNCOV
829
                satPerKw = FeePerKwFloor
×
UNCOV
830
        }
×
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 {
1✔
843
        log.Infof("Starting Web API fee estimator...")
1✔
844

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

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

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

859
        feeUpdateTimeout := w.randomFeeUpdateTimeout()
1✔
860

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

1✔
864
        w.updateFeeTicker = time.NewTicker(feeUpdateTimeout)
1✔
865

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

1✔
869
        return nil
1✔
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 {
1✔
877
        log.Infof("Stopping web API fee estimator")
1✔
878

1✔
879
        if w.stopped.Swap(true) {
1✔
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 {
1✔
UNCOV
885
                return nil
×
UNCOV
886
        }
×
887

888
        if w.updateFeeTicker != nil {
2✔
889
                w.updateFeeTicker.Stop()
1✔
890
        }
1✔
891

892
        close(w.quit)
1✔
893
        w.wg.Wait()
1✔
894

1✔
895
        return nil
1✔
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.
UNCOV
902
func (w *WebAPIEstimator) RelayFeePerKW() SatPerKWeight {
×
UNCOV
903
        if !w.started.Load() {
×
904
                log.Error("WebAPIEstimator not started")
×
905
        }
×
906

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

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

×
UNCOV
915
        return w.minRelayFeerate.FeePerKWeight()
×
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.
921
func (w *WebAPIEstimator) randomFeeUpdateTimeout() time.Duration {
1,001✔
922
        lower := int64(w.minFeeUpdateTimeout)
1,001✔
923
        upper := int64(w.maxFeeUpdateTimeout)
1,001✔
924
        return time.Duration(
1,001✔
925
                prand.Int63n(upper-lower) + lower, //nolint:gosec
1,001✔
926
        ).Round(time.Second)
1,001✔
927
}
1,001✔
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) {
8✔
935
        w.feesMtx.Lock()
8✔
936
        defer w.feesMtx.Unlock()
8✔
937

8✔
938
        // If the cache is empty, return an error.
8✔
939
        if len(w.feeByBlockTarget) == 0 {
9✔
940
                return 0, fmt.Errorf("web API error: %w", errEmptyCache)
1✔
941
        }
1✔
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]
7✔
946

7✔
947
        // If the conf target can be found, exit early.
7✔
948
        if ok {
9✔
949
                return fee, nil
2✔
950
        }
2✔
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-- {
114✔
956
                fee, ok := w.feeByBlockTarget[target]
109✔
957
                if !ok {
215✔
958
                        continue
106✔
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.
978
        minTargetCached := uint32(math.MaxUint32)
2✔
979
        for target := range w.feeByBlockTarget {
6✔
980
                if target < minTargetCached {
6✔
981
                        minTargetCached = target
2✔
982
                }
2✔
983
        }
984

985
        fee, ok = w.feeByBlockTarget[minTargetCached]
2✔
986
        if !ok {
2✔
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.
994
        log.Errorf("Web API does not have a fee rate for target=%d, "+
2✔
995
                "using the fee rate for target=%d instead",
2✔
996
                numBlocks, minTargetCached)
2✔
997

2✔
998
        return fee, nil
2✔
999
}
1000

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

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

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

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

1✔
1027
        for {
2✔
1028
                select {
1✔
1029
                case <-w.updateFeeTicker.C:
×
1030
                        w.updateFeeEstimates()
×
1031
                case <-w.quit:
1✔
1032
                        return
1✔
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