diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 06079229d..0136d7772 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,13 @@ 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 + consensusGasLimitCache map[int64]int64 } func (b *Backend) GetConfig() config.Config { @@ -199,17 +208,73 @@ 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), + consensusGasLimitCache: make(map[int64]int64), } 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 +} + +// 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/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/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/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()) } 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 } 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" + +