diff --git a/api/coreservice.go b/api/coreservice.go index f18b261f23..66ace25a13 100644 --- a/api/coreservice.go +++ b/api/coreservice.go @@ -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 @@ -127,7 +129,7 @@ type ( ActionByActionHash(h hash.Hash256) (*action.SealedEnvelope, *block.Block, uint32, error) // PendingActionByActionHash returns action by action hash PendingActionByActionHash(h hash.Hash256) (*action.SealedEnvelope, error) - // ActPoolActions returns the all Transaction Identifiers in the actpool + // ActionsInActPool returns the all Transaction Identifiers in the actpool ActionsInActPool(actHashes []string) ([]*action.SealedEnvelope, error) // BlockByHeightRange returns blocks within the height range BlockByHeightRange(uint64, uint64) ([]*apitypes.BlockWithReceipts, error) @@ -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) diff --git a/api/web3server.go b/api/web3server.go index 5307bcb578..414e1d009e 100644 --- a/api/web3server.go +++ b/api/web3server.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "math/big" "strconv" "time" @@ -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": @@ -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 } diff --git a/api/web3server_integrity_test.go b/api/web3server_integrity_test.go index 3eaeb286dd..8c3dc89938 100644 --- a/api/web3server_integrity_test.go +++ b/api/web3server_integrity_test.go @@ -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()) { @@ -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)) + } +} diff --git a/api/web3server_marshal.go b/api/web3server_marshal.go index 945d92261b..f5cd5f5855 100644 --- a/api/web3server_marshal.go +++ b/api/web3server_marshal.go @@ -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 ( diff --git a/api/web3server_utils.go b/api/web3server_utils.go index dd264b1404..8e1ad917d5 100644 --- a/api/web3server_utils.go +++ b/api/web3server_utils.go @@ -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 { diff --git a/gasstation/config.go b/gasstation/config.go index 40e54d3918..b1afb88fc9 100644 --- a/gasstation/config.go +++ b/gasstation/config.go @@ -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, } diff --git a/gasstation/gasstattion.go b/gasstation/gasstattion.go index 6853c698fa..b21ffdbdf8 100644 --- a/gasstation/gasstattion.go +++ b/gasstation/gasstattion.go @@ -10,19 +10,27 @@ 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 @@ -30,17 +38,21 @@ type SimulateFunc func(context.Context, address.Address, *action.Execution, evm. // 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), } } @@ -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 +} diff --git a/ioctl/cmd/bc/bcbucketlist.go b/ioctl/cmd/bc/bcbucketlist.go index 9eee2531db..f5788d4680 100644 --- a/ioctl/cmd/bc/bcbucketlist.go +++ b/ioctl/cmd/bc/bcbucketlist.go @@ -110,7 +110,7 @@ func getBucketList(method, addr string, args ...string) (err error) { return output.NewError(output.InputError, "unknown ", nil) } -// getBucketList get bucket list from chain by voter address +// getBucketListByVoter get bucket list from chain by voter address func getBucketListByVoter(addr string, offset, limit uint32) error { address, err := util.GetAddress(addr) if err != nil { diff --git a/server/itx/nodestats/systemstats.go b/server/itx/nodestats/systemstats.go index 30d66cc5a9..c9f18615d2 100644 --- a/server/itx/nodestats/systemstats.go +++ b/server/itx/nodestats/systemstats.go @@ -17,7 +17,7 @@ type DiskStatus struct { Free uint64 `json:"Free"` } -// diskusage of path/disk +// diskUsage of path/disk func diskUsage(path string) (disk DiskStatus) { fs := syscall.Statfs_t{} err := syscall.Statfs(path, &fs) diff --git a/state/factory/factory.go b/state/factory/factory.go index 228f5ca268..37cd5a7b62 100644 --- a/state/factory/factory.go +++ b/state/factory/factory.go @@ -499,7 +499,7 @@ func (sf *factory) State(s interface{}, opts ...protocol.StateOption) (uint64, e return sf.currentChainHeight, state.Deserialize(s, value) } -// State returns a set states in the state factory +// States returns a set states in the state factory func (sf *factory) States(opts ...protocol.StateOption) (uint64, state.Iterator, error) { sf.mutex.RLock() defer sf.mutex.RUnlock() diff --git a/state/factory/statedb.go b/state/factory/statedb.go index 2e2be991aa..638debf7ab 100644 --- a/state/factory/statedb.go +++ b/state/factory/statedb.go @@ -339,7 +339,7 @@ func (sdb *stateDB) State(s interface{}, opts ...protocol.StateOption) (uint64, return sdb.currentChainHeight, sdb.state(sdb.currentChainHeight, cfg.Namespace, cfg.Key, s) } -// State returns a set of states in the state factory +// States returns a set of states in the state factory func (sdb *stateDB) States(opts ...protocol.StateOption) (uint64, state.Iterator, error) { cfg, err := processOptions(opts...) if err != nil { diff --git a/state/factory/workingset.go b/state/factory/workingset.go index cb28cf880a..890ae6d0f4 100644 --- a/state/factory/workingset.go +++ b/state/factory/workingset.go @@ -422,7 +422,7 @@ func (ws *workingSet) Reset() { ws.dock.Reset() } -// createGenesisStates initialize the genesis states +// CreateGenesisStates initialize the genesis states func (ws *workingSet) CreateGenesisStates(ctx context.Context) error { if reg, ok := protocol.GetRegistry(ctx); ok { for _, p := range reg.All() { diff --git a/test/mock/mock_apicoreservice/mock_apicoreservice.go b/test/mock/mock_apicoreservice/mock_apicoreservice.go index 7777a1b9c3..49890a041d 100644 --- a/test/mock/mock_apicoreservice/mock_apicoreservice.go +++ b/test/mock/mock_apicoreservice/mock_apicoreservice.go @@ -372,6 +372,26 @@ func (mr *MockCoreServiceMockRecorder) EstimateMigrateStakeGasConsumption(arg0, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimateMigrateStakeGasConsumption", reflect.TypeOf((*MockCoreService)(nil).EstimateMigrateStakeGasConsumption), arg0, arg1, arg2) } +// FeeHistory mocks base method. +func (m *MockCoreService) FeeHistory(ctx context.Context, blocks, lastBlock uint64, rewardPercentiles []float64) (uint64, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FeeHistory", ctx, blocks, lastBlock, rewardPercentiles) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].([][]*big.Int) + ret2, _ := ret[2].([]*big.Int) + ret3, _ := ret[3].([]float64) + ret4, _ := ret[4].([]*big.Int) + ret5, _ := ret[5].([]float64) + ret6, _ := ret[6].(error) + return ret0, ret1, ret2, ret3, ret4, ret5, ret6 +} + +// FeeHistory indicates an expected call of FeeHistory. +func (mr *MockCoreServiceMockRecorder) FeeHistory(ctx, blocks, lastBlock, rewardPercentiles interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FeeHistory", reflect.TypeOf((*MockCoreService)(nil).FeeHistory), ctx, blocks, lastBlock, rewardPercentiles) +} + // Genesis mocks base method. func (m *MockCoreService) Genesis() genesis.Genesis { m.ctrl.T.Helper()