Skip to content

Commit

Permalink
[api] impl eth_feeHistory (#4527)
Browse files Browse the repository at this point in the history
  • Loading branch information
envestcc authored Dec 25, 2024
1 parent 3580fd0 commit c8c7a84
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 12 deletions.
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
}
res[i] = ascFees[idx]
}
return res
}
Loading

0 comments on commit c8c7a84

Please sign in to comment.