diff --git a/evmrpc/simulate.go b/evmrpc/simulate.go index 50d4f1ca12..8da2dd2f1c 100644 --- a/evmrpc/simulate.go +++ b/evmrpc/simulate.go @@ -261,10 +261,11 @@ func NewBackend( } func (b *Backend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (vm.StateDB, *ethtypes.Header, error) { - height, isLatestBlock, err := b.getBlockHeight(ctx, blockNrOrHash) + tmBlock, isLatestBlock, err := b.getBlockByNumberOrHash(ctx, blockNrOrHash) if err != nil { return nil, nil, err } + height := tmBlock.Block.Height isWasmdCall, ok := ctx.Value(CtxIsWasmdPrecompileCallKey).(bool) sdkCtx := b.ctxProvider(height).WithIsEVM(true).WithEVMEntryViaWasmdPrecompile(ok && isWasmdCall) if !isLatestBlock { @@ -273,7 +274,7 @@ func (b *Backend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHas return nil, nil, err } } - header := b.getHeader(big.NewInt(height)) + header := b.getHeader(ctx, big.NewInt(height), tmBlock) header.BaseFee = b.keeper.GetNextBaseFeePerGas(b.ctxProvider(LatestCtxHeight)).TruncateInt().BigInt() return state.NewDBImpl(sdkCtx, b.keeper, true), header, nil } @@ -388,7 +389,7 @@ func (b Backend) BlockByNumber(ctx context.Context, bn rpc.BlockNumber) (*ethtyp }) } } - header := b.getHeader(big.NewInt(blockNum)) + header := b.getHeader(ctx, big.NewInt(blockNum), tmBlock) block := ðtypes.Block{ Header_: header, Txs: txs, @@ -433,11 +434,12 @@ func (b *Backend) Engine() consensus.Engine { } func (b *Backend) HeaderByNumber(ctx context.Context, bn rpc.BlockNumber) (*ethtypes.Header, error) { - height, _, err := b.getBlockHeight(ctx, rpc.BlockNumberOrHashWithNumber(bn)) + tmBlock, _, err := b.getBlockByNumberOrHash(ctx, rpc.BlockNumberOrHashWithNumber(bn)) if err != nil { return nil, err } - return b.getHeader(big.NewInt(height)), nil + height := tmBlock.Block.Height + return b.getHeader(ctx, big.NewInt(height), tmBlock), nil } func (b *Backend) StateAtTransaction(ctx context.Context, block *ethtypes.Block, txIndex int, reexec uint64) (*ethtypes.Transaction, vm.BlockContext, vm.StateDB, tracers.StateReleaseFunc, error) { @@ -552,7 +554,7 @@ func (b *Backend) GetEVM(_ context.Context, msg *core.Message, stateDB vm.StateD } func (b *Backend) CurrentHeader() *ethtypes.Header { - header := b.getHeader(big.NewInt(b.ctxProvider(LatestCtxHeight).BlockHeight())) + header := b.getHeader(context.Background(), big.NewInt(b.ctxProvider(LatestCtxHeight).BlockHeight()), nil) header.BaseFee = b.keeper.GetNextBaseFeePerGas(b.ctxProvider(LatestCtxHeight)).TruncateInt().BigInt() return header } @@ -561,7 +563,9 @@ func (b *Backend) SuggestGasTipCap(context.Context) (*big.Int, error) { return utils.Big0, nil } -func (b *Backend) getBlockHeight(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (int64, bool, error) { +// getBlockByNumberOrHash resolves blockNrOrHash to a Tendermint ResultBlock in one RPC path +// (by hash or by number, including latest). Callers can pass tmBlock to getHeader to avoid a second block fetch. +func (b *Backend) getBlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*coretypes.ResultBlock, bool, error) { var ( block *coretypes.ResultBlock err error @@ -571,16 +575,16 @@ func (b *Backend) getBlockHeight(ctx context.Context, blockNrOrHash rpc.BlockNum if blockNrOrHash.BlockHash != nil { block, err = blockByHashRespectingWatermarks(ctx, b.tmClient, b.watermarks, blockNrOrHash.BlockHash[:], 1) if err != nil { - return 0, false, err + return nil, false, err } - return block.Block.Height, false, nil + return block, false, nil } var blockNumberPtr *int64 if blockNrOrHash.BlockNumber != nil { blockNumberPtr, err = getBlockNumber(ctx, b.tmClient, *blockNrOrHash.BlockNumber) if err != nil { - return 0, false, err + return nil, false, err } if blockNumberPtr == nil { isLatestBlock = true @@ -590,25 +594,30 @@ func (b *Backend) getBlockHeight(ctx context.Context, blockNrOrHash rpc.BlockNum } block, err = blockByNumberRespectingWatermarks(ctx, b.tmClient, b.watermarks, blockNumberPtr, 1) if err != nil { - return 0, false, err + return nil, false, err } - return block.Block.Height, isLatestBlock, nil + return block, isLatestBlock, nil } -func (b *Backend) getHeader(blockNumber *big.Int) *ethtypes.Header { +func (b *Backend) getHeader(ctx context.Context, blockNumber *big.Int, tmBlock *coretypes.ResultBlock) *ethtypes.Header { zeroExcessBlobGas := uint64(0) baseFee := b.keeper.GetNextBaseFeePerGas(b.ctxProvider(blockNumber.Int64() - 1)).TruncateInt().BigInt() - ctx := b.ctxProvider(blockNumber.Int64()) - if ctx.ChainID() == "pacific-1" && ctx.BlockHeight() < b.keeper.UpgradeKeeper().GetDoneHeight(ctx.WithGasMeter(sdk.NewInfiniteGasMeter(1, 1)), "6.2.0") { + sdkCtx := b.ctxProvider(blockNumber.Int64()) + if sdkCtx.ChainID() == "pacific-1" && sdkCtx.BlockHeight() < b.keeper.UpgradeKeeper().GetDoneHeight(sdkCtx.WithGasMeter(sdk.NewInfiniteGasMeter(1, 1)), "6.2.0") { baseFee = nil } - // Get block results to access consensus parameters number := blockNumber.Int64() - block, blockErr := blockByNumberRespectingWatermarks(context.Background(), b.tmClient, b.watermarks, &number, 1) + var block *coretypes.ResultBlock + var blockErr error + if tmBlock != nil { + block = tmBlock + } else { + block, blockErr = blockByNumberRespectingWatermarks(ctx, b.tmClient, b.watermarks, &number, 1) + } var gasLimit uint64 if blockErr == nil { // Try to get consensus parameters from block results - blockRes, blockResErr := blockResultsWithRetry(context.Background(), b.tmClient, &number) + blockRes, blockResErr := blockResultsWithRetry(ctx, b.tmClient, &number) if blockResErr == nil && blockRes.ConsensusParamUpdates != nil && blockRes.ConsensusParamUpdates.Block != nil { gasLimit = uint64(blockRes.ConsensusParamUpdates.Block.MaxGas) //nolint:gosec } else { diff --git a/evmrpc/simulate_test.go b/evmrpc/simulate_test.go index a00df9b966..89bd731bf3 100644 --- a/evmrpc/simulate_test.go +++ b/evmrpc/simulate_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/export" @@ -28,18 +29,13 @@ import ( "github.com/stretchr/testify/require" ) -// brFailClient fails BlockResults; bcFailClient fails Block +// brFailClient fails BlockResults type brFailClient struct{ *MockClient } func (br brFailClient) BlockResults(ctx context.Context, h *int64) (*coretypes.ResultBlockResults, error) { return nil, fmt.Errorf("fail br") } -type bcFailClient struct { - *MockClient - first bool -} - func primeReceiptStore(t *testing.T, store receipt.ReceiptStore, latest int64) { t.Helper() if store == nil { @@ -52,11 +48,10 @@ func primeReceiptStore(t *testing.T, store receipt.ReceiptStore, latest int64) { require.NoError(t, store.SetEarliestVersion(1)) } -func (bc *bcFailClient) Block(ctx context.Context, h *int64) (*coretypes.ResultBlock, error) { - if !bc.first { - bc.first = true - return bc.MockClient.Block(ctx, h) - } +// bcAlwaysFailClient fails every Block call (header resolution uses a single block fetch). +type bcAlwaysFailClient struct{ *MockClient } + +func (bc bcAlwaysFailClient) Block(ctx context.Context, h *int64) (*coretypes.ResultBlock, error) { return nil, fmt.Errorf("fail bc") } @@ -451,13 +446,52 @@ func TestGasLimitFallbackToDefault(t *testing.T) { require.NoError(t, err) require.Equal(t, uint64(10_000_000), h1.GasLimit) // DefaultBlockGasLimit - // Case 2: Block fails - bcClient := &bcFailClient{MockClient: &MockClient{}} + // Case 2: Block fails — with one RPC path for the block, resolution errors out entirely. + bcClient := &bcAlwaysFailClient{MockClient: &MockClient{}} watermarks2 := evmrpc.NewWatermarkManager(bcClient, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore()) backend2 := evmrpc.NewBackend(ctxProvider, &testApp.EvmKeeper, legacyabci.BeginBlockKeepers{}, func(int64) client.TxConfig { return TxConfig }, bcClient, cfg, testApp.BaseApp, testApp.TracerAnteHandler, evmrpc.NewBlockCache(3000), &sync.Mutex{}, watermarks2) - h2, err := backend2.HeaderByNumber(context.Background(), 1) - require.NoError(t, err) - require.Equal(t, uint64(10_000_000), h2.GasLimit) // DefaultBlockGasLimit + _, err = backend2.HeaderByNumber(context.Background(), 1) + require.Error(t, err) +} + +// Exercises getHeader when tmBlock is nil (CurrentHeader): successful block fetch vs gas-limit +// fallback when Block RPC fails. HeaderByNumber / BlockByNumber already cover the tmBlock != nil path. +func TestSimulateBackendBlockResolutionCoverage(t *testing.T) { + testApp := app.Setup(t, false, false, false) + baseCtx := testApp.GetContextForDeliverTx([]byte{}).WithBlockHeight(1) + ctxProvider := func(h int64) sdk.Context { + if h == evmrpc.LatestCtxHeight { + return baseCtx + } + return baseCtx.WithBlockHeight(h) + } + cfg := &evmrpc.SimulateConfig{GasCap: 10_000_000, EVMTimeout: time.Second} + primeReceiptStore(t, testApp.EvmKeeper.ReceiptStore(), 1) + tmClient := &MockClient{} + watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore()) + backend := evmrpc.NewBackend(ctxProvider, &testApp.EvmKeeper, + legacyabci.BeginBlockKeepers{}, func(int64) client.TxConfig { return TxConfig }, + tmClient, cfg, testApp.BaseApp, testApp.TracerAnteHandler, evmrpc.NewBlockCache(3000), &sync.Mutex{}, watermarks) + + t.Run("CurrentHeader_fetches_block_when_tmBlock_nil", func(t *testing.T) { + h := backend.CurrentHeader() + require.NotNil(t, h) + require.Equal(t, int64(1), h.Number.Int64()) + require.Equal(t, common.BytesToHash(MockBlockID.Hash), h.ParentHash) + }) + + t.Run("CurrentHeader_fallback_gas_limit_when_block_unavailable", func(t *testing.T) { + bcClient := &bcAlwaysFailClient{MockClient: &MockClient{}} + wm := evmrpc.NewWatermarkManager(bcClient, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore()) + b2 := evmrpc.NewBackend(ctxProvider, &testApp.EvmKeeper, + legacyabci.BeginBlockKeepers{}, func(int64) client.TxConfig { return TxConfig }, + bcClient, cfg, testApp.BaseApp, testApp.TracerAnteHandler, evmrpc.NewBlockCache(3000), &sync.Mutex{}, wm) + h := b2.CurrentHeader() + require.NotNil(t, h) + require.Equal(t, int64(1), h.Number.Int64()) + require.Equal(t, uint64(10_000_000), h.GasLimit) + require.Equal(t, common.Hash{}, h.ParentHash) + }) } func TestSimulationAPIRequestLimiter(t *testing.T) {