From aa378dae43f431e51ddb05e998cd9f13b928e160 Mon Sep 17 00:00:00 2001 From: ptrus Date: Fri, 5 Jan 2024 16:34:38 +0100 Subject: [PATCH 1/2] feat: implement eth_feeHistory endpoint --- conf/config.go | 2 + gas/backend.go | 85 +++++++++--------- gas/backend_test.go | 7 +- gas/fee_history.go | 181 +++++++++++++++++++++++++++++++++++++++ indexer/backend.go | 6 +- indexer/backend_cache.go | 4 +- indexer/indexer.go | 8 +- indexer/utils.go | 13 ++- rpc/eth/api.go | 27 ++++++ rpc/eth/metrics/api.go | 11 +++ rpc/metrics/metrics.go | 2 +- rpc/utils/utils.go | 7 +- tests/rpc/rpc_test.go | 49 +++++++++++ 13 files changed, 345 insertions(+), 57 deletions(-) create mode 100644 gas/fee_history.go diff --git a/conf/config.go b/conf/config.go index fa24f4a3..8d672370 100644 --- a/conf/config.go +++ b/conf/config.go @@ -243,6 +243,8 @@ type GasConfig struct { WindowSize uint64 `koanf:"window_size"` // ComputedPriceMargin is the gas price to add to the computed gas price. ComputedPriceMargin uint64 `koanf:"computed_price_margin"` + // FeeHistorySize is the number of recent blocks to store for the fee history query. + FeeHistorySize uint64 `koanf:"fee_history_size"` } // Validate validates the gas configuration. diff --git a/gas/backend.go b/gas/backend.go index e5b567c4..f34c13e7 100644 --- a/gas/backend.go +++ b/gas/backend.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -21,7 +22,7 @@ import ( ) var ( - metricNodeMinPrice = promauto.NewGauge(prometheus.GaugeOpts{Name: "oasis_web3_gateway_gas_orcale_node_min_price", Help: "Min gas price periodically queried from the node."}) + metricNodeMinPrice = promauto.NewGauge(prometheus.GaugeOpts{Name: "oasis_web3_gateway_gas_oracle_node_min_price", Help: "Min gas price queried from the node."}) metricComputedPrice = promauto.NewGauge(prometheus.GaugeOpts{Name: "oasis_web3_gateway_gas_oracle_computed_price", Help: "Computed recommended gas price based on recent full blocks. -1 if none (no recent full blocks)."}) ) @@ -31,6 +32,9 @@ type Backend interface { // GasPrice returns the currently recommended minimum gas price. GasPrice() *hexutil.Big + + // FeeHistory returns the fee history of the last blocks and percentiles. + FeeHistory(blockCount uint64, lastBlock rpc.BlockNumber, percentiles []float64) *FeeHistoryResult } const ( @@ -94,6 +98,12 @@ type gasPriceOracle struct { // tracks the current index of the blockPrices rolling array.:w blockPricesCurrentIdx int + // protects feeHistoryData. + feeHistoryLock sync.RWMutex + // feeHistoryData contains the per block data needed to compute the fee history. + feeHistoryData []*feeHistoryBlockData + feeHistorySize uint64 + // Configuration parameters. windowSize uint64 fullBlockThreshold float64 @@ -111,7 +121,8 @@ func New(ctx context.Context, cfg *conf.GasConfig, blockWatcher indexer.BlockWat blockFullThreshold := defaultFullBlockThreshold minGasPrice := defaultGasPrice computedPriceMargin := defaultComputedPriceMargin - if cfg != nil { + feeHistorySize := defaultFeeHistorySize + if cfg != nil { // nolint: nestif if cfg.WindowSize != 0 { windowSize = cfg.WindowSize } @@ -124,13 +135,17 @@ func New(ctx context.Context, cfg *conf.GasConfig, blockWatcher indexer.BlockWat if cfg.ComputedPriceMargin != 0 { computedPriceMargin = *quantity.NewFromUint64(cfg.ComputedPriceMargin) } + if cfg.FeeHistorySize != 0 { + feeHistorySize = cfg.FeeHistorySize + } } g := &gasPriceOracle{ BaseBackgroundService: *service.NewBaseBackgroundService("gas-price-oracle"), ctx: ctxB, cancelCtx: cancelCtx, - blockPrices: make([]*quantity.Quantity, 0, windowSize), + feeHistoryData: make([]*feeHistoryBlockData, 0, feeHistorySize), + feeHistorySize: feeHistorySize, windowSize: windowSize, fullBlockThreshold: blockFullThreshold, defaultGasPrice: minGasPrice, @@ -139,6 +154,11 @@ func New(ctx context.Context, cfg *conf.GasConfig, blockWatcher indexer.BlockWat coreClient: coreClient, } + g.blockPrices = make([]*quantity.Quantity, windowSize) + for i := range windowSize { + g.blockPrices[i] = quantity.NewQuantity() + } + return g } @@ -154,6 +174,7 @@ func (g *gasPriceOracle) Stop() { g.cancelCtx() } +// Implements Backend. func (g *gasPriceOracle) GasPrice() *hexutil.Big { g.priceLock.RLock() defer g.priceLock.RUnlock() @@ -231,6 +252,8 @@ func (g *gasPriceOracle) indexedBlockWatcher() { // Track price for the block. g.onBlock(blk.Block, blk.MedianTransactionGasPrice) + // Track fee history. + g.trackFeeHistory(blk.Block, blk.UniqueTxes, blk.Receipts) } } } @@ -240,58 +263,42 @@ func (g *gasPriceOracle) onBlock(b *model.Block, medTxPrice *quantity.Quantity) blockFull := (float64(b.Header.GasLimit) * g.fullBlockThreshold) <= float64(b.Header.GasUsed) if !blockFull { // Track 0 for non-full blocks. - g.trackPrice(quantity.NewFromUint64(0)) + g.trackPrice(quantity.NewQuantity()) return } + if medTxPrice == nil { g.Logger.Error("no med tx gas price for block", "block", b) return } - trackPrice := medTxPrice.Clone() if err := trackPrice.Add(&g.computedPriceMargin); err != nil { g.Logger.Error("failed to add minPriceEps to medTxPrice", "err", err) } - g.trackPrice(trackPrice) } func (g *gasPriceOracle) trackPrice(price *quantity.Quantity) { - // One item always gets added added to the prices array. - // Bump the current index for next iteration. - defer func() { - g.blockPricesCurrentIdx = (g.blockPricesCurrentIdx + 1) % int(g.windowSize) - }() - - // Recalculate the maximum median-price over the block window. - defer func() { - // Find maximum gas price. - maxPrice := quantity.NewFromUint64(0) - for _, price := range g.blockPrices { - if price.Cmp(maxPrice) > 0 { - maxPrice = price - } - } - - // No full blocks among last `windowSize` blocks. - if maxPrice.IsZero() { - g.priceLock.Lock() - g.computedGasPrice = nil - g.priceLock.Unlock() - metricComputedPrice.Set(float64(-1)) + g.blockPrices[g.blockPricesCurrentIdx] = price + g.blockPricesCurrentIdx = (g.blockPricesCurrentIdx + 1) % int(g.windowSize) - return + // Find maximum gas price. + maxPrice := quantity.NewQuantity() + for _, price := range g.blockPrices { + if price.Cmp(maxPrice) > 0 { + maxPrice = price } + } - g.priceLock.Lock() - g.computedGasPrice = maxPrice - g.priceLock.Unlock() - metricComputedPrice.Set(float64(maxPrice.ToBigInt().Int64())) - }() - - if len(g.blockPrices) < int(g.windowSize) { - g.blockPrices = append(g.blockPrices, price) - return + reportedPrice := float64(maxPrice.ToBigInt().Int64()) + // No full blocks among last `windowSize` blocks. + if maxPrice.IsZero() { + maxPrice = nil + reportedPrice = float64(-1) } - g.blockPrices[g.blockPricesCurrentIdx] = price + + g.priceLock.Lock() + g.computedGasPrice = maxPrice + g.priceLock.Unlock() + metricComputedPrice.Set(reportedPrice) } diff --git a/gas/backend_test.go b/gas/backend_test.go index 70a299a7..bc469816 100644 --- a/gas/backend_test.go +++ b/gas/backend_test.go @@ -126,6 +126,11 @@ func TestGasPriceOracle(t *testing.T) { // Default gas price should be returned by the oracle. require.EqualValues(defaultGasPrice.ToBigInt(), gasPriceOracle.GasPrice(), "oracle should return default gas price") + fh := gasPriceOracle.FeeHistory(10, 10, []float64{0.25, 0.5}) + require.EqualValues(0, fh.OldestBlock.ToInt().Int64(), "fee history should be empty") + require.Empty(0, fh.GasUsedRatio, "fee history should be empty") + require.Empty(0, fh.Reward, "fee history should be empty") + // Emit a non-full block. emitBlock(&emitter, false, nil) @@ -136,7 +141,7 @@ func TestGasPriceOracle(t *testing.T) { emitBlock(&emitter, true, quantity.NewFromUint64(1_000_000_000_000)) // 1000 gwei. // 1001 gwei should be returned. - require.EqualValues(big.NewInt(1_001_000_000_000), gasPriceOracle.GasPrice(), "oracle should return correct gas price") + require.EqualValues(big.NewInt(1_001_000_000_000), gasPriceOracle.GasPrice().ToInt(), "oracle should return correct gas price") // Emit a non-full block. emitBlock(&emitter, false, nil) diff --git a/gas/fee_history.go b/gas/fee_history.go new file mode 100644 index 00000000..4ee1f577 --- /dev/null +++ b/gas/fee_history.go @@ -0,0 +1,181 @@ +package gas + +import ( + "math/big" + "slices" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethMath "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/oasisprotocol/oasis-web3-gateway/db/model" +) + +// defaultFeeHistorySize is the default number of recent blocks to store for the fee history query. +const defaultFeeHistorySize uint64 = 20 + +// FeeHistoryResult is the result of a fee history query. +type FeeHistoryResult struct { + OldestBlock *hexutil.Big `json:"oldestBlock"` + Reward [][]*hexutil.Big `json:"reward,omitempty"` + BaseFee []*hexutil.Big `json:"baseFeePerGas,omitempty"` + GasUsedRatio []float64 `json:"gasUsedRatio"` +} + +type txGasAndReward struct { + gasUsed uint64 + reward *hexutil.Big +} + +type feeHistoryBlockData struct { + height uint64 + baseFee *hexutil.Big + gasUsedRatio float64 + gasUsed uint64 + + // Sorted list of transaction by reward. This is used to + // compute the queried percentiles in the fee history query. + sortedTxs []*txGasAndReward +} + +// rewards computes the queried reward percentiles for the given block data. +func (d *feeHistoryBlockData) rewards(percentiles []float64) []*hexutil.Big { + rewards := make([]*hexutil.Big, len(percentiles)) + + // No percentiles requested. + if len(percentiles) == 0 { + return rewards + } + + // No transactions in the block. + if len(d.sortedTxs) == 0 { + for i := range rewards { + rewards[i] = (*hexutil.Big)(common.Big0) + } + return rewards + } + + // Compute the requested percentiles. + var txIndex int + sumGasUsed := d.sortedTxs[0].gasUsed + for i, p := range percentiles { + thresholdGasUsed := uint64(float64(d.gasUsed) * p / 100) + for sumGasUsed < thresholdGasUsed && txIndex < len(d.sortedTxs)-1 { + txIndex++ + sumGasUsed += d.sortedTxs[txIndex].gasUsed + } + rewards[i] = d.sortedTxs[txIndex].reward + } + + return rewards +} + +// Implements Backend. +func (g *gasPriceOracle) FeeHistory(blockCount uint64, lastBlock rpc.BlockNumber, percentiles []float64) *FeeHistoryResult { + g.feeHistoryLock.RLock() + defer g.feeHistoryLock.RUnlock() + + // No history available. + if len(g.feeHistoryData) == 0 { + return &FeeHistoryResult{OldestBlock: (*hexutil.Big)(common.Big0)} + } + + // Find the latest block index. + var lastBlockIdx int + switch lastBlock { + case rpc.PendingBlockNumber, rpc.LatestBlockNumber, rpc.FinalizedBlockNumber, rpc.SafeBlockNumber: + // Latest available block. + lastBlockIdx = len(g.feeHistoryData) - 1 + case rpc.EarliestBlockNumber: + // Doesn't make sense to start at earliest block. + return &FeeHistoryResult{OldestBlock: (*hexutil.Big)(common.Big0)} + default: + // Check if the requested block number is available. + var found bool + for i, d := range g.feeHistoryData { + if d.height == uint64(lastBlock) { + lastBlockIdx = i + found = true + break + } + } + // Data for requested block number not available. + if !found { + return &FeeHistoryResult{OldestBlock: (*hexutil.Big)(common.Big0)} + } + } + + // Find the oldest block index. + var oldestBlockIdx int + if blockCount > uint64(lastBlockIdx) { + // Not enough blocks available, return all available blocks. + oldestBlockIdx = 0 + } else { + oldestBlockIdx = lastBlockIdx + 1 - int(blockCount) + } + + // Return the requested fee history. + result := &FeeHistoryResult{ + OldestBlock: (*hexutil.Big)(big.NewInt(int64(g.feeHistoryData[oldestBlockIdx].height))), + Reward: make([][]*hexutil.Big, lastBlockIdx-oldestBlockIdx+1), + BaseFee: make([]*hexutil.Big, lastBlockIdx-oldestBlockIdx+1), + GasUsedRatio: make([]float64, lastBlockIdx-oldestBlockIdx+1), + } + for i := oldestBlockIdx; i <= lastBlockIdx; i++ { + result.Reward[i-oldestBlockIdx] = g.feeHistoryData[i].rewards(percentiles) + result.BaseFee[i-oldestBlockIdx] = g.feeHistoryData[i].baseFee + result.GasUsedRatio[i-oldestBlockIdx] = g.feeHistoryData[i].gasUsedRatio + } + + return result +} + +func (g *gasPriceOracle) trackFeeHistory(block *model.Block, txs []*model.Transaction, receipts []*model.Receipt) { + // TODO: could populate old blocks on first received block (if available). + + d := &feeHistoryBlockData{ + height: block.Round, + gasUsed: block.Header.GasUsed, + gasUsedRatio: float64(block.Header.GasUsed) / float64(block.Header.GasLimit), + sortedTxs: make([]*txGasAndReward, len(receipts)), + } + + // Base fee. + var baseFee big.Int + if err := baseFee.UnmarshalText([]byte(block.Header.BaseFee)); err != nil { + g.Logger.Error("unmarshal base fee", "base_fee", block.Header.BaseFee, "block", block, "err", err) + return + } + d.baseFee = (*hexutil.Big)(&baseFee) + + // Transactions. + for i, tx := range txs { + var tipGas, feeCap big.Int + if err := feeCap.UnmarshalText([]byte(tx.GasFeeCap)); err != nil { + g.Logger.Error("unmarshal gas fee cap", "fee_cap", tx.GasFeeCap, "block", block, "tx", tx, "err", err) + return + } + if err := tipGas.UnmarshalText([]byte(tx.GasTipCap)); err != nil { + g.Logger.Error("unmarshal gas tip cap", "tip_cap", tx.GasTipCap, "block", block, "tx", tx, "err", err) + return + } + + d.sortedTxs[i] = &txGasAndReward{ + gasUsed: receipts[i].GasUsed, + reward: (*hexutil.Big)(ethMath.BigMin(&tipGas, &feeCap)), + } + } + slices.SortStableFunc(d.sortedTxs, func(a, b *txGasAndReward) int { + return a.reward.ToInt().Cmp(b.reward.ToInt()) + }) + + // Add new data to the history. + g.feeHistoryLock.Lock() + defer g.feeHistoryLock.Unlock() + // Delete oldest entry if we are at capacity. + if len(g.feeHistoryData) == int(g.feeHistorySize) { + g.feeHistoryData = g.feeHistoryData[1:] + } + g.feeHistoryData = append(g.feeHistoryData, d) +} diff --git a/indexer/backend.go b/indexer/backend.go index 0170ae43..aa2da775 100644 --- a/indexer/backend.go +++ b/indexer/backend.go @@ -84,7 +84,7 @@ type Backend interface { ctx context.Context, oasisBlock *block.Block, txResults []*client.TransactionWithResults, - blockGasLimit uint64, + coreParameters *core.Parameters, rtInfo *core.RuntimeInfoResponse, ) error @@ -141,10 +141,10 @@ func (ib *indexBackend) SetObserver(ob BackendObserver) { } // Index indexes oasis block. -func (ib *indexBackend) Index(ctx context.Context, oasisBlock *block.Block, txResults []*client.TransactionWithResults, blockGasLimit uint64, rtInfo *core.RuntimeInfoResponse) error { +func (ib *indexBackend) Index(ctx context.Context, oasisBlock *block.Block, txResults []*client.TransactionWithResults, coreParameters *core.Parameters, rtInfo *core.RuntimeInfoResponse) error { round := oasisBlock.Header.Round - err := ib.StoreBlockData(ctx, oasisBlock, txResults, blockGasLimit) + err := ib.StoreBlockData(ctx, oasisBlock, txResults, coreParameters) if err != nil { ib.logger.Error("generateEthBlock failed", "err", err) return err diff --git a/indexer/backend_cache.go b/indexer/backend_cache.go index 97295beb..7b686514 100644 --- a/indexer/backend_cache.go +++ b/indexer/backend_cache.go @@ -169,10 +169,10 @@ func (cb *cachingBackend) Index( ctx context.Context, oasisBlock *block.Block, txResults []*client.TransactionWithResults, - blockGasLimit uint64, + coreParameters *core.Parameters, rtInfo *core.RuntimeInfoResponse, ) error { - return cb.inner.Index(ctx, oasisBlock, txResults, blockGasLimit, rtInfo) + return cb.inner.Index(ctx, oasisBlock, txResults, coreParameters, rtInfo) } func (cb *cachingBackend) Prune( diff --git a/indexer/indexer.go b/indexer/indexer.go index 179068ef..761a6080 100644 --- a/indexer/indexer.go +++ b/indexer/indexer.go @@ -55,7 +55,7 @@ type Service struct { core core.V1 queryEpochParameters bool - blockGasLimit uint64 + coreParameters *core.Parameters rtInfo *core.RuntimeInfoResponse ctx context.Context @@ -79,14 +79,14 @@ func (s *Service) indexBlock(ctx context.Context, round uint64) error { return fmt.Errorf("querying block: %w", err) } - if s.blockGasLimit == 0 || s.rtInfo == nil || s.queryEpochParameters { + if s.coreParameters == nil || s.rtInfo == nil || s.queryEpochParameters { // Query parameters for block gas limit. var params *core.Parameters params, err = s.core.Parameters(ctx, round) if err != nil { return fmt.Errorf("querying block parameters: %w", err) } - s.blockGasLimit = params.MaxBatchGas + s.coreParameters = params // Query runtime info. s.rtInfo, err = s.core.RuntimeInfo(ctx) @@ -100,7 +100,7 @@ func (s *Service) indexBlock(ctx context.Context, round uint64) error { return fmt.Errorf("querying transactions with results: %w", err) } - err = s.backend.Index(ctx, blk, txs, s.blockGasLimit, s.rtInfo) + err = s.backend.Index(ctx, blk, txs, s.coreParameters, s.rtInfo) if err != nil { return fmt.Errorf("indexing block: %w", err) } diff --git a/indexer/utils.go b/indexer/utils.go index 48702aea..38976011 100644 --- a/indexer/utils.go +++ b/indexer/utils.go @@ -94,6 +94,7 @@ func blockToModels( txsGas []txGas, results []types.CallResult, blockGasLimit uint64, + baseFee *big.Int, ) (*model.Block, []*model.Transaction, []*model.Receipt, []*model.Log, error) { dbLogs, txLogs := ethToModelLogs(logs) @@ -104,7 +105,6 @@ func blockToModels( logsBloom := ethtypes.BytesToBloom(ethtypes.LogsBloom(logs)) bloomData, _ := logsBloom.MarshalText() bloomHex := hex.EncodeToString(bloomData) - baseFee := big.NewInt(10) var btxHash string if len(transactions) == 0 { btxHash = ethtypes.EmptyRootHash.Hex() @@ -227,7 +227,7 @@ type txGas struct { } // StoreBlockData parses oasis block and stores in db. -func (ib *indexBackend) StoreBlockData(ctx context.Context, oasisBlock *block.Block, txResults []*client.TransactionWithResults, blockGasLimit uint64) error { //nolint: gocyclo +func (ib *indexBackend) StoreBlockData(ctx context.Context, oasisBlock *block.Block, txResults []*client.TransactionWithResults, coreParameters *core.Parameters) error { //nolint: gocyclo encoded := oasisBlock.Header.EncodedHash() bhash := common.HexToHash(encoded.Hex()) blockNum := oasisBlock.Header.Round @@ -387,7 +387,14 @@ func (ib *indexBackend) StoreBlockData(ctx context.Context, oasisBlock *block.Bl } // Convert to DB models. - blk, txs, receipts, dbLogs, err := blockToModels(oasisBlock, ethTxs, logs, txsStatus, txsGas, results, blockGasLimit) + var minGasPrice big.Int + var blockGasLimit uint64 + if coreParameters != nil { + mgp := coreParameters.MinGasPrice[types.NativeDenomination] + minGasPrice = *mgp.ToBigInt() + blockGasLimit = coreParameters.MaxBatchGas + } + blk, txs, receipts, dbLogs, err := blockToModels(oasisBlock, ethTxs, logs, txsStatus, txsGas, results, blockGasLimit, &minGasPrice) if err != nil { ib.logger.Debug("Failed to ConvertToEthBlock", "height", blockNum, "err", err) return err diff --git a/rpc/eth/api.go b/rpc/eth/api.go index 2eed2fb9..7abd0f00 100644 --- a/rpc/eth/api.go +++ b/rpc/eth/api.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/filters" ethrpc "github.com/ethereum/go-ethereum/rpc" @@ -42,6 +43,7 @@ var ( ErrMalformedTransaction = errors.New("malformed transaction") ErrMalformedBlockNumber = errors.New("malformed blocknumber") ErrInvalidRequest = errors.New("invalid request") + ErrInvalidPercentile = errors.New("invalid reward percentile") // estimateGasSigSpec is a dummy signature spec used by the estimate gas method, as // otherwise transactions without signature would be underestimated. @@ -62,6 +64,8 @@ type API interface { ChainId() (*hexutil.Big, error) // GasPrice returns a suggestion for a gas price for legacy transactions. GasPrice(ctx context.Context) (*hexutil.Big, error) + // FeeHistory returns the transaction base fee per gas and effective priority fee per gas for the requested/supported block range. + FeeHistory(ctx context.Context, blockCount math.HexOrDecimal64, lastBlock ethrpc.BlockNumber, rewardPercentiles []float64) (*gas.FeeHistoryResult, error) // GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. GetBlockTransactionCountByHash(ctx context.Context, blockHash common.Hash) (hexutil.Uint, error) // GetTransactionCount returns the number of transactions the given address has sent for the given block number. @@ -290,6 +294,29 @@ func (api *publicAPI) GasPrice(_ context.Context) (*hexutil.Big, error) { return api.gasPriceOracle.GasPrice(), nil } +func (api *publicAPI) FeeHistory(_ context.Context, blockCount math.HexOrDecimal64, lastBlock ethrpc.BlockNumber, rewardPercentiles []float64) (*gas.FeeHistoryResult, error) { + logger := api.Logger.With("method", "eth_feeHistory", "block_count", blockCount, "last_block", lastBlock, "reward_percentiles", rewardPercentiles) + logger.Debug("request") + + // Validate blockCount. + if blockCount < 1 { + // Returning with no data and no error means there are no retrievable blocks. + return &gas.FeeHistoryResult{OldestBlock: (*hexutil.Big)(common.Big0)}, nil + } + + // Validate reward percentiles. + for i, p := range rewardPercentiles { + if p < 0 || p > 100 { + return nil, fmt.Errorf("%w: %f", ErrInvalidPercentile, p) + } + if i > 0 && p < rewardPercentiles[i-1] { + return nil, fmt.Errorf("%w: #%d:%f > #%d:%f", ErrInvalidPercentile, i-1, rewardPercentiles[i-1], i, p) + } + } + + return api.gasPriceOracle.FeeHistory(uint64(blockCount), lastBlock, rewardPercentiles), nil +} + func (api *publicAPI) GetBlockTransactionCountByHash(ctx context.Context, blockHash common.Hash) (hexutil.Uint, error) { logger := api.Logger.With("method", "eth_getBlockTransactionCountByHash", "block_hash", blockHash.Hex()) logger.Debug("request") diff --git a/rpc/eth/metrics/api.go b/rpc/eth/metrics/api.go index 1d0930be..508dc118 100644 --- a/rpc/eth/metrics/api.go +++ b/rpc/eth/metrics/api.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/filters" ethrpc "github.com/ethereum/go-ethereum/rpc" @@ -14,6 +15,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/logging" + "github.com/oasisprotocol/oasis-web3-gateway/gas" "github.com/oasisprotocol/oasis-web3-gateway/indexer" "github.com/oasisprotocol/oasis-web3-gateway/rpc/eth" "github.com/oasisprotocol/oasis-web3-gateway/rpc/metrics" @@ -146,6 +148,15 @@ func (m *metricsWrapper) GasPrice(ctx context.Context) (res *hexutil.Big, err er return } +// FeeHistory implements eth.API. +func (m *metricsWrapper) FeeHistory(ctx context.Context, blockCount math.HexOrDecimal64, lastBlock ethrpc.BlockNumber, rewardPercentiles []float64) (res *gas.FeeHistoryResult, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_feeHistory") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles) + return +} + // GetBalance implements eth.API. func (m *metricsWrapper) GetBalance(ctx context.Context, address common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (res *hexutil.Big, err error) { r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getBalance") diff --git a/rpc/metrics/metrics.go b/rpc/metrics/metrics.go index 1e2a1dc5..33363f32 100644 --- a/rpc/metrics/metrics.go +++ b/rpc/metrics/metrics.go @@ -24,7 +24,7 @@ func GetAPIMethodMetrics(method string) (prometheus.Counter, prometheus.Counter, // InstrumentCaller instruments the caller method. // -// Use InstrumentCaller is usually used the following way: +// The InstrumentCaller should be used the following way: // // func InstrumentMe() (err error) { // r, s, f, i, d := metrics.GetAPIMethodMetrics("method") diff --git a/rpc/utils/utils.go b/rpc/utils/utils.go index 8037baf6..67296317 100644 --- a/rpc/utils/utils.go +++ b/rpc/utils/utils.go @@ -145,10 +145,10 @@ func DB2EthLogs(dbLogs []*model.Log) []*ethtypes.Log { // DB2EthHeader converts block in db to ethereum header. func DB2EthHeader(block *model.Block) *ethtypes.Header { - v1 := big.NewInt(0) - diff, _ := v1.SetString(block.Header.Difficulty, 10) + diff, _ := new(big.Int).SetString(block.Header.Difficulty, 10) noPrefix := block.Header.Bloom[2 : len(block.Header.Bloom)-1] bloomData, _ := hex.DecodeString(noPrefix) + baseFee, _ := new(big.Int).SetString(block.Header.BaseFee, 10) res := ðtypes.Header{ ParentHash: common.HexToHash(block.Header.ParentHash), UncleHash: common.HexToHash(block.Header.UncleHash), @@ -165,8 +165,7 @@ func DB2EthHeader(block *model.Block) *ethtypes.Header { Extra: []byte(block.Header.Extra), MixDigest: common.HexToHash(block.Header.MixDigest), Nonce: ethtypes.EncodeNonce(block.Header.Nonce), - // BaseFee was added by EIP-1559 and is ignored in legacy headers. - BaseFee: big.NewInt(0), + BaseFee: baseFee, } return res diff --git a/tests/rpc/rpc_test.go b/tests/rpc/rpc_test.go index a5b9e927..a2d6170b 100644 --- a/tests/rpc/rpc_test.go +++ b/tests/rpc/rpc_test.go @@ -146,6 +146,55 @@ func TestEth_GasPrice(t *testing.T) { t.Logf("gas price: %v", price) } +func TestEth_FeeHistory(t *testing.T) { + ec := localClient(t, false) + + ctx, cancel := context.WithTimeout(context.Background(), OasisBlockTimeout) + defer cancel() + + // Submit some test transactions. + for i := 0; i < 5; i++ { + receipt := submitTestTransaction(ctx, t) + require.EqualValues(t, 1, receipt.Status) + require.NotNil(t, receipt) + } + + // Base fee history test. + feeHistory, err := ec.FeeHistory(context.Background(), 10, nil, []float64{25, 50, 75, 100}) + require.NoError(t, err, "get fee history") + + t.Logf("fee history: %v", feeHistory) + require.Len(t, feeHistory.BaseFee, 10, "fee history base fee should have 10 elements") + for _, fee := range feeHistory.BaseFee { + require.Greater(t, fee.Int64(), int64(0), "base fee should be greater than 0") + } + require.Len(t, feeHistory.Reward, 10, "fee history reward should have 10 elements") + + // More cases. + for _, tc := range []struct { + name string + blockCount uint64 + lastBlock *big.Int + percentiles []float64 + expectErr bool + }{ + {name: "Query with no reward percentiles", blockCount: 5, lastBlock: nil, percentiles: nil, expectErr: false}, + {name: "Query specific block range", blockCount: 3, lastBlock: big.NewInt(5), percentiles: []float64{50}, expectErr: false}, + {name: "Query specific block range (large)", blockCount: 3, lastBlock: big.NewInt(100000), percentiles: []float64{50}, expectErr: false}, + {name: "Query with zero block count (empty response)", blockCount: 0, lastBlock: nil, percentiles: []float64{50}, expectErr: false}, + {name: "Query large block count", blockCount: 10_000, lastBlock: big.NewInt(11_000), percentiles: []float64{50}, expectErr: false}, + {name: "Invalid percentile", blockCount: 5, lastBlock: nil, percentiles: []float64{150}, expectErr: true}, // Invalid percentile > 100 + } { + _, err := ec.FeeHistory(ctx, tc.blockCount, tc.lastBlock, tc.percentiles) + switch tc.expectErr { + case true: + require.Error(t, err, tc.name) + default: + require.NoError(t, err, tc.name) + } + } +} + // TestEth_SendRawTransaction post eth raw transaction with ethclient from go-ethereum. func TestEth_SendRawTransaction(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), OasisBlockTimeout) From 1ca188380a4dfe4f5b23750c2f040fea4411707e Mon Sep 17 00:00:00 2001 From: ptrus Date: Wed, 20 Nov 2024 10:09:24 +0100 Subject: [PATCH 2/2] feat: support eth_maxPriorityFeePerGas query --- rpc/eth/api.go | 9 +++++++++ rpc/eth/metrics/api.go | 9 +++++++++ tests/rpc/rpc_test.go | 9 +++++++++ 3 files changed, 27 insertions(+) diff --git a/rpc/eth/api.go b/rpc/eth/api.go index 7abd0f00..e3144a4a 100644 --- a/rpc/eth/api.go +++ b/rpc/eth/api.go @@ -64,6 +64,8 @@ type API interface { ChainId() (*hexutil.Big, error) // GasPrice returns a suggestion for a gas price for legacy transactions. GasPrice(ctx context.Context) (*hexutil.Big, error) + // MaxPriorityFeePerGas returns a suggestion for a gas tip cap for dynamic fee transactions + MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, error) // FeeHistory returns the transaction base fee per gas and effective priority fee per gas for the requested/supported block range. FeeHistory(ctx context.Context, blockCount math.HexOrDecimal64, lastBlock ethrpc.BlockNumber, rewardPercentiles []float64) (*gas.FeeHistoryResult, error) // GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. @@ -294,6 +296,13 @@ func (api *publicAPI) GasPrice(_ context.Context) (*hexutil.Big, error) { return api.gasPriceOracle.GasPrice(), nil } +func (api *publicAPI) MaxPriorityFeePerGas(_ context.Context) (*hexutil.Big, error) { + logger := api.Logger.With("method", "eth_maxPriorityFeePerGas") + logger.Debug("request") + + return api.gasPriceOracle.GasPrice(), nil +} + func (api *publicAPI) FeeHistory(_ context.Context, blockCount math.HexOrDecimal64, lastBlock ethrpc.BlockNumber, rewardPercentiles []float64) (*gas.FeeHistoryResult, error) { logger := api.Logger.With("method", "eth_feeHistory", "block_count", blockCount, "last_block", lastBlock, "reward_percentiles", rewardPercentiles) logger.Debug("request") diff --git a/rpc/eth/metrics/api.go b/rpc/eth/metrics/api.go index 508dc118..052aad26 100644 --- a/rpc/eth/metrics/api.go +++ b/rpc/eth/metrics/api.go @@ -148,6 +148,15 @@ func (m *metricsWrapper) GasPrice(ctx context.Context) (res *hexutil.Big, err er return } +// MaxPriorityFeePerGas implements eth.API. +func (m *metricsWrapper) MaxPriorityFeePerGas(ctx context.Context) (res *hexutil.Big, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_maxPriorityFeePerGas") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.MaxPriorityFeePerGas(ctx) + return +} + // FeeHistory implements eth.API. func (m *metricsWrapper) FeeHistory(ctx context.Context, blockCount math.HexOrDecimal64, lastBlock ethrpc.BlockNumber, rewardPercentiles []float64) (res *gas.FeeHistoryResult, err error) { r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_feeHistory") diff --git a/tests/rpc/rpc_test.go b/tests/rpc/rpc_test.go index a2d6170b..1b4bfc39 100644 --- a/tests/rpc/rpc_test.go +++ b/tests/rpc/rpc_test.go @@ -146,6 +146,15 @@ func TestEth_GasPrice(t *testing.T) { t.Logf("gas price: %v", price) } +func TestEth_MaxPriorityFeePerGas(t *testing.T) { + ec := localClient(t, false) + + price, err := ec.SuggestGasTipCap(context.Background()) + require.Nil(t, err, "get maxPriorityFeePerGas") + + t.Logf("max priority fee per gas: %v", price) +} + func TestEth_FeeHistory(t *testing.T) { ec := localClient(t, false)