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

lightningnetwork / lnd / 11216766535

07 Oct 2024 01:37PM UTC coverage: 57.817% (-1.0%) from 58.817%
11216766535

Pull #9148

github

ProofOfKeags
lnwire: remove kickoff feerate from propose/commit
Pull Request #9148: DynComms [2/n]: lnwire: add authenticated wire messages for Dyn*

571 of 879 new or added lines in 16 files covered. (64.96%)

23253 existing lines in 251 files now uncovered.

99022 of 171268 relevant lines covered (57.82%)

38420.67 hits per line

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

33.46
/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 {
398✔
100
        return &StaticEstimator{
398✔
101
                feePerKW: feePerKW,
398✔
102
                relayFee: relayFee,
398✔
103
        }
398✔
104
}
398✔
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) {
403✔
110
        return e.feePerKW, nil
403✔
111
}
403✔
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

×
242
        return nil
×
243
}
×
244

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

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

263
        return feeEstimate, nil
×
264
}
265

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

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

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

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

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

×
300
                satPerKw = absoluteMinFee
×
301
        }
×
302

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

×
306
        return satPerKw, nil
×
307
}
308

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
410
        b.filterManager.Start()
×
411

×
412
        return nil
×
413
}
414

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

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

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

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

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

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

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

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

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

480
        return feeEstimate, nil
×
481
}
482

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

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

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

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

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

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

×
521
                satPerKw = absoluteMinFee
×
522
        }
×
523

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

×
527
        return satPerKw, nil
×
528
}
529

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

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

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

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

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

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

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

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

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

×
591
        minRelayFee := minRelayFeeFunc()
×
592

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

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

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

618
                return medianFilter
×
619
        }
620

621
        return minRelayFee
×
622
}
623

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

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

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

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

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

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

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

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

678
        return resp, nil
2✔
679
}
680

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

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

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

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

UNCOV
720
        return parsedResp, nil
×
721
}
722

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
868
        return nil
1✔
869
}
870

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

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

882
        // Update loop is not running when we don't cache.
883
        if w.noCache {
1✔
UNCOV
884
                return nil
×
UNCOV
885
        }
×
886

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

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

1✔
894
        return nil
1✔
895
}
896

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

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

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

×
UNCOV
914
        return w.minRelayFeerate.FeePerKWeight()
×
915
}
916

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

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

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

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

7✔
946
        // If the conf target can be found, exit early.
7✔
947
        if ok {
9✔
948
                return fee, nil
2✔
949
        }
2✔
950

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

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

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

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

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

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

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

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

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

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

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

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

1036
// A compile-time assertion to ensure that WebAPIEstimator implements the
1037
// Estimator interface.
1038
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