Skip to content

Commit

Permalink
[EVM-697]: Implement eth_maxPriorityFeePerGas (#1629)
Browse files Browse the repository at this point in the history
* Initial change

* UTs

* Small fix

* Small fix

* Comments fix

* Comments fix

* Comments fix

* Comments fix
  • Loading branch information
goran-ethernal committed Jun 21, 2023
1 parent 06c027d commit d98b3e5
Show file tree
Hide file tree
Showing 4 changed files with 541 additions and 0 deletions.
254 changes: 254 additions & 0 deletions gasprice/gasprice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package gasprice

import (
"fmt"
"math/big"
"sort"
"sync"

"github.com/0xPolygon/polygon-edge/chain"
"github.com/0xPolygon/polygon-edge/crypto"
"github.com/0xPolygon/polygon-edge/types"
"github.com/umbracle/ethgo"
)

const couldNotFoundBlockFormat = "could not find block. Number: %d, Hash: %s"

// DefaultGasHelperConfig is the default config for gas helper (as per ethereum)
var DefaultGasHelperConfig = &Config{
NumOfBlocksToCheck: 20,
PricePercentile: 60,
SampleNumber: 3,
MaxPrice: ethgo.Gwei(500),
LastPrice: ethgo.Gwei(1),
IgnorePrice: big.NewInt(2), // 2 wei
}

// Config is a struct that holds configuration of GasHelper
type Config struct {
// NumOfBlocksToCheck is the number of blocks to sample
NumOfBlocksToCheck uint64
// PricePercentile is the sample percentile of transactions in a block
PricePercentile uint64
// SampleNumber is number of transactions sampled in a block
SampleNumber uint64
// MaxPrice is the tip max price
MaxPrice *big.Int
// LastPrice is the last price returned for maxPriorityFeePerGas
// when starting node it will be some default value
LastPrice *big.Int
// IgnorePrice is the lowest price to take into consideration
// when collecting transactions
IgnorePrice *big.Int
}

// Blockchain is the interface representing blockchain
type Blockchain interface {
GetBlockByHash(hash types.Hash, full bool) (*types.Block, bool)
Header() *types.Header
Config() *chain.Params
}

// GasStore interface is providing functions regarding gas and fees
type GasStore interface {
// MaxPriorityFeePerGas calculates the priority fee needed for transaction to be included in a block
MaxPriorityFeePerGas() (*big.Int, error)
}

var _ GasStore = (*GasHelper)(nil)

// GasHelper struct implements functions from the GasStore interface
type GasHelper struct {
// numOfBlocksToCheck is the number of blocks to sample
numOfBlocksToCheck uint64
// pricePercentile is the sample percentile of transactions in a block
pricePercentile uint64
// sampleNumber is number of transactions sampled in a block
sampleNumber uint64
// maxPrice is the tip max price
maxPrice *big.Int
// lastPrice is the last price returned for maxPriorityFeePerGas
lastPrice *big.Int
// ignorePrice is the lowest price to take into consideration
// when collecting transactions
ignorePrice *big.Int
// backend is an abstraction of blockchain
backend Blockchain
// lastHeaderHash is the last header for which maxPriorityFeePerGas was returned
lastHeaderHash types.Hash

lock sync.Mutex
}

// NewGasHelper is the constructor function for GasHelper struct
func NewGasHelper(config *Config, backend Blockchain) *GasHelper {
pricePercentile := config.PricePercentile
if pricePercentile > 100 {
pricePercentile = 100
}

return &GasHelper{
numOfBlocksToCheck: config.NumOfBlocksToCheck,
pricePercentile: pricePercentile,
sampleNumber: config.SampleNumber,
ignorePrice: config.IgnorePrice,
lastPrice: config.LastPrice,
maxPrice: config.MaxPrice,
backend: backend,
}
}

// MaxPriorityFeePerGas calculates the priority fee needed for transaction to be included in a block
// The function does following:
// - takes chain header
// - iterates for numOfBlocksToCheck from chain header to previous blocks
// - collects at most the sample number of sorted transactions in block
// - if not enough transactions were collected and their tips, go through some more blocks to get
// more accurate calculation
// - when enough transactions and their tips are collected, take the one that is in pricePercentile
// - if given price is larger then maxPrice then return the maxPrice
func (g *GasHelper) MaxPriorityFeePerGas() (*big.Int, error) {
currentHeader := g.backend.Header()

currentBlock, found := g.backend.GetBlockByHash(currentHeader.Hash, true)
if !found {
return nil, fmt.Errorf(couldNotFoundBlockFormat, currentHeader.Number, currentHeader.Hash)
}

g.lock.Lock()
lastPrice := g.lastPrice
lastHeader := g.lastHeaderHash
g.lock.Unlock()

if currentHeader.Hash == lastHeader {
// small optimization, if we calculated already the price for given block
return new(big.Int).Set(lastPrice), nil
}

var allPrices []*big.Int

collectPrices := func(block *types.Block) error {
baseFee := block.Header.BaseFee
txSorter := newTxByEffectiveTipSorter(block.Transactions, baseFee)
sort.Sort(txSorter)

blockMiner := types.BytesToAddress(block.Header.Miner)
signer := crypto.NewSigner(g.backend.Config().Forks.At(block.Number()),
uint64(g.backend.Config().ChainID))
blockTxPrices := make([]*big.Int, 0)

for _, tx := range txSorter.txs {
tip := tx.EffectiveTip(baseFee)

if tip.Cmp(g.ignorePrice) == -1 {
// ignore transactions with tip lower than ignore price
continue
}

sender, err := signer.Sender(tx)
if err != nil {
return fmt.Errorf("could not get sender of transaction: %s. Error: %w", tx.Hash, err)
}

if sender != blockMiner {
blockTxPrices = append(blockTxPrices, tip)

// if sample number of txs from block is reached,
// don't process any more txs
if len(blockTxPrices) >= int(g.sampleNumber) {
break
}
}
}

if len(blockTxPrices) == 0 {
// either block is empty or all transactions in block are sent by the miner.
// in this case add the latests calculated price for sampling
blockTxPrices = append(blockTxPrices, lastPrice)
}

// add the block prices to the slice of all prices
allPrices = append(allPrices, blockTxPrices...)

return nil
}

// iterate from current block to previous blocks determined by numOfBlocksToCheck
// if chain doesn't have that many blocks, we need to stop the loop (currentBlock.Number() > 0)
for i := uint64(0); i < g.numOfBlocksToCheck && currentBlock.Number() > 0; i++ {
if err := collectPrices(currentBlock); err != nil {
return nil, err
}

currentBlock, found = g.backend.GetBlockByHash(currentBlock.ParentHash(), true)
if !found {
return nil, fmt.Errorf(couldNotFoundBlockFormat, currentHeader.Number, currentHeader.Hash)
}
}

// at least amount of transactions to get
minNumOfTx := int(g.numOfBlocksToCheck) * 2
// collect some more blocks and transactions if not enough transactions were collected
for len(allPrices) < minNumOfTx && currentBlock.Number() > 0 {
if err := collectPrices(currentBlock); err != nil {
return nil, err
}
}

price := lastPrice

if len(allPrices) > 0 {
// sort prices from lowest to highest
sort.Slice(allPrices, func(i, j int) bool {
return allPrices[i].Cmp(allPrices[j]) < 0
})
// take the biggest price that is in the configured percentage
// by default it's 60, so it will take the price on that percentage
// of all prices in the array
price = allPrices[(len(allPrices)-1)*int(g.pricePercentile)/100]
}

if price.Cmp(g.maxPrice) > 0 {
// if price is larger than the configured max price
// return max price
price = new(big.Int).Set(g.maxPrice)
}

// cache the calculated price and header hash
g.lock.Lock()
g.lastPrice = price
g.lastHeaderHash = currentHeader.Hash
g.lock.Unlock()

return price, nil
}

// txSortedByEffectiveTip sorts transactions by effective tip from smallest to largest
type txSortedByEffectiveTip struct {
txs []*types.Transaction
baseFee uint64
}

// newTxByEffectiveTipSorter is constructor function for txSortedByEffectiveTip
func newTxByEffectiveTipSorter(txs []*types.Transaction, baseFee uint64) *txSortedByEffectiveTip {
return &txSortedByEffectiveTip{
txs: txs,
baseFee: baseFee,
}
}

// Len is implementation of sort.Interface
func (t *txSortedByEffectiveTip) Len() int { return len(t.txs) }

// Swap is implementation of sort.Interface
func (t *txSortedByEffectiveTip) Swap(i, j int) {
t.txs[i], t.txs[j] = t.txs[j], t.txs[i]
}

// Less is implementation of sort.Interface
func (t *txSortedByEffectiveTip) Less(i, j int) bool {
tip1 := t.txs[i].EffectiveTip(t.baseFee)
tip2 := t.txs[j].EffectiveTip(t.baseFee)

return tip1.Cmp(tip2) < 0
}
Loading

0 comments on commit d98b3e5

Please sign in to comment.