github.com/unicornultrafoundation/go-u2u@v1.0.0-rc1.0.20240205080301-e74a83d3fadc/gossip/gasprice/reactive.go (about)

     1  package gasprice
     2  
     3  import (
     4  	"math/big"
     5  	"sort"
     6  	"sync/atomic"
     7  	"time"
     8  
     9  	"github.com/unicornultrafoundation/go-helios/utils/piecefunc"
    10  	"github.com/unicornultrafoundation/go-u2u/core/types"
    11  )
    12  
    13  const (
    14  	percentilesPerStat = 20
    15  	statUpdatePeriod   = 1 * time.Second
    16  	statsBuffer        = int((15 * time.Second) / statUpdatePeriod)
    17  	maxGasToIndex      = 40000000
    18  )
    19  
    20  type txpoolStat struct {
    21  	totalGas    uint64
    22  	percentiles [percentilesPerStat]*big.Int
    23  }
    24  
    25  type circularTxpoolStats struct {
    26  	stats     [statsBuffer]txpoolStat
    27  	i         int
    28  	activated uint32
    29  	avg       atomic.Value
    30  }
    31  
    32  var certaintyToGasAbove = piecefunc.NewFunc([]piecefunc.Dot{
    33  	{
    34  		X: 0,
    35  		Y: 50000000,
    36  	},
    37  	{
    38  		X: 0.2 * DecimalUnit,
    39  		Y: 20000000,
    40  	},
    41  	{
    42  		X: 0.5 * DecimalUnit,
    43  		Y: 8000000,
    44  	},
    45  	{
    46  		X: DecimalUnit,
    47  		Y: 0,
    48  	},
    49  })
    50  
    51  func (gpo *Oracle) reactiveGasPrice(certainty uint64) *big.Int {
    52  	gasAbove := certaintyToGasAbove(certainty)
    53  
    54  	return gpo.c.getGasPriceForGasAbove(gasAbove)
    55  }
    56  
    57  func (gpo *Oracle) txpoolStatsTick() {
    58  	c := &gpo.c
    59  	// calculate txpool statistic and push into the circular buffer
    60  	c.stats[c.i] = gpo.calcTxpoolStat()
    61  	c.i = (c.i + 1) % len(c.stats)
    62  	// calculate average of statistics in the circular buffer
    63  	c.avg.Store(c.calcAvg())
    64  }
    65  
    66  func (gpo *Oracle) txpoolStatsLoop() {
    67  	ticker := time.NewTicker(statUpdatePeriod)
    68  	defer ticker.Stop()
    69  	for i := uint32(0); ; i++ {
    70  		select {
    71  		case <-ticker.C:
    72  			// calculate more frequently after first request
    73  			if atomic.LoadUint32(&gpo.c.activated) != 0 || i%5 == 0 {
    74  				gpo.txpoolStatsTick()
    75  			}
    76  		case <-gpo.quit:
    77  			return
    78  		}
    79  	}
    80  }
    81  
    82  func (c *circularTxpoolStats) dec(v int) int {
    83  	if v == 0 {
    84  		return len(c.stats) - 1
    85  	}
    86  	return v - 1
    87  }
    88  
    89  // calcAvg calculates average of statistics in the circular buffer
    90  func (c *circularTxpoolStats) calcAvg() txpoolStat {
    91  	avg := txpoolStat{}
    92  	for p := range avg.percentiles {
    93  		avg.percentiles[p] = new(big.Int)
    94  	}
    95  	nonZero := uint64(0)
    96  	for _, s := range c.stats {
    97  		if s.totalGas == 0 {
    98  			continue
    99  		}
   100  		nonZero++
   101  		avg.totalGas += s.totalGas
   102  		for p := range s.percentiles {
   103  			if s.percentiles[p] == nil {
   104  				continue
   105  			}
   106  			avg.percentiles[p].Add(avg.percentiles[p], s.percentiles[p])
   107  		}
   108  	}
   109  	if nonZero == 0 {
   110  		return avg
   111  	}
   112  	avg.totalGas /= nonZero
   113  	nonZeroBn := new(big.Int).SetUint64(nonZero)
   114  	for p := range avg.percentiles {
   115  		avg.percentiles[p].Div(avg.percentiles[p], nonZeroBn)
   116  	}
   117  
   118  	// take maximum from previous 4 stats plus 5%
   119  	rec := txpoolStat{}
   120  	for p := range rec.percentiles {
   121  		rec.percentiles[p] = new(big.Int)
   122  	}
   123  	recI1 := c.dec(c.i)
   124  	recI2 := c.dec(recI1)
   125  	recI3 := c.dec(recI2)
   126  	recI4 := c.dec(recI3)
   127  	for _, s := range []txpoolStat{c.stats[recI1], c.stats[recI2], c.stats[recI3], c.stats[recI4]} {
   128  		for p := range s.percentiles {
   129  			if s.percentiles[p] == nil {
   130  				continue
   131  			}
   132  			if rec.percentiles[p].Cmp(s.percentiles[p]) < 0 {
   133  				rec.percentiles[p].Set(s.percentiles[p])
   134  			}
   135  		}
   136  	}
   137  	// increase by 5%
   138  	for p := range rec.percentiles {
   139  		rec.percentiles[p].Mul(rec.percentiles[p], big.NewInt(21))
   140  		rec.percentiles[p].Div(rec.percentiles[p], big.NewInt(20))
   141  	}
   142  
   143  	// return minimum from max(recent two stats * 1.05) and avg stats
   144  	res := txpoolStat{}
   145  	res.totalGas = avg.totalGas
   146  	for _, s := range []txpoolStat{avg, rec} {
   147  		for p := range s.percentiles {
   148  			if res.percentiles[p] == nil || res.percentiles[p].Cmp(s.percentiles[p]) > 0 {
   149  				res.percentiles[p] = s.percentiles[p]
   150  			}
   151  		}
   152  	}
   153  
   154  	return res
   155  }
   156  
   157  func (c *circularTxpoolStats) getGasPriceForGasAbove(gas uint64) *big.Int {
   158  	atomic.StoreUint32(&c.activated, 1)
   159  	avg_c := c.avg.Load()
   160  	if avg_c == nil {
   161  		return new(big.Int)
   162  	}
   163  	avg := avg_c.(txpoolStat)
   164  	if avg.totalGas == 0 {
   165  		return new(big.Int)
   166  	}
   167  	if gas > maxGasToIndex {
   168  		// extrapolate linearly
   169  		v := new(big.Int).Mul(avg.percentiles[len(avg.percentiles)-1], new(big.Int).SetUint64(maxGasToIndex))
   170  		v.Div(v, new(big.Int).SetUint64(gas+1))
   171  		return v
   172  	}
   173  	p0 := gas * uint64(len(avg.percentiles)) / maxGasToIndex
   174  	if p0 >= uint64(len(avg.percentiles))-1 {
   175  		return avg.percentiles[len(avg.percentiles)-1]
   176  	}
   177  	// interpolate linearly
   178  	p1 := p0 + 1
   179  	x := gas
   180  	x0, x1 := p0*maxGasToIndex/uint64(len(avg.percentiles)), p1*maxGasToIndex/uint64(len(avg.percentiles))
   181  	y0, y1 := avg.percentiles[p0], avg.percentiles[p1]
   182  	return div64I(addBigI(mul64N(y0, x1-x), mul64N(y1, x-x0)), x1-x0)
   183  }
   184  
   185  func mul64N(a *big.Int, b uint64) *big.Int {
   186  	return new(big.Int).Mul(a, new(big.Int).SetUint64(b))
   187  }
   188  
   189  func div64I(a *big.Int, b uint64) *big.Int {
   190  	return a.Div(a, new(big.Int).SetUint64(b))
   191  }
   192  
   193  func addBigI(a, b *big.Int) *big.Int {
   194  	return a.Add(a, b)
   195  }
   196  
   197  func (c *circularTxpoolStats) totalGas() uint64 {
   198  	atomic.StoreUint32(&c.activated, 1)
   199  	avgC := c.avg.Load()
   200  	if avgC == nil {
   201  		return 0
   202  	}
   203  	avg := avgC.(txpoolStat)
   204  	return avg.totalGas
   205  }
   206  
   207  // calcTxpoolStat retrieves txpool transactions and calculates statistics
   208  func (gpo *Oracle) calcTxpoolStat() txpoolStat {
   209  	txsMap := gpo.backend.PendingTxs()
   210  	s := txpoolStat{}
   211  	if len(txsMap) == 0 {
   212  		// short circuit if empty txpool
   213  		return s
   214  	}
   215  	// take only one tx from each account
   216  	txs := make(types.Transactions, 0, 1000)
   217  	for _, aTxs := range txsMap {
   218  		txs = append(txs, aTxs[0])
   219  	}
   220  
   221  	// don't index more transactions than needed for GPO purposes
   222  	const maxTxsToIndex = 400
   223  
   224  	minGasPrice := gpo.backend.GetRules().Economy.MinGasPrice
   225  	// txs are sorted from large price to small
   226  	sorted := txs
   227  	sort.Slice(sorted, func(i, j int) bool {
   228  		a, b := sorted[i], sorted[j]
   229  		cmp := a.EffectiveGasTipCmp(b, minGasPrice)
   230  		if cmp == 0 {
   231  			return a.Gas() > b.Gas()
   232  		}
   233  		return cmp > 0
   234  	})
   235  
   236  	for i, tx := range sorted {
   237  		s.totalGas += tx.Gas()
   238  		if s.totalGas > maxGasToIndex || i > maxTxsToIndex {
   239  			sorted = sorted[:i+1]
   240  			break
   241  		}
   242  	}
   243  
   244  	gasCounter := uint64(0)
   245  	p := uint64(0)
   246  	for _, tx := range sorted {
   247  		for p < uint64(len(s.percentiles)) && gasCounter >= p*maxGasToIndex/uint64(len(s.percentiles)) {
   248  			s.percentiles[p] = tx.EffectiveGasTipValue(minGasPrice)
   249  			if s.percentiles[p].Sign() < 0 {
   250  				s.percentiles[p] = minGasPrice
   251  			} else {
   252  				s.percentiles[p].Add(s.percentiles[p], minGasPrice)
   253  			}
   254  			p++
   255  		}
   256  		gasCounter += tx.Gas()
   257  	}
   258  
   259  	return s
   260  }