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  }