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

lightningnetwork / lnd / 9915780197

13 Jul 2024 12:30AM UTC coverage: 49.268% (-9.1%) from 58.413%
9915780197

push

github

web-flow
Merge pull request #8653 from ProofOfKeags/fn-prim

DynComms [0/n]: `fn` package additions

92837 of 188433 relevant lines covered (49.27%)

1.55 hits per line

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

25.83
/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
        "time"
14

15
        "github.com/btcsuite/btcd/btcutil"
16
        "github.com/btcsuite/btcd/rpcclient"
17
)
18

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

154
        btcdConn *rpcclient.Client
155

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

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

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

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

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

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

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

×
208
        b.filterManager.Start()
×
209

×
210
        return nil
×
211
}
212

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

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

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

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

×
238
        b.btcdConn.Shutdown()
×
239

×
240
        return nil
×
241
}
×
242

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

257
        case feeEstimate == 0:
×
258
                return b.fallbackFeePerKW, nil
×
259
        }
260

261
        return feeEstimate, nil
×
262
}
263

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

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

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

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

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

×
298
                satPerKw = absoluteMinFee
×
299
        }
×
300

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

×
304
        return satPerKw, nil
×
305
}
306

307
func (b *BtcdEstimator) fetchEstimateInner(confTarget uint32) (SatPerKWeight,
308
        error) {
×
309

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
408
        b.filterManager.Start()
×
409

×
410
        return nil
×
411
}
412

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

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

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

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

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

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

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

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

474
        case feeEstimate == 0:
×
475
                return b.fallbackFeePerKW, nil
×
476
        }
477

478
        return feeEstimate, nil
×
479
}
480

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

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

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

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

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

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

×
519
                satPerKw = absoluteMinFee
×
520
        }
×
521

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

×
525
        return satPerKw, nil
×
526
}
527

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

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

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

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

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

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

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

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

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

×
589
        minRelayFee := minRelayFeeFunc()
×
590

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

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

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

616
                return medianFilter
×
617
        }
618

619
        return minRelayFee
×
620
}
621

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

626
// WebAPIFeeSource is an interface allows the WebAPIEstimator to query an
627
// arbitrary HTTP-based fee estimator. Each new set/network will gain an
628
// implementation of this interface in order to allow the WebAPIEstimator to
629
// be fully generic in its logic.
630
type WebAPIFeeSource interface {
631
        // GetFeeMap will query the web API, parse the response and return a
632
        // map of confirmation targets to sat/kw fees.
633
        GetFeeMap() (map[uint32]uint32, error)
634
}
635

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

645
// parseResponse attempts to parse the body of the response generated by the
646
// above query URL. Typically this will be JSON, but the specifics are left to
647
// the WebAPIFeeSource implementation.
648
func (s SparseConfFeeSource) parseResponse(r io.Reader) (
649
        map[uint32]uint32, error) {
3✔
650

3✔
651
        type jsonResp struct {
3✔
652
                FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"`
3✔
653
        }
3✔
654

3✔
655
        resp := jsonResp{
3✔
656
                FeeByBlockTarget: make(map[uint32]uint32),
3✔
657
        }
3✔
658
        jsonReader := json.NewDecoder(r)
3✔
659
        if err := jsonReader.Decode(&resp); err != nil {
3✔
660
                return nil, err
×
661
        }
×
662

663
        return resp.FeeByBlockTarget, nil
3✔
664
}
665

666
// GetFeeMap will query the web API, parse the response and return a map of
667
// confirmation targets to sat/kw fees.
668
func (s SparseConfFeeSource) GetFeeMap() (map[uint32]uint32, error) {
3✔
669
        // Rather than use the default http.Client, we'll make a custom one
3✔
670
        // which will allow us to control how long we'll wait to read the
3✔
671
        // response from the service. This way, if the service is down or
3✔
672
        // overloaded, we can exit early and use our default fee.
3✔
673
        netTransport := &http.Transport{
3✔
674
                Dial: (&net.Dialer{
3✔
675
                        Timeout: WebAPIConnectionTimeout,
3✔
676
                }).Dial,
3✔
677
                TLSHandshakeTimeout: WebAPIConnectionTimeout,
3✔
678
        }
3✔
679
        netClient := &http.Client{
3✔
680
                Timeout:   WebAPIResponseTimeout,
3✔
681
                Transport: netTransport,
3✔
682
        }
3✔
683

3✔
684
        // With the client created, we'll query the API source to fetch the URL
3✔
685
        // that we should use to query for the fee estimation.
3✔
686
        targetURL := s.URL
3✔
687
        resp, err := netClient.Get(targetURL)
3✔
688
        if err != nil {
3✔
689
                log.Errorf("unable to query web api for fee response: %v",
×
690
                        err)
×
691
                return nil, err
×
692
        }
×
693
        defer resp.Body.Close()
3✔
694

3✔
695
        // Once we've obtained the response, we'll instruct the WebAPIFeeSource
3✔
696
        // to parse out the body to obtain our final result.
3✔
697
        feesByBlockTarget, err := s.parseResponse(resp.Body)
3✔
698
        if err != nil {
3✔
699
                log.Errorf("unable to parse fee api response: %v", err)
×
700

×
701
                return nil, err
×
702
        }
×
703

704
        return feesByBlockTarget, nil
3✔
705
}
706

707
// A compile-time assertion to ensure that SparseConfFeeSource implements the
708
// WebAPIFeeSource interface.
709
var _ WebAPIFeeSource = (*SparseConfFeeSource)(nil)
710

711
// WebAPIEstimator is an implementation of the Estimator interface that
712
// queries an HTTP-based fee estimation from an existing web API.
713
type WebAPIEstimator struct {
714
        started sync.Once
715
        stopped sync.Once
716

717
        // apiSource is the backing web API source we'll use for our queries.
718
        apiSource WebAPIFeeSource
719

720
        // updateFeeTicker is the ticker responsible for updating the Estimator's
721
        // fee estimates every time it fires.
722
        updateFeeTicker *time.Ticker
723

724
        // feeByBlockTarget is our cache for fees pulled from the API. When a
725
        // fee estimate request comes in, we pull the estimate from this array
726
        // rather than re-querying the API, to prevent an inadvertent DoS attack.
727
        feesMtx          sync.Mutex
728
        feeByBlockTarget map[uint32]uint32
729

730
        // noCache determines whether the web estimator should cache fee
731
        // estimates.
732
        noCache bool
733

734
        // minFeeUpdateTimeout represents the minimum interval in which the
735
        // web estimator will request fresh fees from its API.
736
        minFeeUpdateTimeout time.Duration
737

738
        // minFeeUpdateTimeout represents the maximum interval in which the
739
        // web estimator will request fresh fees from its API.
740
        maxFeeUpdateTimeout time.Duration
741

742
        quit chan struct{}
743
        wg   sync.WaitGroup
744
}
745

746
// NewWebAPIEstimator creates a new WebAPIEstimator from a given URL and a
747
// fallback default fee. The fees are updated whenever a new block is mined.
748
func NewWebAPIEstimator(api WebAPIFeeSource, noCache bool,
749
        minFeeUpdateTimeout time.Duration,
750
        maxFeeUpdateTimeout time.Duration) (*WebAPIEstimator, error) {
3✔
751

3✔
752
        if minFeeUpdateTimeout == 0 || maxFeeUpdateTimeout == 0 {
3✔
753
                return nil, fmt.Errorf("minFeeUpdateTimeout and " +
×
754
                        "maxFeeUpdateTimeout must be greater than 0")
×
755
        }
×
756

757
        if minFeeUpdateTimeout >= maxFeeUpdateTimeout {
3✔
758
                return nil, fmt.Errorf("minFeeUpdateTimeout target of %v "+
×
759
                        "cannot be greater than maxFeeUpdateTimeout of %v",
×
760
                        minFeeUpdateTimeout, maxFeeUpdateTimeout)
×
761
        }
×
762

763
        return &WebAPIEstimator{
3✔
764
                apiSource:           api,
3✔
765
                feeByBlockTarget:    make(map[uint32]uint32),
3✔
766
                noCache:             noCache,
3✔
767
                quit:                make(chan struct{}),
3✔
768
                minFeeUpdateTimeout: minFeeUpdateTimeout,
3✔
769
                maxFeeUpdateTimeout: maxFeeUpdateTimeout,
3✔
770
        }, nil
3✔
771
}
772

773
// EstimateFeePerKW takes in a target for the number of blocks until an initial
774
// confirmation and returns the estimated fee expressed in sat/kw.
775
//
776
// NOTE: This method is part of the Estimator interface.
777
func (w *WebAPIEstimator) EstimateFeePerKW(numBlocks uint32) (
778
        SatPerKWeight, error) {
3✔
779

3✔
780
        if numBlocks > MaxBlockTarget {
3✔
781
                numBlocks = MaxBlockTarget
×
782
        } else if numBlocks < minBlockTarget {
3✔
783
                return 0, fmt.Errorf("conf target of %v is too low, minimum "+
×
784
                        "accepted is %v", numBlocks, minBlockTarget)
×
785
        }
×
786

787
        // Get fee estimates now if we don't refresh periodically.
788
        if w.noCache {
6✔
789
                w.updateFeeEstimates()
3✔
790
        }
3✔
791

792
        feePerKb, err := w.getCachedFee(numBlocks)
3✔
793

3✔
794
        // If the estimator returns an error, a zero value fee rate will be
3✔
795
        // returned. We will log the error and return the fall back fee rate
3✔
796
        // instead.
3✔
797
        if err != nil {
3✔
798
                log.Errorf("Unable to query estimator: %v", err)
×
799
        }
×
800

801
        // If the result is too low, then we'll clamp it to our current fee
802
        // floor.
803
        satPerKw := SatPerKVByte(feePerKb).FeePerKWeight()
3✔
804
        if satPerKw < FeePerKwFloor {
6✔
805
                satPerKw = FeePerKwFloor
3✔
806
        }
3✔
807

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

3✔
811
        return satPerKw, nil
3✔
812
}
813

814
// Start signals the Estimator to start any processes or goroutines it needs
815
// to perform its duty.
816
//
817
// NOTE: This method is part of the Estimator interface.
818
func (w *WebAPIEstimator) Start() error {
3✔
819
        // No update loop is needed when we don't cache.
3✔
820
        if w.noCache {
6✔
821
                return nil
3✔
822
        }
3✔
823

824
        var err error
×
825
        w.started.Do(func() {
×
826
                log.Infof("Starting web API fee estimator")
×
827

×
828
                feeUpdateTimeout := w.randomFeeUpdateTimeout()
×
829

×
830
                log.Infof("Web API fee estimator using update timeout of %v",
×
831
                        feeUpdateTimeout)
×
832

×
833
                w.updateFeeTicker = time.NewTicker(feeUpdateTimeout)
×
834
                w.updateFeeEstimates()
×
835

×
836
                w.wg.Add(1)
×
837
                go w.feeUpdateManager()
×
838

×
839
        })
×
840
        return err
×
841
}
842

843
// Stop stops any spawned goroutines and cleans up the resources used by the
844
// fee estimator.
845
//
846
// NOTE: This method is part of the Estimator interface.
847
func (w *WebAPIEstimator) Stop() error {
3✔
848
        // Update loop is not running when we don't cache.
3✔
849
        if w.noCache {
6✔
850
                return nil
3✔
851
        }
3✔
852

853
        w.stopped.Do(func() {
×
854
                log.Infof("Stopping web API fee estimator")
×
855

×
856
                w.updateFeeTicker.Stop()
×
857

×
858
                close(w.quit)
×
859
                w.wg.Wait()
×
860
        })
×
861
        return nil
×
862
}
863

864
// RelayFeePerKW returns the minimum fee rate required for transactions to be
865
// relayed.
866
//
867
// NOTE: This method is part of the Estimator interface.
868
func (w *WebAPIEstimator) RelayFeePerKW() SatPerKWeight {
3✔
869
        return FeePerKwFloor
3✔
870
}
3✔
871

872
// randomFeeUpdateTimeout returns a random timeout between minFeeUpdateTimeout
873
// and maxFeeUpdateTimeout that will be used to determine how often the Estimator
874
// should retrieve fresh fees from its API.
875
func (w *WebAPIEstimator) randomFeeUpdateTimeout() time.Duration {
×
876
        lower := int64(w.minFeeUpdateTimeout)
×
877
        upper := int64(w.maxFeeUpdateTimeout)
×
878
        return time.Duration(
×
879
                prand.Int63n(upper-lower) + lower, //nolint:gosec
×
880
        ).Round(time.Second)
×
881
}
×
882

883
// getCachedFee takes a conf target and returns the cached fee rate. When the
884
// fee rate cannot be found, it will search the cache by decrementing the conf
885
// target until a fee rate is found. If still not found, it will return the fee
886
// rate of the minimum conf target cached, in other words, the most expensive
887
// fee rate it knows of.
888
func (w *WebAPIEstimator) getCachedFee(numBlocks uint32) (uint32, error) {
3✔
889
        w.feesMtx.Lock()
3✔
890
        defer w.feesMtx.Unlock()
3✔
891

3✔
892
        // If the cache is empty, return an error.
3✔
893
        if len(w.feeByBlockTarget) == 0 {
3✔
894
                return 0, fmt.Errorf("web API error: %w", errEmptyCache)
×
895
        }
×
896

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

3✔
901
        // If the conf target can be found, exit early.
3✔
902
        if ok {
6✔
903
                return fee, nil
3✔
904
        }
3✔
905

906
        // The conf target cannot be found. We will first search the cache
907
        // using a lower conf target. This is a conservative approach as the
908
        // fee rate returned will be larger than what's requested.
909
        for target := numBlocks; target >= minBlockTarget; target-- {
6✔
910
                fee, ok := w.feeByBlockTarget[target]
3✔
911
                if !ok {
6✔
912
                        continue
3✔
913
                }
914

915
                log.Warnf("Web API does not have a fee rate for target=%d, "+
3✔
916
                        "using the fee rate for target=%d instead",
3✔
917
                        numBlocks, target)
3✔
918

3✔
919
                // Return the fee rate found, which will be more expensive than
3✔
920
                // requested. We will not cache the fee rate here in the hope
3✔
921
                // that the web API will later populate this value.
3✔
922
                return fee, nil
3✔
923
        }
924

925
        // There are no lower conf targets cached, which is likely when the
926
        // requested conf target is 1. We will search the cache using a higher
927
        // conf target, which gives a fee rate that's cheaper than requested.
928
        //
929
        // NOTE: we can only get here iff the requested conf target is smaller
930
        // than the minimum conf target cached, so we return the minimum conf
931
        // target from the cache.
932
        minTargetCached := uint32(math.MaxUint32)
×
933
        for target := range w.feeByBlockTarget {
×
934
                if target < minTargetCached {
×
935
                        minTargetCached = target
×
936
                }
×
937
        }
938

939
        fee, ok = w.feeByBlockTarget[minTargetCached]
×
940
        if !ok {
×
941
                // We should never get here, just a vanity check.
×
942
                return 0, fmt.Errorf("web API error: %w, conf target: %d",
×
943
                        errNoFeeRateFound, numBlocks)
×
944
        }
×
945

946
        // Log an error instead of a warning as a cheaper fee rate may delay
947
        // the confirmation for some important transactions.
948
        log.Errorf("Web API does not have a fee rate for target=%d, "+
×
949
                "using the fee rate for target=%d instead",
×
950
                numBlocks, minTargetCached)
×
951

×
952
        return fee, nil
×
953
}
954

955
// updateFeeEstimates re-queries the API for fresh fees and caches them.
956
func (w *WebAPIEstimator) updateFeeEstimates() {
3✔
957
        // Once we've obtained the response, we'll instruct the WebAPIFeeSource
3✔
958
        // to parse out the body to obtain our final result.
3✔
959
        feesByBlockTarget, err := w.apiSource.GetFeeMap()
3✔
960
        if err != nil {
3✔
961
                log.Errorf("unable to get fee response: %v", err)
×
962
                return
×
963
        }
×
964

965
        w.feesMtx.Lock()
3✔
966
        w.feeByBlockTarget = feesByBlockTarget
3✔
967
        w.feesMtx.Unlock()
3✔
968
}
969

970
// feeUpdateManager updates the fee estimates whenever a new block comes in.
971
func (w *WebAPIEstimator) feeUpdateManager() {
×
972
        defer w.wg.Done()
×
973

×
974
        for {
×
975
                select {
×
976
                case <-w.updateFeeTicker.C:
×
977
                        w.updateFeeEstimates()
×
978
                case <-w.quit:
×
979
                        return
×
980
                }
981
        }
982
}
983

984
// A compile-time assertion to ensure that WebAPIEstimator implements the
985
// Estimator interface.
986
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