Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EVM-696] eth_FeeHistory #1669

Merged
merged 17 commits into from
Jul 4, 2023
160 changes: 160 additions & 0 deletions gasprice/feehistory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package gasprice

import (
"encoding/binary"
"errors"
"math"
"math/big"
"sort"
)

var (
ErrInvalidPercentile = errors.New("invalid percentile")
ErrBlockCount = errors.New("blockCount must be greater than 0")
ErrBlockNotFound = errors.New("could not find block")
)

const (
maxBlockRequest = 1024
)

type cacheKey struct {
number uint64
percentiles string
}

// processedFees contains the results of a processed block.
type processedFees struct {
reward []uint64
baseFee uint64
gasUsedRatio float64
}

type txGasAndReward struct {
gasUsed *big.Int
reward *big.Int
}

func (g *GasHelper) FeeHistory(blockCount uint64, newestBlock uint64, rewardPercentiles []float64) (
*uint64, *[]uint64, *[]float64, *[][]uint64, error) {
vcastellm marked this conversation as resolved.
Show resolved Hide resolved
if blockCount < 1 {
return nil, nil, nil, nil, ErrBlockCount
}

if newestBlock > g.backend.Header().Number {
newestBlock = g.backend.Header().Number
}

if blockCount > maxBlockRequest {
blockCount = maxBlockRequest
}

if blockCount > newestBlock {
blockCount = newestBlock
}

for i, p := range rewardPercentiles {
goran-ethernal marked this conversation as resolved.
Show resolved Hide resolved
if p < 0 || p > 100 {
return nil, nil, nil, nil, ErrInvalidPercentile
}

if i > 0 && p < rewardPercentiles[i-1] {
return nil, nil, nil, nil, ErrInvalidPercentile
}
}

var (
oldestBlock = newestBlock - blockCount + 1
baseFeePerGas = make([]uint64, blockCount+1)
gasUsedRatio = make([]float64, blockCount)
reward = make([][]uint64, blockCount)
)

if oldestBlock < 1 {
oldestBlock = 1
}

percentileKey := make([]byte, 8*len(rewardPercentiles))
for i, p := range rewardPercentiles {
binary.LittleEndian.PutUint64(percentileKey[i*8:(i+1)*8], math.Float64bits(p))
}

for i := oldestBlock; i <= newestBlock; i++ {
cacheKey := cacheKey{number: i, percentiles: string(percentileKey)}
//cache is hit, load from cache and continue to next block
if p, ok := g.historyCache.Get(cacheKey); ok {
processedFee, isOk := p.(*processedFees)
if !isOk {
return nil, nil, nil, nil, errors.New("could not convert catched processed fee")
}

baseFeePerGas[i-oldestBlock] = processedFee.baseFee
gasUsedRatio[i-oldestBlock] = processedFee.gasUsedRatio
reward[i-oldestBlock] = processedFee.reward

continue
}

block, ok := g.backend.GetBlockByNumber(i, false)
if !ok {
return nil, nil, nil, nil, ErrBlockNotFound
}

baseFeePerGas[i-oldestBlock] = block.Header.BaseFee
gasUsedRatio[i-oldestBlock] = float64(block.Header.GasUsed) / float64(block.Header.GasLimit)

if len(rewardPercentiles) == 0 {
//reward percentiles not requested, skip rest of this loop
continue
}

reward[i-oldestBlock] = make([]uint64, len(rewardPercentiles))
if len(block.Transactions) == 0 {
for j := range reward[i-oldestBlock] {
reward[i-oldestBlock][j] = 0
}
//no transactions in block, set rewards to 0 and move to next block
continue
}

sorter := make([]*txGasAndReward, len(block.Transactions))

for j, tx := range block.Transactions {
cost := tx.Cost()
sorter[j] = &txGasAndReward{
gasUsed: cost.Sub(cost, tx.Value),
reward: tx.EffectiveTip(block.Header.BaseFee),
}
}

sort.Slice(sorter, func(i, j int) bool {
return sorter[i].reward.Cmp(sorter[j].reward) < 0
})

var txIndex int

sumGasUsed := sorter[0].gasUsed.Uint64()

// calculate reward for each percentile
for c, v := range rewardPercentiles {
thresholdGasUsed := uint64(float64(block.Header.GasUsed) * v / 100)
for sumGasUsed < thresholdGasUsed && txIndex < len(block.Transactions)-1 {
txIndex++
sumGasUsed += sorter[txIndex].gasUsed.Uint64()
}

reward[i-oldestBlock][c] = sorter[txIndex].reward.Uint64()
}

blockFees := &processedFees{
reward: reward[i-oldestBlock],
baseFee: block.Header.BaseFee,
gasUsedRatio: gasUsedRatio[i-oldestBlock],
}
g.historyCache.Add(cacheKey, blockFees)
}

baseFeePerGas[blockCount] = g.backend.Header().BaseFee

return &oldestBlock, &baseFeePerGas, &gasUsedRatio, &reward, nil
}
112 changes: 112 additions & 0 deletions gasprice/feehistory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package gasprice

import (
"errors"
"math/big"
"testing"

"github.com/0xPolygon/polygon-edge/types"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

vcastellm marked this conversation as resolved.
Show resolved Hide resolved
func TestGasHelper_FeeHistory(t *testing.T) {
t.Parallel()

var cases = []struct {
Name string
ExpectedOldestBlock uint64
ExpectedBaseFeePerGas []uint64
ExpectedGasUsedRatio []float64
ExpectedRewards [][]uint64
BlockRange uint64
NewestBlock uint64
RewardPercentiles []float64
Error bool
GetBackend func() Blockchain
}{
{
Name: "Block does not exist",
Error: true,
BlockRange: 10,
NewestBlock: 30,
RewardPercentiles: []float64{15, 20},
GetBackend: func() Blockchain {
header := &types.Header{
Number: 0,
Hash: types.StringToHash("some header"),
}
backend := new(backendMock)
backend.On("Header").Return(header)
backend.On("GetBlockByNumber", mock.Anything, true).Return(errors.New("block not found"))
goran-ethernal marked this conversation as resolved.
Show resolved Hide resolved

return backend
},
},
{
Name: "Block Range < 1",
Error: true,
BlockRange: 0,
NewestBlock: 30,
RewardPercentiles: []float64{15, 20},
GetBackend: func() Blockchain {
backend := createTestBlocks(t, 30)
createTestTxs(t, backend, 3, 200)

return backend
},
},
{
Name: "Invalid rewardPercentile",
Error: true,
BlockRange: 10,
NewestBlock: 30,
RewardPercentiles: []float64{101, 0},
GetBackend: func() Blockchain {
backend := createTestBlocks(t, 50)
createTestTxs(t, backend, 1, 200)

return backend
},
},
}

for _, tc := range cases {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()

backend := tc.GetBackend()
gasHelper, err := NewGasHelper(DefaultGasHelperConfig, backend)
require.NoError(t, err)
oldestBlock, baseFeePerGas, gasUsedRatio, rewards, err := gasHelper.FeeHistory(tc.BlockRange, tc.NewestBlock, tc.RewardPercentiles)

if tc.Error {
require.Error(t, err)
} else {
require.NoError(t, err)
require.True(t, oldestBlock == &tc.ExpectedOldestBlock)
require.True(t, baseFeePerGas == &tc.ExpectedBaseFeePerGas)
require.True(t, gasUsedRatio == &tc.ExpectedGasUsedRatio)
require.True(t, rewards == &tc.ExpectedRewards)
}
})
}
}

var _ Blockchain = (*backendMock)(nil)

func (b *backendMock) GetBlockByNumber(n uint64, full bool) (*types.Block, bool) {
if len(b.blocks) == 0 {
args := b.Called(n, full)

return args.Get(0).(*types.Block), args.Get(1).(bool) //nolint:forcetypeassert
}

block, exists := b.blocks[uint64ToHash(n)]

return block, exists
}
func uint64ToHash(n uint64) types.Hash {
return types.BytesToHash(big.NewInt(int64(n)).Bytes())
}
15 changes: 13 additions & 2 deletions gasprice/gasprice.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/0xPolygon/polygon-edge/chain"
"github.com/0xPolygon/polygon-edge/crypto"
"github.com/0xPolygon/polygon-edge/types"
lru "github.com/hashicorp/golang-lru"
"github.com/umbracle/ethgo"
)

Expand Down Expand Up @@ -44,6 +45,7 @@ type Config struct {

// Blockchain is the interface representing blockchain
type Blockchain interface {
GetBlockByNumber(number uint64, full bool) (*types.Block, bool)
GetBlockByHash(hash types.Hash, full bool) (*types.Block, bool)
Header() *types.Header
Config() *chain.Params
Expand All @@ -53,6 +55,7 @@ type Blockchain interface {
type GasStore interface {
// MaxPriorityFeePerGas calculates the priority fee needed for transaction to be included in a block
MaxPriorityFeePerGas() (*big.Int, error)
FeeHistory(uint64, uint64, []float64) (*uint64, *[]uint64, *[]float64, *[][]uint64, error)
Stefan-Ethernal marked this conversation as resolved.
Show resolved Hide resolved
}

var _ GasStore = (*GasHelper)(nil)
Expand All @@ -78,15 +81,22 @@ type GasHelper struct {
lastHeaderHash types.Hash

lock sync.Mutex

historyCache *lru.Cache
}

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

cache, err := lru.New(100)
if err != nil {
return nil, err
}

return &GasHelper{
numOfBlocksToCheck: config.NumOfBlocksToCheck,
pricePercentile: pricePercentile,
Expand All @@ -95,7 +105,8 @@ func NewGasHelper(config *Config, backend Blockchain) *GasHelper {
lastPrice: config.LastPrice,
maxPrice: config.MaxPrice,
backend: backend,
}
historyCache: cache,
}, nil
}

// MaxPriorityFeePerGas calculates the priority fee needed for transaction to be included in a block
Expand Down
3 changes: 2 additions & 1 deletion gasprice/gasprice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ func TestGasHelper_MaxPriorityFeePerGas(t *testing.T) {
t.Parallel()

backend := tc.GetBackend()
gasHelper := NewGasHelper(DefaultGasHelperConfig, backend)
gasHelper, err := NewGasHelper(DefaultGasHelperConfig, backend)
require.NoError(t, err)
price, err := gasHelper.MaxPriorityFeePerGas()

if tc.Error {
Expand Down
Loading
Loading