github.com/0xsequence/ethkit@v1.25.0/ethgas/ethgas.go (about) 1 package ethgas 2 3 import ( 4 "context" 5 "fmt" 6 "math/big" 7 "sort" 8 "sync/atomic" 9 10 "github.com/0xsequence/ethkit/ethmonitor" 11 "github.com/goware/logger" 12 ) 13 14 const ( 15 ONE_GWEI = uint64(1e9) 16 ONE_GWEI_MINUS_ONE_WEI = ONE_GWEI - 1 17 ) 18 19 var ( 20 ONE_GWEI_BIG = big.NewInt(int64(ONE_GWEI)) 21 ONE_GWEI_MINUS_ONE_WEI_BIG = big.NewInt(int64(ONE_GWEI_MINUS_ONE_WEI)) 22 BUCKET_RANGE = big.NewInt(int64(5 * ONE_GWEI)) 23 ) 24 25 type GasGauge struct { 26 log logger.Logger 27 monitor *ethmonitor.Monitor 28 chainID uint64 29 gasPriceBidReader GasPriceReader 30 gasPricePaidReader GasPriceReader 31 suggestedGasPriceBid SuggestedGasPrice 32 suggestedPaidGasPrice SuggestedGasPrice 33 useEIP1559 bool // TODO: currently not in use, but once we think about block utilization, then will be useful 34 minGasPrice *big.Int 35 36 ctx context.Context 37 ctxStop context.CancelFunc 38 running int32 39 } 40 41 type SuggestedGasPrice struct { 42 Instant uint64 `json:"instant"` // in gwei 43 Fast uint64 `json:"fast"` 44 Standard uint64 `json:"standard"` 45 Slow uint64 `json:"slow"` 46 47 InstantWei *big.Int `json:"instantWei"` 48 FastWei *big.Int `json:"fastWei"` 49 StandardWei *big.Int `json:"standardWei"` 50 SlowWei *big.Int `json:"slowWei"` 51 52 BlockNum *big.Int `json:"blockNum"` 53 BlockTime uint64 `json:"blockTime"` 54 } 55 56 func (p SuggestedGasPrice) WithMin(minWei *big.Int) SuggestedGasPrice { 57 if minWei == nil { 58 minWei = new(big.Int) 59 } 60 61 minGwei := new(big.Int).Div( 62 new(big.Int).Add( 63 minWei, 64 ONE_GWEI_MINUS_ONE_WEI_BIG, 65 ), 66 ONE_GWEI_BIG, 67 ).Uint64() 68 69 p.Instant = max(minGwei, p.Instant) 70 p.Fast = max(minGwei, p.Fast) 71 p.Standard = max(minGwei, p.Standard) 72 p.Slow = max(minGwei, p.Slow) 73 74 p.InstantWei = bigIntMax(minWei, p.InstantWei) 75 p.FastWei = bigIntMax(minWei, p.FastWei) 76 p.StandardWei = bigIntMax(minWei, p.StandardWei) 77 p.SlowWei = bigIntMax(minWei, p.SlowWei) 78 79 return p 80 } 81 82 func NewGasGaugeWei(log logger.Logger, monitor *ethmonitor.Monitor, minGasPriceInWei uint64, useEIP1559 bool) (*GasGauge, error) { 83 if minGasPriceInWei == 0 { 84 return nil, fmt.Errorf("minGasPriceInWei cannot be 0, pass at least 1") 85 } 86 chainID, err := monitor.Provider().ChainID(context.TODO()) 87 if err != nil { 88 return nil, fmt.Errorf("unable to get chain ID: %w", err) 89 } 90 gasPriceBidReader, ok := CustomGasPriceBidReaders[chainID.Uint64()] 91 if !ok { 92 gasPriceBidReader = DefaultGasPriceBidReader 93 } 94 gasPricePaidReader, ok := CustomGasPricePaidReaders[chainID.Uint64()] 95 if !ok { 96 gasPricePaidReader = DefaultGasPricePaidReader 97 } 98 return &GasGauge{ 99 log: log, 100 monitor: monitor, 101 chainID: chainID.Uint64(), 102 gasPriceBidReader: gasPriceBidReader, 103 gasPricePaidReader: gasPricePaidReader, 104 minGasPrice: big.NewInt(int64(minGasPriceInWei)), 105 useEIP1559: useEIP1559, 106 }, nil 107 } 108 109 func NewGasGauge(log logger.Logger, monitor *ethmonitor.Monitor, minGasPriceInGwei uint64, useEIP1559 bool) (*GasGauge, error) { 110 if minGasPriceInGwei >= ONE_GWEI { 111 return nil, fmt.Errorf("minGasPriceInGwei argument expected to be passed as Gwei, but your units look like wei") 112 } 113 if minGasPriceInGwei == 0 { 114 return nil, fmt.Errorf("minGasPriceInGwei cannot be 0, pass at least 1") 115 } 116 gasGauge, err := NewGasGaugeWei(log, monitor, minGasPriceInGwei*ONE_GWEI, useEIP1559) 117 if err != nil { 118 return nil, err 119 } 120 gasGauge.minGasPrice.Mul(big.NewInt(int64(minGasPriceInGwei)), ONE_GWEI_BIG) 121 return gasGauge, nil 122 } 123 124 func (g *GasGauge) Run(ctx context.Context) error { 125 if g.IsRunning() { 126 return fmt.Errorf("ethgas: already running") 127 } 128 129 g.ctx, g.ctxStop = context.WithCancel(ctx) 130 131 atomic.StoreInt32(&g.running, 1) 132 defer atomic.StoreInt32(&g.running, 0) 133 134 return g.run() 135 } 136 137 func (g *GasGauge) Stop() { 138 g.ctxStop() 139 } 140 141 func (g *GasGauge) IsRunning() bool { 142 return atomic.LoadInt32(&g.running) == 1 143 } 144 145 func (g *GasGauge) SuggestedGasPrice() SuggestedGasPrice { 146 return g.SuggestedPaidGasPrice() 147 } 148 149 func (g *GasGauge) SuggestedGasPriceBid() SuggestedGasPrice { 150 return g.suggestedGasPriceBid.WithMin(g.minGasPrice) 151 } 152 153 func (g *GasGauge) SuggestedPaidGasPrice() SuggestedGasPrice { 154 return g.suggestedPaidGasPrice.WithMin(g.minGasPrice) 155 } 156 157 func (g *GasGauge) Subscribe() ethmonitor.Subscription { 158 return g.monitor.Subscribe("ethgas") 159 } 160 161 func (g *GasGauge) run() error { 162 sub := g.monitor.Subscribe("ethgas:run") 163 defer sub.Unsubscribe() 164 165 bidEMA := newEMAs(0.5) 166 paidEMA := newEMAs(0.5) 167 168 for { 169 select { 170 171 // service is stopping 172 case <-g.ctx.Done(): 173 return nil 174 175 // eth monitor has stopped 176 case <-sub.Done(): 177 return fmt.Errorf("ethmonitor has stopped so the gauge cannot continue, stopping") 178 179 // received new mined block from ethmonitor 180 case blocks := <-sub.Blocks(): 181 latestBlock := blocks.LatestBlock() 182 if latestBlock == nil { 183 continue 184 } 185 186 // read gas price bids from block 187 prices := g.gasPriceBidReader(latestBlock) 188 gasPriceBids := make([]*big.Int, 0, len(prices)) 189 // skip prices which are outliers / "deals with miner" 190 for _, price := range prices { 191 if price.Cmp(g.minGasPrice) >= 0 { 192 gasPriceBids = append(gasPriceBids, price) 193 } 194 } 195 // sort gas list from low to high 196 sort.Slice(gasPriceBids, func(i, j int) bool { 197 return gasPriceBids[i].Cmp(gasPriceBids[j]) < 0 198 }) 199 200 // read paid gas prices from block 201 prices = g.gasPricePaidReader(latestBlock) 202 paidGasPrices := make([]*big.Int, 0, len(prices)) 203 // skip prices which are outliers / "deals with miner" 204 for _, price := range prices { 205 if price.Cmp(g.minGasPrice) >= 0 { 206 paidGasPrices = append(paidGasPrices, price) 207 } 208 } 209 // sort gas list from low to high 210 sort.Slice(paidGasPrices, func(i, j int) bool { 211 return paidGasPrices[i].Cmp(paidGasPrices[j]) < 0 212 }) 213 214 updatedGasPriceBid := bidEMA.update(gasPriceBids, g.minGasPrice) 215 updatedPaidGasPrice := paidEMA.update(paidGasPrices, g.minGasPrice) 216 if updatedGasPriceBid != nil || updatedPaidGasPrice != nil { 217 if updatedGasPriceBid != nil { 218 updatedGasPriceBid.BlockNum = latestBlock.Number() 219 updatedGasPriceBid.BlockTime = latestBlock.Time() 220 g.suggestedGasPriceBid = *updatedGasPriceBid 221 } 222 if updatedPaidGasPrice != nil { 223 updatedPaidGasPrice.BlockNum = latestBlock.Number() 224 updatedPaidGasPrice.BlockTime = latestBlock.Time() 225 g.suggestedPaidGasPrice = *updatedPaidGasPrice 226 } 227 } 228 } 229 } 230 } 231 232 type emas struct { 233 instant, fast, standard, slow *EMA 234 } 235 236 func newEMAs(decay float64) *emas { 237 return &emas{ 238 instant: NewEMA(decay), 239 fast: NewEMA(decay), 240 standard: NewEMA(decay), 241 slow: NewEMA(decay), 242 } 243 } 244 245 func (e *emas) update(prices []*big.Int, minPrice *big.Int) *SuggestedGasPrice { 246 if len(prices) == 0 { 247 return nil 248 } 249 250 // calculate gas price samples via histogram method 251 hist := gasPriceHistogram(prices) 252 high, mid, low := hist.samplePrices() 253 254 if high.Sign() == 0 || mid.Sign() == 0 || low.Sign() == 0 { 255 return nil 256 } 257 258 // get gas price from list at percentile position (old method) 259 // high = percentileValue(gasPrices, 0.9) / ONE_GWEI // expensive 260 // mid = percentileValue(gasPrices, 0.75) / ONE_GWEI // mid 261 // low = percentileValue(gasPrices, 0.4) / ONE_GWEI // low 262 263 // TODO: lets consider the block GasLimit, GasUsed, and multipler of the node 264 // so we can account for the utilization of a block on the network and consider it as a factor of the gas price 265 266 instant := bigIntMax(high, minPrice) 267 fast := bigIntMax(mid, minPrice) 268 standard := bigIntMax(low, minPrice) 269 slow := bigIntMax(new(big.Int).Div(new(big.Int).Mul(standard, big.NewInt(85)), big.NewInt(100)), minPrice) 270 271 // tick 272 e.instant.Tick(instant) 273 e.fast.Tick(fast) 274 e.standard.Tick(standard) 275 e.slow.Tick(slow) 276 277 // compute final suggested gas price by averaging the samples 278 // over a period of time 279 return &SuggestedGasPrice{ 280 InstantWei: e.instant.Value(), 281 FastWei: e.fast.Value(), 282 StandardWei: e.standard.Value(), 283 SlowWei: e.slow.Value(), 284 Instant: new(big.Int).Div(e.instant.Value(), ONE_GWEI_BIG).Uint64(), 285 Fast: new(big.Int).Div(e.fast.Value(), ONE_GWEI_BIG).Uint64(), 286 Standard: new(big.Int).Div(e.standard.Value(), ONE_GWEI_BIG).Uint64(), 287 Slow: new(big.Int).Div(e.slow.Value(), ONE_GWEI_BIG).Uint64(), 288 } 289 } 290 291 func gasPriceHistogram(list []*big.Int) histogram { 292 if len(list) == 0 { 293 return histogram{} 294 } 295 296 min := new(big.Int).Set(list[0]) 297 hist := histogram{} 298 299 b1 := new(big.Int).Set(min) 300 b2 := new(big.Int).Add(min, BUCKET_RANGE) 301 h := uint64(0) 302 x := 0 303 304 for _, v := range list { 305 gp := new(big.Int).Set(v) 306 307 fit: 308 if gp.Cmp(b1) >= 0 && gp.Cmp(b2) < 0 { 309 x++ 310 if h == 0 { 311 h++ 312 hist = append(hist, histogramBucket{value: new(big.Int).Set(b1), count: 1}) 313 } else { 314 h++ 315 hist[len(hist)-1].count = h 316 } 317 } else { 318 h = 0 319 b1.Add(b1, BUCKET_RANGE) 320 b2.Add(b2, BUCKET_RANGE) 321 goto fit 322 } 323 } 324 325 // trim over-paying outliers 326 hist2 := hist.trimOutliers() 327 sort.Slice(hist2, hist2.sortByValue) 328 329 return hist2 330 } 331 332 func percentileValue(list []uint64, percentile float64) uint64 { 333 return list[int(float64(len(list)-1)*percentile)] 334 } 335 336 func periodEMA(price uint64, group *[]uint64, size int) uint64 { 337 *group = append(*group, price) 338 if len(*group) > size { 339 *group = (*group)[1:] 340 } 341 ema := NewEMA(0.2) 342 for _, v := range *group { 343 ema.Tick(new(big.Int).SetUint64(v)) 344 } 345 return ema.Value().Uint64() 346 } 347 348 type histogram []histogramBucket 349 350 type histogramBucket struct { 351 value *big.Int 352 count uint64 353 } 354 355 func (h histogram) sortByCount(i, j int) bool { 356 if h[i].count > h[j].count { 357 return true 358 } 359 return h[i].count == h[j].count && h[i].value.Cmp(h[j].value) < 0 360 } 361 362 func (h histogram) sortByValue(i, j int) bool { 363 return h[i].value.Cmp(h[j].value) < 0 364 } 365 366 func (h histogram) trimOutliers() histogram { 367 h2 := histogram{} 368 for _, v := range h { 369 h2 = append(h2, v) 370 } 371 sort.Slice(h2, h2.sortByValue) 372 373 if len(h2) == 0 { 374 return h2 375 } 376 377 // for the last 25% of buckets, if we see a jump by 200%, then full-stop there 378 x := int(float64(len(h2)) * 0.75) 379 if x == len(h2) || x == 0 { 380 return h2 381 } 382 383 h3 := h2[:x] 384 last := h2[x-1].value 385 for i := x; i < len(h2); i++ { 386 v := h2[i].value 387 if v.Cmp(new(big.Int).Mul(big.NewInt(2), last)) >= 0 { 388 break 389 } 390 h3 = append(h3, h2[i]) 391 last = v 392 } 393 394 return h3 395 } 396 397 func (h histogram) percentileValue(percentile float64) *big.Int { 398 if percentile < 0 { 399 percentile = 0 400 } else if percentile > 1 { 401 percentile = 1 402 } 403 404 numSamples := uint64(0) 405 406 for _, bucket := range h { 407 numSamples += bucket.count 408 } 409 410 // suppose numSamples = 100 411 // suppose percentile = 0.8 412 // then we want to find the 80th sample and return its value 413 414 // if percentile = 80%, then i want index = numSamples * 80% 415 index := uint64(float64(numSamples) * percentile) 416 // index = numSamples - 1 417 418 // find the sample at index, then return its value 419 numberOfSamplesConsidered := uint64(0) 420 for _, bucket := range h { 421 if numberOfSamplesConsidered+bucket.count > index { 422 return new(big.Int).Set(bucket.value) 423 } 424 425 numberOfSamplesConsidered += bucket.count 426 } 427 428 return new(big.Int).Set(h[len(h)-1].value) 429 } 430 431 // returns sample inputs for: instant, fast, standard 432 func (h histogram) samplePrices() (*big.Int, *big.Int, *big.Int) { 433 if len(h) == 0 { 434 return big.NewInt(0), big.NewInt(0), big.NewInt(0) 435 } 436 437 sort.Slice(h, h.sortByValue) 438 439 high := h.percentileValue(0.7) // instant 440 mid := h.percentileValue(0.6) // fast 441 low := h.percentileValue(0.5) // standard 442 443 return high, mid, low 444 } 445 446 func bigIntMax(a, b *big.Int) *big.Int { 447 if a == nil { 448 a = new(big.Int) 449 } 450 if b == nil { 451 b = new(big.Int) 452 } 453 454 if a.Cmp(b) >= 0 { 455 return new(big.Int).Set(a) 456 } else { 457 return new(big.Int).Set(b) 458 } 459 }