From 0de946d814517847a5d9ee74e3e3e5a7fdc173a2 Mon Sep 17 00:00:00 2001 From: Abdul Azeem Date: Thu, 30 Oct 2025 15:02:13 +0530 Subject: [PATCH 1/3] feat : Adds caching in feehistory RPC endpoint Implements caching for CometBFT block results and fee market parameters, reducing the number of direct RPC calls. This significantly improves performance by storing frequently accessed data in memory and reusing it. The cache is bound to `FeeHistoryCap * 2` to prevent unbounded memory growth. Uses a read/write mutex to ensure thread-safe access to the cache. chore : improve formatting --- rpc/backend/backend.go | 55 ++++++++++++++++++++++++++++++++++-------- rpc/backend/comet.go | 37 +++++++++++++++++++++++++++- rpc/backend/utils.go | 6 ++--- 3 files changed, 83 insertions(+), 15 deletions(-) diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 06079229d..9a441418b 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "math/big" + "sync" "time" "github.com/ethereum/go-ethereum/common" @@ -22,6 +23,7 @@ import ( "github.com/cosmos/evm/rpc/types" "github.com/cosmos/evm/server/config" servertypes "github.com/cosmos/evm/server/types" + feemarkettypes "github.com/cosmos/evm/x/feemarket/types" evmtypes "github.com/cosmos/evm/x/vm/types" "cosmossdk.io/log" @@ -173,6 +175,12 @@ type Backend struct { Indexer servertypes.EVMTxIndexer ProcessBlocker ProcessBlocker Mempool *evmmempool.ExperimentalEVMMempool + + // simple caches + cacheMu sync.RWMutex + cometBlockCache map[int64]*tmrpctypes.ResultBlock + cometBlockResultsCache map[int64]*tmrpctypes.ResultBlockResults + feeParamsCache map[int64]feemarkettypes.Params } func (b *Backend) GetConfig() config.Config { @@ -199,17 +207,44 @@ func NewBackend( } b := &Backend{ - Ctx: context.Background(), - ClientCtx: clientCtx, - RPCClient: rpcClient, - QueryClient: types.NewQueryClient(clientCtx), - Logger: logger.With("module", "backend"), - EvmChainID: big.NewInt(int64(appConf.EVM.EVMChainID)), //nolint:gosec // G115 // won't exceed uint64 - Cfg: appConf, - AllowUnprotectedTxs: allowUnprotectedTxs, - Indexer: indexer, - Mempool: mempool, + Ctx: context.Background(), + ClientCtx: clientCtx, + RPCClient: rpcClient, + QueryClient: types.NewQueryClient(clientCtx), + Logger: logger.With("module", "backend"), + EvmChainID: big.NewInt(int64(appConf.EVM.EVMChainID)), //nolint:gosec // G115 // won't exceed uint64 + Cfg: appConf, + AllowUnprotectedTxs: allowUnprotectedTxs, + Indexer: indexer, + Mempool: mempool, + cometBlockCache: make(map[int64]*tmrpctypes.ResultBlock), + cometBlockResultsCache: make(map[int64]*tmrpctypes.ResultBlockResults), + feeParamsCache: make(map[int64]feemarkettypes.Params), } b.ProcessBlocker = b.ProcessBlock return b } + +// getFeeMarketParamsAtHeight returns FeeMarket params for a given height using a height-keyed cache. +func (b *Backend) getFeeMarketParamsAtHeight(height int64) (feemarkettypes.Params, error) { + b.cacheMu.RLock() + if p, ok := b.feeParamsCache[height]; ok { + b.cacheMu.RUnlock() + return p, nil + } + b.cacheMu.RUnlock() + res, err := b.QueryClient.FeeMarket.Params(types.ContextWithHeight(height), &feemarkettypes.QueryParamsRequest{}) + if err != nil { + return feemarkettypes.Params{}, err + } + b.cacheMu.Lock() + if cap := int(b.Cfg.JSONRPC.FeeHistoryCap) * 2; cap > 0 && len(b.feeParamsCache) >= cap { + for k := range b.feeParamsCache { + delete(b.feeParamsCache, k) + break + } + } + b.feeParamsCache[height] = res.Params + b.cacheMu.Unlock() + return res.Params, nil +} diff --git a/rpc/backend/comet.go b/rpc/backend/comet.go index 3b295b08b..bdb32a79c 100644 --- a/rpc/backend/comet.go +++ b/rpc/backend/comet.go @@ -19,6 +19,13 @@ func (b *Backend) CometBlockByNumber(blockNum rpctypes.BlockNumber) (*cmtrpctype if err != nil { return nil, err } + // cache lookup + b.cacheMu.RLock() + if cached, ok := b.cometBlockCache[height]; ok { + b.cacheMu.RUnlock() + return cached, nil + } + b.cacheMu.RUnlock() resBlock, err := b.RPCClient.Block(b.Ctx, &height) if err != nil { b.Logger.Debug("cometbft client failed to get block", "height", height, "error", err.Error()) @@ -30,6 +37,16 @@ func (b *Backend) CometBlockByNumber(blockNum rpctypes.BlockNumber) (*cmtrpctype return nil, nil } + // store in cache (simple bound: FeeHistoryCap*2) + b.cacheMu.Lock() + if cap := int(b.Cfg.JSONRPC.FeeHistoryCap) * 2; cap > 0 && len(b.cometBlockCache) >= cap { + for k := range b.cometBlockCache { + delete(b.cometBlockCache, k) + break + } + } + b.cometBlockCache[height] = resBlock + b.cacheMu.Unlock() return resBlock, nil } @@ -49,11 +66,29 @@ func (b *Backend) CometBlockResultByNumber(height *int64) (*cmtrpctypes.ResultBl if height != nil && *height == 0 { height = nil } + if height != nil { + b.cacheMu.RLock() + if cached, ok := b.cometBlockResultsCache[*height]; ok { + b.cacheMu.RUnlock() + return cached, nil + } + b.cacheMu.RUnlock() + } res, err := b.RPCClient.BlockResults(b.Ctx, height) if err != nil { return nil, fmt.Errorf("failed to fetch block result from CometBFT %d: %w", *height, err) } - + if height != nil { + b.cacheMu.Lock() + if cap := int(b.Cfg.JSONRPC.FeeHistoryCap) * 2; cap > 0 && len(b.cometBlockResultsCache) >= cap { + for k := range b.cometBlockResultsCache { + delete(b.cometBlockResultsCache, k) + break + } + } + b.cometBlockResultsCache[*height] = res + b.cacheMu.Unlock() + } return res, nil } diff --git a/rpc/backend/utils.go b/rpc/backend/utils.go index ca46109b0..f721af541 100644 --- a/rpc/backend/utils.go +++ b/rpc/backend/utils.go @@ -20,7 +20,6 @@ import ( "github.com/cosmos/evm/rpc/types" "github.com/cosmos/evm/utils" - feemarkettypes "github.com/cosmos/evm/x/feemarket/types" evmtypes "github.com/cosmos/evm/x/vm/types" "cosmossdk.io/log" @@ -176,12 +175,11 @@ func (b *Backend) ProcessBlock( targetOneFeeHistory.BlobGasUsedRatio = 0 if cfg.IsLondon(big.NewInt(blockHeight + 1)) { - ctx := types.ContextWithHeight(blockHeight) - params, err := b.QueryClient.FeeMarket.Params(ctx, &feemarkettypes.QueryParamsRequest{}) + feeParams, err := b.getFeeMarketParamsAtHeight(blockHeight) if err != nil { return err } - nextBaseFee, err := types.CalcBaseFee(cfg, &header, params.Params) + nextBaseFee, err := types.CalcBaseFee(cfg, &header, feeParams) if err != nil { return err } From 8ea864bd36c2a3e6be1880d91f1c84b46c450fa6 Mon Sep 17 00:00:00 2001 From: Abdul Azeem Date: Thu, 30 Oct 2025 15:04:40 +0530 Subject: [PATCH 2/3] feat : Optimizes FeeHistory by reusing CometBFT data This commit optimizes the FeeHistory RPC method by reusing the CometBFT block and result data already fetched. This avoids redundant data fetching and improves performance. Additionally, this commit adds benchmark scripts for feeHistory to measure the impact of the caching. chore : improve formatting again --- rpc/backend/chain_info.go | 14 +++++++------- scripts/feeHistory_bench.ps1 | 25 +++++++++++++++++++++++++ scripts/feeHistory_bench.sh | 25 +++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 scripts/feeHistory_bench.ps1 create mode 100644 scripts/feeHistory_bench.sh diff --git a/rpc/backend/chain_info.go b/rpc/backend/chain_info.go index 3686a3250..265f27c35 100644 --- a/rpc/backend/chain_info.go +++ b/rpc/backend/chain_info.go @@ -247,13 +247,6 @@ func (b *Backend) FeeHistory( return } - // eth block - ethBlock, err := b.GetBlockByNumber(blockNum, true) - if ethBlock == nil { - chanErr <- err - return - } - // CometBFT block result cometBlockResult, err := b.CometBlockResultByNumber(&cometBlock.Block.Height) if cometBlockResult == nil { @@ -262,6 +255,13 @@ func (b *Backend) FeeHistory( return } + // Build Ethereum-formatted block using the already fetched Comet block and results + ethBlock, err := b.RPCBlockFromCometBlock(cometBlock, cometBlockResult, true) + if err != nil { + chanErr <- err + return + } + oneFeeHistory := rpctypes.OneFeeHistory{} err = b.ProcessBlocker(cometBlock, ðBlock, rewardPercentiles, cometBlockResult, &oneFeeHistory) if err != nil { diff --git a/scripts/feeHistory_bench.ps1 b/scripts/feeHistory_bench.ps1 new file mode 100644 index 000000000..b46e1c7b1 --- /dev/null +++ b/scripts/feeHistory_bench.ps1 @@ -0,0 +1,25 @@ +param( + [string]$Endpoint = "http://127.0.0.1:8545", + [string]$Blocks = "0x40", + [int]$Rounds = 8, + [int[]]$Percentiles = @(25,50,75) +) + +$body = @{ jsonrpc = "2.0"; id = 1; method = "eth_feeHistory"; params = @($Blocks, "latest", $Percentiles) } | ConvertTo-Json -Compress +$times = @() +Write-Host ("eth_feeHistory {0}, percentiles=[{1}], rounds={2}" -f $Blocks, ($Percentiles -join ","), $Rounds) +for ($i=1; $i -le $Rounds; $i++) { + $sw = [System.Diagnostics.Stopwatch]::StartNew() + try { Invoke-RestMethod -Uri $Endpoint -Method Post -ContentType "application/json" -Body $body | Out-Null } catch {} + $sw.Stop() + $ms = [int][Math]::Round($sw.Elapsed.TotalMilliseconds) + Write-Host ("Run {0}: {1} ms" -f $i, $ms) + $times += $ms + Start-Sleep -Milliseconds 150 +} +$avg = [Math]::Round(($times | Measure-Object -Average).Average,0) +$min = ($times | Measure-Object -Minimum).Minimum +$max = ($times | Measure-Object -Maximum).Maximum +Write-Host ("Avg: {0} ms Min: {1} ms Max: {2} ms" -f $avg, $min, $max) + + diff --git a/scripts/feeHistory_bench.sh b/scripts/feeHistory_bench.sh new file mode 100644 index 000000000..673beaac3 --- /dev/null +++ b/scripts/feeHistory_bench.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +ENDPOINT=${1:-http://127.0.0.1:8545} +BLOCKS=${2:-0x40} +ROUNDS=${3:-8} +PCTS=${4:-[25,50,75]} + +BODY='{"jsonrpc":"2.0","id":1,"method":"eth_feeHistory","params":["'"$BLOCKS"'","latest",'"$PCTS"']}' + +sum=0; min=999999; max=0 +echo "eth_feeHistory $BLOCKS, percentiles=$PCTS, rounds=$ROUNDS" +for i in $(seq 1 "$ROUNDS"); do + t=$(curl -s -o /dev/null -w '%{time_total}\n' -H 'Content-Type: application/json' -d "$BODY" "$ENDPOINT") + t_ms=$(awk -v t="$t" 'BEGIN { printf("%.0f", t*1000) }') + echo "Run $i: ${t_ms} ms" + sum=$((sum + t_ms)) + (( t_ms < min )) && min=$t_ms + (( t_ms > max )) && max=$t_ms + sleep 0.15 +done +avg=$((sum / ROUNDS)) +printf "Avg: %d ms Min: %d ms Max: %d ms\n" "$avg" "$min" "$max" + + From 549564ad2dfcbb503d82e6b21d522ff38ec6d9fc Mon Sep 17 00:00:00 2001 From: Abdul Azeem Date: Fri, 31 Oct 2025 11:15:14 +0530 Subject: [PATCH 3/3] feat: adds block gas limit caching by height Implements a height-keyed cache for consensus block gas limits. This improves performance by reducing the number of queries to the consensus parameters, especially when fetching multiple blocks within a similar height range. The cache is pruned to align with the fee history window, preventing unbounded memory usage. --- rpc/backend/backend.go | 30 ++++++++++++++++++++++++++++++ rpc/backend/comet_to_eth.go | 5 ++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 9a441418b..0136d7772 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -181,6 +181,7 @@ type Backend struct { cometBlockCache map[int64]*tmrpctypes.ResultBlock cometBlockResultsCache map[int64]*tmrpctypes.ResultBlockResults feeParamsCache map[int64]feemarkettypes.Params + consensusGasLimitCache map[int64]int64 } func (b *Backend) GetConfig() config.Config { @@ -220,6 +221,7 @@ func NewBackend( cometBlockCache: make(map[int64]*tmrpctypes.ResultBlock), cometBlockResultsCache: make(map[int64]*tmrpctypes.ResultBlockResults), feeParamsCache: make(map[int64]feemarkettypes.Params), + consensusGasLimitCache: make(map[int64]int64), } b.ProcessBlocker = b.ProcessBlock return b @@ -248,3 +250,31 @@ func (b *Backend) getFeeMarketParamsAtHeight(height int64) (feemarkettypes.Param b.cacheMu.Unlock() return res.Params, nil } + +// BlockMaxGasAtHeight returns the consensus block gas limit for a given height using a height-keyed cache. +func (b *Backend) BlockMaxGasAtHeight(height int64) (int64, error) { + b.cacheMu.RLock() + if gl, ok := b.consensusGasLimitCache[height]; ok { + b.cacheMu.RUnlock() + return gl, nil + } + b.cacheMu.RUnlock() + + ctx := types.ContextWithHeight(height) + gasLimit, err := types.BlockMaxGasFromConsensusParams(ctx, b.ClientCtx, height) + if err != nil { + return gasLimit, err + } + + b.cacheMu.Lock() + // simple prune aligned with fee history window + if cap := int(b.Cfg.JSONRPC.FeeHistoryCap) * 2; cap > 0 && len(b.consensusGasLimitCache) >= cap { + for k := range b.consensusGasLimitCache { + delete(b.consensusGasLimitCache, k) + break + } + } + b.consensusGasLimitCache[height] = gasLimit + b.cacheMu.Unlock() + return gasLimit, nil +} diff --git a/rpc/backend/comet_to_eth.go b/rpc/backend/comet_to_eth.go index 8b4847726..27475c7ed 100644 --- a/rpc/backend/comet_to_eth.go +++ b/rpc/backend/comet_to_eth.go @@ -143,9 +143,8 @@ func (b *Backend) EthBlockFromCometBlock( return nil, fmt.Errorf("failed to get miner(block proposer) address from comet block") } - // 3. get block gasLimit - ctx := rpctypes.ContextWithHeight(cmtBlock.Height) - gasLimit, err := rpctypes.BlockMaxGasFromConsensusParams(ctx, b.ClientCtx, cmtBlock.Height) + // 3. get block gasLimit (cached by height) + gasLimit, err := b.BlockMaxGasAtHeight(cmtBlock.Height) if err != nil { b.Logger.Error("failed to query consensus params", "error", err.Error()) }