github.com/decred/dcrlnd@v0.7.6/lnwallet/chainfee/estimator.go (about) 1 package chainfee 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "math" 10 prand "math/rand" 11 "net" 12 "net/http" 13 "sync" 14 "time" 15 16 "github.com/decred/dcrd/dcrutil/v4" 17 dcrjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4" 18 "github.com/decred/dcrd/rpcclient/v8" 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 // minFeeUpdateTimeout represents the minimum interval in which a 34 // WebAPIEstimator will request fresh fees from its API. 35 minFeeUpdateTimeout = 5 * time.Minute 36 37 // maxFeeUpdateTimeout represents the maximum interval in which a 38 // WebAPIEstimator will request fresh fees from its API. 39 maxFeeUpdateTimeout = 20 * time.Minute 40 ) 41 42 var ( 43 // errNoFeeRateFound is used when a given conf target cannot be found 44 // from the fee estimator. 45 errNoFeeRateFound = errors.New("no fee estimation for block target") 46 47 // errEmptyCache is used when the fee rate cache is empty. 48 errEmptyCache = errors.New("fee rate cache is empty") 49 ) 50 51 // Estimator provides the ability to estimate on-chain transaction fees for 52 // various combinations of transaction sizes and desired confirmation time 53 // (measured by number of blocks). 54 type Estimator interface { 55 // EstimateFeePerKB takes in a target for the number of blocks until 56 // an initial confirmation and returns the estimated fee expressed in 57 // atoms/byte. 58 EstimateFeePerKB(numBlocks uint32) (AtomPerKByte, error) 59 60 // Start signals the Estimator to start any processes or goroutines 61 // it needs to perform its duty. 62 Start() error 63 64 // Stop stops any spawned goroutines and cleans up the resources used 65 // by the fee estimator. 66 Stop() error 67 68 // RelayFeePerKB returns the minimum fee rate required for transactions 69 // to be relayed. This is also the basis for calculation of the dust 70 // limit. 71 RelayFeePerKB() AtomPerKByte 72 } 73 74 // StaticEstimator will return a static value for all fee calculation requests. 75 // It is designed to be replaced by a proper fee calculation implementation. 76 // The fees are not accessible directly, because changing them would not be 77 // thread safe. 78 type StaticEstimator struct { 79 // feePerKB is the static fee rate in atoms-per-kB that will be 80 // returned by this fee estimator. 81 feePerKB AtomPerKByte 82 83 // relayFee is the minimum fee rate required for transactions to be 84 // relayed. 85 relayFee AtomPerKByte 86 } 87 88 // NewStaticEstimator returns a new static fee estimator instance. 89 func NewStaticEstimator(feePerKB, 90 relayFee AtomPerKByte) *StaticEstimator { 91 92 return &StaticEstimator{ 93 feePerKB: feePerKB, 94 relayFee: relayFee, 95 } 96 } 97 98 // EstimateFeePerKB will return the static value for fee calculations. 99 // 100 // NOTE: This method is part of the FeeEstimator interface. 101 func (e StaticEstimator) EstimateFeePerKB(numBlocks uint32) (AtomPerKByte, error) { 102 return e.feePerKB, nil 103 } 104 105 // RelayFeePerKB returns the minimum fee rate required for transactions to be 106 // relayed. 107 // 108 // NOTE: This method is part of the FeeEstimator interface. 109 func (e StaticEstimator) RelayFeePerKB() AtomPerKByte { 110 return e.relayFee 111 } 112 113 // Start signals the Estimator to start any processes or goroutines 114 // it needs to perform its duty. 115 // 116 // NOTE: This method is part of the Estimator interface. 117 func (e StaticEstimator) Start() error { 118 return nil 119 } 120 121 // Stop stops any spawned goroutines and cleans up the resources used 122 // by the fee estimator. 123 // 124 // NOTE: This method is part of the Estimator interface. 125 func (e StaticEstimator) Stop() error { 126 return nil 127 } 128 129 // A compile-time assertion to ensure that StaticFeeEstimator implements the 130 // Estimator interface. 131 var _ Estimator = (*StaticEstimator)(nil) 132 133 // DcrdEstimator is an implementation of the FeeEstimator interface backed by 134 // the RPC interface of an active dcrd node. This implementation will proxy any 135 // fee estimation requests to dcrd's RPC interface. 136 type DcrdEstimator struct { 137 ctx context.Context 138 cancel func() 139 140 // fallbackFeePerKB is the fall back fee rate in atoms/kB that is 141 // returned if the fee estimator does not yet have enough data to 142 // actually produce fee estimates. 143 fallbackFeePerKB AtomPerKByte 144 145 // minFeeManager is used to query the current minimum fee, in atoms/kB, 146 // that we should enforce. This will be used to determine fee rate for 147 // a transaction when the estimated fee rate is too low to allow the 148 // transaction to propagate through the network. 149 minFeeManager *minFeeManager 150 151 dcrdConn *rpcclient.Client 152 } 153 154 // NewDcrdEstimator creates a new DcrdFeeEstimator given a fully populated rpc 155 // config that is able to successfully connect and authenticate with the dcrd 156 // node, and also a fall back fee rate. The fallback fee rate is used in the 157 // occasion that the estimator has insufficient data, or returns zero for a fee 158 // estimate. 159 func NewDcrdEstimator(rpcConfig rpcclient.ConnConfig, 160 fallBackFeeRate AtomPerKByte) (*DcrdEstimator, error) { 161 162 rpcConfig.DisableConnectOnNew = true 163 rpcConfig.DisableAutoReconnect = false 164 chainConn, err := rpcclient.New(&rpcConfig, nil) 165 if err != nil { 166 return nil, err 167 } 168 169 ctx, cancel := context.WithCancel(context.Background()) 170 171 return &DcrdEstimator{ 172 ctx: ctx, 173 cancel: cancel, 174 fallbackFeePerKB: fallBackFeeRate, 175 dcrdConn: chainConn, 176 }, nil 177 } 178 179 // Start signals the Estimator to start any processes or goroutines 180 // it needs to perform its duty. 181 // 182 // NOTE: This method is part of the FeeEstimator interface. 183 func (b *DcrdEstimator) Start() error { 184 ctx := context.Background() 185 if err := b.dcrdConn.Connect(ctx, true); err != nil { 186 return err 187 } 188 189 // Once the connection to the backend node has been established, we 190 // can initialise the minimum relay fee manager which queries the 191 // chain backend for the minimum relay fee on construction. 192 minRelayFeeManager, err := newMinFeeManager( 193 defaultUpdateInterval, b.fetchMinRelayFee, 194 ) 195 if err != nil { 196 return err 197 198 } 199 b.minFeeManager = minRelayFeeManager 200 201 return nil 202 } 203 204 func (b *DcrdEstimator) fetchMinRelayFee() (AtomPerKByte, error) { 205 // Once the connection to the backend node has been established, we'll 206 // query it for its minimum relay fee. 207 info, err := b.dcrdConn.GetInfo(b.ctx) 208 if err != nil { 209 return 0, err 210 } 211 212 relayFee, err := dcrutil.NewAmount(info.RelayFee) 213 if err != nil { 214 return 0, err 215 } 216 217 // By default, we'll use the backend node's minimum relay fee as the 218 // minimum fee rate we'll propose for transactions. However, if this 219 // happens to be lower than our fee floor, we'll enforce that instead. 220 minFeePerKB := AtomPerKByte(relayFee) 221 if minFeePerKB < FeePerKBFloor { 222 log.Warnf("Dcrd returned fee rate of %s which is "+ 223 "lower than the floor rate", minFeePerKB) 224 minFeePerKB = FeePerKBFloor 225 } 226 227 log.Debugf("Using minimum fee rate of %s", minFeePerKB) 228 229 return minFeePerKB, nil 230 } 231 232 // Stop stops any spawned goroutines and cleans up the resources used 233 // by the fee estimator. 234 // 235 // NOTE: This method is part of the FeeEstimator interface. 236 func (b *DcrdEstimator) Stop() error { 237 b.cancel() 238 b.dcrdConn.Shutdown() 239 return nil 240 } 241 242 // RelayFeePerKB returns the minimum fee rate required for transactions to be 243 // relayed. 244 // 245 // NOTE: This method is part of the FeeEstimator interface. 246 func (b *DcrdEstimator) RelayFeePerKB() AtomPerKByte { 247 return b.minFeeManager.fetchMinFee() 248 } 249 250 // EstimateFeePerKB queries the connected chain client for a fee estimation for 251 // the given block range. 252 // 253 // NOTE: This method is part of the FeeEstimator interface. 254 func (b *DcrdEstimator) EstimateFeePerKB(numBlocks uint32) (AtomPerKByte, error) { 255 if numBlocks > maxBlockTarget { 256 log.Debugf("conf target %d exceeds the max value, "+ 257 "use %d instead.", numBlocks, maxBlockTarget, 258 ) 259 numBlocks = maxBlockTarget 260 } 261 262 feeEstimate, err := b.fetchEstimate(numBlocks) 263 switch { 264 // If the estimator doesn't have enough data, or returns an error, then 265 // to return a proper value, then we'll return the default fall back 266 // fee rate. 267 case err != nil: 268 log.Errorf("unable to query estimator: %v", err) 269 fallthrough 270 271 case feeEstimate == 0: 272 return b.fallbackFeePerKB, nil 273 } 274 275 return feeEstimate, nil 276 } 277 278 // fetchEstimate returns a fee estimate for a transaction to be confirmed in 279 // confTarget blocks. The estimate is returned in atom/kB. 280 func (b *DcrdEstimator) fetchEstimate(confTarget uint32) (AtomPerKByte, error) { 281 // First, we'll fetch the estimate for our confirmation target. 282 dcrPerKB, err := b.dcrdConn.EstimateSmartFee(b.ctx, int64(confTarget), 283 dcrjson.EstimateSmartFeeConservative) 284 if err != nil { 285 return 0, err 286 } 287 288 // Next, we'll convert the returned value to atoms, as it's 289 // currently returned in DCR. 290 atoms, err := dcrutil.NewAmount(dcrPerKB.FeeRate) 291 if err != nil { 292 return 0, err 293 } 294 atomsPerKB := AtomPerKByte(atoms) 295 296 // Finally, we'll enforce our fee floor. 297 minFee := b.minFeeManager.fetchMinFee() 298 if atomsPerKB < minFee { 299 log.Debugf("Estimated fee rate of %s is too low, "+ 300 "using fee floor of %s", atomsPerKB, minFee) 301 atomsPerKB = minFee 302 } 303 304 log.Debugf("Returning %s for conf target of %d", 305 atomsPerKB, confTarget) 306 307 return atomsPerKB, nil 308 } 309 310 // A compile-time assertion to ensure that DcrdFeeEstimator implements the 311 // FeeEstimator interface. 312 var _ Estimator = (*DcrdEstimator)(nil) 313 314 // WebAPIFeeSource is an interface allows the WebAPIEstimator to query an 315 // arbitrary HTTP-based fee estimator. Each new set/network will gain an 316 // implementation of this interface in order to allow the WebAPIEstimator to 317 // be fully generic in its logic. 318 type WebAPIFeeSource interface { 319 // GenQueryURL generates the full query URL. The value returned by this 320 // method should be able to be used directly as a path for an HTTP GET 321 // request. 322 GenQueryURL() string 323 324 // ParseResponse attempts to parse the body of the response generated 325 // by the above query URL. Typically this will be JSON, but the 326 // specifics are left to the WebAPIFeeSource implementation. 327 ParseResponse(r io.Reader) (map[uint32]uint32, error) 328 } 329 330 // SparseConfFeeSource is an implementation of the WebAPIFeeSource that utilizes 331 // a user-specified fee estimation API for Bitcoin. It expects the response 332 // to be in the JSON format: `fee_by_block_target: { ... }` where the value maps 333 // block targets to fee estimates (in sat per kilovbyte). 334 type SparseConfFeeSource struct { 335 // URL is the fee estimation API specified by the user. 336 URL string 337 } 338 339 // GenQueryURL generates the full query URL. The value returned by this 340 // method should be able to be used directly as a path for an HTTP GET 341 // request. 342 // 343 // NOTE: Part of the WebAPIFeeSource interface. 344 func (s SparseConfFeeSource) GenQueryURL() string { 345 return s.URL 346 } 347 348 // ParseResponse attempts to parse the body of the response generated by the 349 // above query URL. Typically this will be JSON, but the specifics are left to 350 // the WebAPIFeeSource implementation. 351 // 352 // NOTE: Part of the WebAPIFeeSource interface. 353 func (s SparseConfFeeSource) ParseResponse(r io.Reader) (map[uint32]uint32, error) { 354 type jsonResp struct { 355 FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"` 356 } 357 358 resp := jsonResp{ 359 FeeByBlockTarget: make(map[uint32]uint32), 360 } 361 jsonReader := json.NewDecoder(r) 362 if err := jsonReader.Decode(&resp); err != nil { 363 return nil, err 364 } 365 366 return resp.FeeByBlockTarget, nil 367 } 368 369 // A compile-time assertion to ensure that SparseConfFeeSource implements the 370 // WebAPIFeeSource interface. 371 var _ WebAPIFeeSource = (*SparseConfFeeSource)(nil) 372 373 // WebAPIEstimator is an implementation of the Estimator interface that 374 // queries an HTTP-based fee estimation from an existing web API. 375 type WebAPIEstimator struct { 376 started sync.Once 377 stopped sync.Once 378 379 // apiSource is the backing web API source we'll use for our queries. 380 apiSource WebAPIFeeSource 381 382 // updateFeeTicker is the ticker responsible for updating the Estimator's 383 // fee estimates every time it fires. 384 updateFeeTicker *time.Ticker 385 386 // feeByBlockTarget is our cache for fees pulled from the API. When a 387 // fee estimate request comes in, we pull the estimate from this array 388 // rather than re-querying the API, to prevent an inadvertent DoS attack. 389 feesMtx sync.Mutex 390 feeByBlockTarget map[uint32]uint32 391 392 // netGetter performs a GET http request to the specified URL and 393 // returns the response. It is exposed here to allow tests to mock the 394 // network. 395 netGetter func(url string) (*http.Response, error) 396 397 // noCache determines whether the web estimator should cache fee 398 // estimates. 399 noCache bool 400 401 quit chan struct{} 402 wg sync.WaitGroup 403 } 404 405 // defaultNetGetter performs a GET request to the specified URL or times out in 406 // at most 10 seconds. 407 func defaultNetGetter(url string) (*http.Response, error) { 408 // Rather than use the default http.Client, we'll make a custom one 409 // which will allow us to control how long we'll wait to read the 410 // response from the service. This way, if the service is down or 411 // overloaded, we can exit early and use our default fee. 412 netTransport := &http.Transport{ 413 Dial: (&net.Dialer{ 414 Timeout: 5 * time.Second, 415 }).Dial, 416 TLSHandshakeTimeout: 5 * time.Second, 417 } 418 netClient := &http.Client{ 419 Timeout: time.Second * 10, 420 Transport: netTransport, 421 } 422 423 return netClient.Get(url) 424 } 425 426 // NewWebAPIEstimator creates a new WebAPIEstimator from a given URL and a 427 // fallback default fee. The fees are updated whenever a new block is mined. 428 func NewWebAPIEstimator(api WebAPIFeeSource, noCache bool) *WebAPIEstimator { 429 return &WebAPIEstimator{ 430 apiSource: api, 431 feeByBlockTarget: make(map[uint32]uint32), 432 netGetter: defaultNetGetter, 433 noCache: noCache, 434 quit: make(chan struct{}), 435 } 436 } 437 438 // EstimateFeePerKB takes in a target for the number of blocks until an initial 439 // confirmation and returns the estimated fee expressed in sat/kw. 440 // 441 // NOTE: This method is part of the FeeEstimator interface. 442 func (w *WebAPIEstimator) EstimateFeePerKB(numBlocks uint32) ( 443 AtomPerKByte, error) { 444 445 if numBlocks > maxBlockTarget { 446 numBlocks = maxBlockTarget 447 } else if numBlocks < minBlockTarget { 448 return 0, fmt.Errorf("conf target of %v is too low, minimum "+ 449 "accepted is %v", numBlocks, minBlockTarget) 450 } 451 452 // Get fee estimates now if we don't refresh periodically. 453 if w.noCache { 454 w.updateFeeEstimates() 455 } 456 457 feePerKb, err := w.getCachedFee(numBlocks) 458 459 // If the estimator returns an error, a zero value fee rate will be 460 // returned. We will log the error and return the fall back fee rate 461 // instead. 462 if err != nil { 463 log.Errorf("unable to query estimator: %v", err) 464 } 465 atomsPerKB := AtomPerKByte(feePerKb) 466 467 // If the result is too low, then we'll clamp it to our current fee 468 // floor. 469 if atomsPerKB < FeePerKBFloor { 470 atomsPerKB = FeePerKBFloor 471 } 472 473 log.Debugf("Web API returning %v atoms/KB for conf target of %v", 474 atomsPerKB, numBlocks) 475 476 return atomsPerKB, nil 477 } 478 479 // Start signals the Estimator to start any processes or goroutines it needs 480 // to perform its duty. 481 // 482 // NOTE: This method is part of the Estimator interface. 483 func (w *WebAPIEstimator) Start() error { 484 // No update loop is needed when we don't cache. 485 if w.noCache { 486 return nil 487 } 488 489 var err error 490 w.started.Do(func() { 491 log.Infof("Starting web API fee estimator") 492 493 w.updateFeeTicker = time.NewTicker(w.randomFeeUpdateTimeout()) 494 w.updateFeeEstimates() 495 496 w.wg.Add(1) 497 go w.feeUpdateManager() 498 499 }) 500 return err 501 } 502 503 // Stop stops any spawned goroutines and cleans up the resources used by the 504 // fee estimator. 505 // 506 // NOTE: This method is part of the Estimator interface. 507 func (w *WebAPIEstimator) Stop() error { 508 // Update loop is not running when we don't cache. 509 if w.noCache { 510 return nil 511 } 512 513 w.stopped.Do(func() { 514 log.Infof("Stopping web API fee estimator") 515 516 w.updateFeeTicker.Stop() 517 518 close(w.quit) 519 w.wg.Wait() 520 }) 521 return nil 522 } 523 524 // RelayFeePerKB returns the minimum fee rate required for transactions to be 525 // relayed. 526 // 527 // NOTE: This method is part of the FeeEstimator interface. 528 func (w *WebAPIEstimator) RelayFeePerKB() AtomPerKByte { 529 return FeePerKBFloor 530 } 531 532 // randomFeeUpdateTimeout returns a random timeout between minFeeUpdateTimeout 533 // and maxFeeUpdateTimeout that will be used to determine how often the Estimator 534 // should retrieve fresh fees from its API. 535 func (w *WebAPIEstimator) randomFeeUpdateTimeout() time.Duration { 536 lower := int64(minFeeUpdateTimeout) 537 upper := int64(maxFeeUpdateTimeout) 538 return time.Duration(prand.Int63n(upper-lower) + lower) 539 } 540 541 // getCachedFee takes a conf target and returns the cached fee rate. When the 542 // fee rate cannot be found, it will search the cache by decrementing the conf 543 // target until a fee rate is found. If still not found, it will return the fee 544 // rate of the minimum conf target cached, in other words, the most expensive 545 // fee rate it knows of. 546 func (w *WebAPIEstimator) getCachedFee(numBlocks uint32) (uint32, error) { 547 w.feesMtx.Lock() 548 defer w.feesMtx.Unlock() 549 550 // If the cache is empty, return an error. 551 if len(w.feeByBlockTarget) == 0 { 552 return 0, fmt.Errorf("web API error: %w", errEmptyCache) 553 } 554 555 // Search the conf target from the cache. We expect a query to the web 556 // API has been made and the result has been cached at this point. 557 fee, ok := w.feeByBlockTarget[numBlocks] 558 559 // If the conf target can be found, exit early. 560 if ok { 561 return fee, nil 562 } 563 564 // The conf target cannot be found. We will first search the cache 565 // using a lower conf target. This is a conservative approach as the 566 // fee rate returned will be larger than what's requested. 567 for target := numBlocks; target >= minBlockTarget; target-- { 568 fee, ok := w.feeByBlockTarget[target] 569 if !ok { 570 continue 571 } 572 573 log.Warnf("Web API does not have a fee rate for target=%d, "+ 574 "using the fee rate for target=%d instead", 575 numBlocks, target) 576 577 // Return the fee rate found, which will be more expensive than 578 // requested. We will not cache the fee rate here in the hope 579 // that the web API will later populate this value. 580 return fee, nil 581 } 582 583 // There are no lower conf targets cached, which is likely when the 584 // requested conf target is 1. We will search the cache using a higher 585 // conf target, which gives a fee rate that's cheaper than requested. 586 // 587 // NOTE: we can only get here iff the requested conf target is smaller 588 // than the minimum conf target cached, so we return the minimum conf 589 // target from the cache. 590 minTargetCached := uint32(math.MaxUint32) 591 for target := range w.feeByBlockTarget { 592 if target < minTargetCached { 593 minTargetCached = target 594 } 595 } 596 597 fee, ok = w.feeByBlockTarget[minTargetCached] 598 if !ok { 599 // We should never get here, just a vanity check. 600 return 0, fmt.Errorf("web API error: %w, conf target: %d", 601 errNoFeeRateFound, numBlocks) 602 } 603 604 // Log an error instead of a warning as a cheaper fee rate may delay 605 // the confirmation for some important transactions. 606 log.Errorf("Web API does not have a fee rate for target=%d, "+ 607 "using the fee rate for target=%d instead", 608 numBlocks, minTargetCached) 609 610 return fee, nil 611 } 612 613 // updateFeeEstimates re-queries the API for fresh fees and caches them. 614 func (w *WebAPIEstimator) updateFeeEstimates() { 615 // With the client created, we'll query the API source to fetch the URL 616 // that we should use to query for the fee estimation. 617 targetURL := w.apiSource.GenQueryURL() 618 resp, err := w.netGetter(targetURL) 619 if err != nil { 620 log.Errorf("unable to query web api for fee response: %v", 621 err) 622 return 623 } 624 defer resp.Body.Close() 625 626 // Once we've obtained the response, we'll instruct the WebAPIFeeSource 627 // to parse out the body to obtain our final result. 628 feesByBlockTarget, err := w.apiSource.ParseResponse(resp.Body) 629 if err != nil { 630 log.Errorf("unable to query web api for fee response: %v", 631 err) 632 return 633 } 634 635 w.feesMtx.Lock() 636 w.feeByBlockTarget = feesByBlockTarget 637 w.feesMtx.Unlock() 638 } 639 640 // feeUpdateManager updates the fee estimates whenever a new block comes in. 641 func (w *WebAPIEstimator) feeUpdateManager() { 642 defer w.wg.Done() 643 644 for { 645 select { 646 case <-w.updateFeeTicker.C: 647 w.updateFeeEstimates() 648 case <-w.quit: 649 return 650 } 651 } 652 } 653 654 // A compile-time assertion to ensure that WebAPIEstimator implements the 655 // Estimator interface. 656 var _ Estimator = (*WebAPIEstimator)(nil)