This repository has been archived by the owner on Oct 31, 2024. It is now read-only.
forked from 0xPolygon/polygon-edge
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[EVM-697]: Implement
eth_maxPriorityFeePerGas
(0xPolygon#1629)
* Initial change * UTs * Small fix * Small fix * Comments fix * Comments fix * Comments fix * Comments fix
- Loading branch information
1 parent
06c027d
commit d98b3e5
Showing
4 changed files
with
541 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.