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

[api] impl eth_feeHistory #4527

Merged
merged 6 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/coreservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ type (
SuggestGasPrice() (uint64, error)
// SuggestGasTipCap suggests gas tip cap
SuggestGasTipCap() (*big.Int, error)
// FeeHistory returns the fee history
FeeHistory(ctx context.Context, blocks, lastBlock uint64, rewardPercentiles []float64) (uint64, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error)
// EstimateGasForAction estimates gas for action
EstimateGasForAction(ctx context.Context, in *iotextypes.Action) (uint64, error)
// EpochMeta gets epoch metadata
Expand Down Expand Up @@ -600,6 +602,11 @@ func (core *coreService) SuggestGasTipCap() (*big.Int, error) {
return fee, nil
}

// FeeHistory returns the fee history
func (core *coreService) FeeHistory(ctx context.Context, blocks, lastBlock uint64, rewardPercentiles []float64) (uint64, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error) {
return core.gs.FeeHistory(ctx, blocks, lastBlock, rewardPercentiles)
}

// EstimateGasForAction estimates gas for action
func (core *coreService) EstimateGasForAction(ctx context.Context, in *iotextypes.Action) (uint64, error) {
selp, err := (&action.Deserializer{}).SetEvmNetworkID(core.EVMNetworkID()).ActionToSealedEnvelope(in)
Expand Down
39 changes: 39 additions & 0 deletions api/web3server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"math/big"
"strconv"
"time"

Expand Down Expand Up @@ -167,6 +168,8 @@ func (svr *web3Handler) handleWeb3Req(ctx context.Context, web3Req *gjson.Result
res, err = svr.gasPrice()
case "eth_maxPriorityFeePerGas":
res, err = svr.maxPriorityFee()
case "eth_feeHistory":
res, err = svr.feeHistory(ctx, web3Req)
case "eth_getBlockByHash":
res, err = svr.getBlockByHash(web3Req)
case "eth_chainId":
Expand Down Expand Up @@ -325,6 +328,42 @@ func (svr *web3Handler) maxPriorityFee() (interface{}, error) {
return uint64ToHex(ret.Uint64()), nil
}

func (svr *web3Handler) feeHistory(ctx context.Context, in *gjson.Result) (interface{}, error) {
blkCnt, newestBlk, rewardPercentiles := in.Get("params.0"), in.Get("params.1"), in.Get("params.2")
if !blkCnt.Exists() || !newestBlk.Exists() {
return nil, errInvalidFormat
}
blocks, err := strconv.ParseUint(blkCnt.String(), 10, 64)
if err != nil {
return nil, err
}
lastBlock, err := svr.parseBlockNumber(newestBlk.String())
if err != nil {
return nil, err
}
rewardPercents := []float64{}
if rewardPercentiles.Exists() {
for _, p := range rewardPercentiles.Array() {
rewardPercents = append(rewardPercents, p.Float())
}
}
oldest, reward, baseFee, gasRatio, blobBaseFee, blobGasRatio, err := svr.coreService.FeeHistory(ctx, blocks, lastBlock, rewardPercents)
if err != nil {
return nil, err
}

return &feeHistoryResult{
OldestBlock: uint64ToHex(oldest),
BaseFeePerGas: mapper(baseFee, bigIntToHex),
GasUsedRatio: gasRatio,
BaseFeePerBlobGas: mapper(blobBaseFee, bigIntToHex),
BlobGasUsedRatio: blobGasRatio,
Reward: mapper(reward, func(a []*big.Int) []string {
return mapper(a, bigIntToHex)
}),
}, nil
}

func (svr *web3Handler) getChainID() (interface{}, error) {
return uint64ToHex(uint64(svr.coreService.EVMNetworkID())), nil
}
Expand Down
36 changes: 36 additions & 0 deletions api/web3server_integrity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ func TestWeb3ServerIntegrity(t *testing.T) {
t.Run("eth_getStorageAt", func(t *testing.T) {
getStorageAt(t, handler, bc, dao, actPool)
})

t.Run("eth_feeHistory", func(t *testing.T) {
feeHistory(t, handler, bc, dao, actPool)
})
}

func setupTestServer() (*ServerV2, blockchain.Blockchain, blockdao.BlockDAO, actpool.ActPool, func()) {
Expand Down Expand Up @@ -813,3 +817,35 @@ func getStorageAt(t *testing.T, handler *hTTPHandler, bc blockchain.Blockchain,
require.Equal("0x0000000000000000000000000000000000000000000000000000000000000000", actual)
}
}

func feeHistory(t *testing.T, handler *hTTPHandler, bc blockchain.Blockchain, dao blockdao.BlockDAO, actPool actpool.ActPool) {
require := require.New(t)
for _, test := range []struct {
params string
expected int
}{
{`[4, "latest", [25,75]]`, 1},
} {
oldnest := max(bc.TipHeight()-4+1, 1)
result := serveTestHTTP(require, handler, "eth_feeHistory", test.params)
if test.expected == 0 {
require.Nil(result)
continue
}
actual, err := json.Marshal(result)
require.NoError(err)
require.JSONEq(fmt.Sprintf(`{
"oldestBlock": "0x%0x",
"reward": [
["0x0", "0x0"],
["0x0", "0x0"],
["0x0", "0x0"],
["0x0", "0x0"]
],
"baseFeePerGas": ["0x0","0x0","0x0","0x0","0x0"],
"gasUsedRatio": [0,0,0,0],
"baseFeePerBlobGas": ["0x1", "0x1", "0x1", "0x1", "0x1"],
"blobGasUsedRatio": [0, 0, 0, 0]
}`, oldnest), string(actual))
}
}
9 changes: 9 additions & 0 deletions api/web3server_marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ type (
Gas uint64 `json:"gas"`
StructLogs []apitypes.StructLog `json:"structLogs"`
}

feeHistoryResult struct {
OldestBlock string `json:"oldestBlock"`
BaseFeePerGas []string `json:"baseFeePerGas"`
GasUsedRatio []float64 `json:"gasUsedRatio"`
BaseFeePerBlobGas []string `json:"baseFeePerBlobGas"`
BlobGasUsedRatio []float64 `json:"blobGasUsedRatio"`
Reward [][]string `json:"reward,omitempty"`
}
)

var (
Expand Down
19 changes: 19 additions & 0 deletions api/web3server_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ func uint64ToHex(val uint64) string {
return "0x" + strconv.FormatUint(val, 16)
}

func bigIntToHex(b *big.Int) string {
if b == nil {
return "0x0"
}
if b.Sign() == 0 {
return "0x0"
}
return "0x" + b.Text(16)
}

// mapper maps a slice of S to a slice of T
func mapper[S, T any](arr []S, fn func(S) T) []T {
ret := make([]T, len(arr))
for i, v := range arr {
ret[i] = fn(v)
}
return ret
}

func intStrToHex(str string) (string, error) {
amount, ok := new(big.Int).SetString(str, 10)
if !ok {
Expand Down
14 changes: 8 additions & 6 deletions gasstation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import "github.com/iotexproject/iotex-core/v2/pkg/unit"

// Config is the gas station config
type Config struct {
SuggestBlockWindow int `yaml:"suggestBlockWindow"`
DefaultGas uint64 `yaml:"defaultGas"`
Percentile int `yaml:"Percentile"`
SuggestBlockWindow int `yaml:"suggestBlockWindow"`
DefaultGas uint64 `yaml:"defaultGas"`
Percentile int `yaml:"Percentile"`
FeeHistoryCacheSize int `yaml:"feeHistoryCacheSize"`
}

// DefaultConfig is the default config
var DefaultConfig = Config{
SuggestBlockWindow: 20,
DefaultGas: uint64(unit.Qev),
Percentile: 60,
SuggestBlockWindow: 20,
DefaultGas: uint64(unit.Qev),
Percentile: 60,
FeeHistoryCacheSize: 1024,
}
148 changes: 142 additions & 6 deletions gasstation/gasstattion.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,49 @@ import (
"math/big"
"sort"

"github.com/ethereum/go-ethereum/params"
"github.com/iotexproject/go-pkgs/cache"
"github.com/iotexproject/go-pkgs/hash"
"github.com/iotexproject/iotex-address/address"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/iotexproject/iotex-core/v2/action"
"github.com/iotexproject/iotex-core/v2/action/protocol"
"github.com/iotexproject/iotex-core/v2/action/protocol/execution/evm"
"github.com/iotexproject/iotex-core/v2/blockchain"
"github.com/iotexproject/iotex-core/v2/blockchain/block"
"github.com/iotexproject/iotex-core/v2/pkg/log"
)

// BlockDAO represents the block data access object
type BlockDAO interface {
GetBlockHash(uint64) (hash.Hash256, error)
GetBlockByHeight(uint64) (*block.Block, error)
GetReceipts(uint64) ([]*action.Receipt, error)
}

// SimulateFunc is function that simulate execution
type SimulateFunc func(context.Context, address.Address, *action.Execution, evm.GetBlockHash) ([]byte, *action.Receipt, error)

// GasStation provide gas related api
type GasStation struct {
bc blockchain.Blockchain
dao BlockDAO
cfg Config
bc blockchain.Blockchain
dao BlockDAO
cfg Config
feeCache cache.LRUCache
percentileCache cache.LRUCache
}

// NewGasStation creates a new gas station
func NewGasStation(bc blockchain.Blockchain, dao BlockDAO, cfg Config) *GasStation {
return &GasStation{
bc: bc,
dao: dao,
cfg: cfg,
bc: bc,
dao: dao,
cfg: cfg,
feeCache: cache.NewThreadSafeLruCache(cfg.FeeHistoryCacheSize),
percentileCache: cache.NewThreadSafeLruCache(cfg.FeeHistoryCacheSize),
}
}

Expand Down Expand Up @@ -103,3 +115,127 @@ func (gs *GasStation) SuggestGasPrice() (uint64, error) {
}
return gasPrice, nil
}

type blockFee struct {
baseFee *big.Int
gasUsedRatio float64
blobBaseFee *big.Int
blobGasRatio float64
}

type blockPercents struct {
ascEffectivePriorityFees []*big.Int
}

// FeeHistory returns fee history over a series of blocks
func (gs *GasStation) FeeHistory(ctx context.Context, blocks, lastBlock uint64, rewardPercentiles []float64) (uint64, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error) {
if blocks < 1 {
return 0, nil, nil, nil, nil, nil, nil
}
maxFeeHistory := uint64(1024)
if blocks > maxFeeHistory {
log.T(ctx).Warn("Sanitizing fee history length", zap.Uint64("requested", blocks), zap.Uint64("truncated", maxFeeHistory))
blocks = maxFeeHistory
}
for i, p := range rewardPercentiles {
if p < 0 || p > 100 {
return 0, nil, nil, nil, nil, nil, status.Error(codes.InvalidArgument, "percentile must be in [0, 100]")
}
if i > 0 && p <= rewardPercentiles[i-1] {
return 0, nil, nil, nil, nil, nil, status.Error(codes.InvalidArgument, "percentiles must be in ascending order")
}
}

var (
rewards = make([][]*big.Int, 0, blocks)
baseFees = make([]*big.Int, blocks+1)
gasUsedRatios = make([]float64, blocks)
blobBaseFees = make([]*big.Int, blocks+1)
blobGasUsedRatios = make([]float64, blocks)
g = gs.bc.Genesis()
lastBlk *block.Block
)
for i := uint64(0); i < blocks; i++ {
height := lastBlock - i
if blkFee, ok := gs.feeCache.Get(height); ok {
// cache hit
log.T(ctx).Debug("fee cache hit", zap.Uint64("height", height))
bf := blkFee.(*blockFee)
baseFees[i] = bf.baseFee
gasUsedRatios[i] = bf.gasUsedRatio
blobBaseFees[i] = bf.blobBaseFee
blobGasUsedRatios[i] = bf.blobGasRatio
} else {
// read block fee from dao
log.T(ctx).Debug("fee cache miss", zap.Uint64("height", height))
blk, err := gs.dao.GetBlockByHeight(height)
if err != nil {
return 0, nil, nil, nil, nil, nil, status.Error(codes.NotFound, err.Error())
}
if i == 0 {
lastBlk = blk
}
baseFees[i] = blk.BaseFee()
gasUsedRatios[i] = float64(blk.GasUsed()) / float64(g.BlockGasLimitByHeight(blk.Height()))
blobBaseFees[i] = protocol.CalcBlobFee(blk.ExcessBlobGas())
blobGasUsedRatios[i] = float64(blk.BlobGasUsed()) / float64(params.MaxBlobGasPerBlock)
gs.feeCache.Add(height, &blockFee{
baseFee: baseFees[i],
gasUsedRatio: gasUsedRatios[i],
blobBaseFee: blobBaseFees[i],
blobGasRatio: blobGasUsedRatios[i],
})
}
// block priority fee percentiles
if len(rewardPercentiles) > 0 {
if blkPercents, ok := gs.percentileCache.Get(height); ok {
log.T(ctx).Debug("percentile cache hit", zap.Uint64("height", height))
rewards = append(rewards, feesPercentiles(blkPercents.(*blockPercents).ascEffectivePriorityFees, rewardPercentiles))
} else {
log.T(ctx).Debug("percentile cache miss", zap.Uint64("height", height))
receipts, err := gs.dao.GetReceipts(height)
if err != nil {
return 0, nil, nil, nil, nil, nil, status.Error(codes.NotFound, err.Error())
}
fees := make([]*big.Int, 0, len(receipts))
for _, r := range receipts {
fees = append(fees, r.PriorityFee())
}
sort.Slice(fees, func(i, j int) bool {
return fees[i].Cmp(fees[j]) < 0
})
rewards = append(rewards, feesPercentiles(fees, rewardPercentiles))
gs.percentileCache.Add(height, &blockPercents{
ascEffectivePriorityFees: fees,
})
}
}
}
// fill next block base fee
if lastBlk == nil {
blk, err := gs.dao.GetBlockByHeight(lastBlock)
if err != nil {
return 0, nil, nil, nil, nil, nil, status.Error(codes.NotFound, err.Error())
}
lastBlk = blk
}
baseFees[blocks] = protocol.CalcBaseFee(g.Blockchain, &protocol.TipInfo{
Height: lastBlock,
GasUsed: lastBlk.GasUsed(),
BaseFee: lastBlk.BaseFee(),
})
blobBaseFees[blocks] = protocol.CalcBlobFee(protocol.CalcExcessBlobGas(lastBlk.ExcessBlobGas(), lastBlk.BlobGasUsed()))
return lastBlock - blocks + 1, rewards, baseFees, gasUsedRatios, blobBaseFees, blobGasUsedRatios, nil
}

func feesPercentiles(ascFees []*big.Int, percentiles []float64) []*big.Int {
res := make([]*big.Int, len(percentiles))
for i, p := range percentiles {
idx := int(float64(len(ascFees)) * p)
if idx >= len(ascFees) {
idx = len(ascFees) - 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible this if can enter multiple times in the loop?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should only happens when p equlas 1, b/c the convertion from float to int will truncate the decimal part in Golang

}
res[i] = ascFees[idx]
}
return res
}
Loading
Loading