feat(evmonly): add eth_call RPC to the EVM-only executor - #4194
Conversation
Adds a read-only eth_call to the Autobahn giga/evmonly RPC surface, reusing the executor's existing EVM-running machinery through a new Executor.Call that opens a store snapshot, builds a vm.EVM, and discards the overlay after execution without ever committing it. - giga/evmonly: Executor.Call runs a core.Message against current state via a fresh store snapshot; nothing it does is ever persisted. - evmonlyapp: evmOnlyApplication.EvmCall builds the call's block context from currently tracked state, extended with a persisted lastBlockTime (mirroring appHash/parentHash) so Time/PrevRandao can be reproduced for a call. - proxy: Proxy.EvmCall reaches the application through a narrow evmCaller capability check instead of abci.Application, since only the EVM-only application can run a call. - rpc: new callAPI registers eth_call, reusing go-ethereum's export.TransactionArgs for arg decoding/gas-cap defaulting and a local revertError matching evmrpc's JSON-RPC revert shape. - docs: update the autobahn README's RPC surface and known-gaps lists.
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## giga-1 #4194 +/- ##
========================================
Coverage 65.55% 65.55%
========================================
Files 2081 2083 +2
Lines 157460 157280 -180
========================================
- Hits 103222 103106 -116
+ Misses 54097 54033 -64
Partials 141 141
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…vm's minimum - Tighten the eth_call path's godocs (giga/evmonly, evmonlyapp, proxy) to state what a function does rather than why/how; surviving rationale moves inline at the code that needs it. - eth_call's TransactionArgs base fee now defaults to x/evm's DefaultMinFeePerGas instead of a hardcoded zero, matching the floor a fresh chain executes at.
PR SummaryMedium Risk Overview Execution path: new RPC layer: decodes Reviewed by Cursor Bugbot for commit cda82e7. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Adds a read-only eth_call to the EVM-only executor RPC, reusing the existing block-execution machinery and the requireCurrentState gate, with good test coverage (including two state-isolation tests). No blockers; two consistency issues worth addressing are the base fee used to build the message and the possible skew between the state a call reads and the block context it runs under.
Findings: 0 blocking | 3 non-blocking | 2 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- [suggestion]
eth_callhere has neither an execution timeout nor a concurrency limit, unlikeevmrpc'sSimulationAPI.Call(which usesRPCEVMTimeoutand arequestLimiter).Executor.Callonly checksctx.Err()before execution andcore.ApplyMessageis not cancellable, so a request that hits the 10M gas cap runs to completion and holds a state view open regardless of client disconnect. The listener binds0.0.0.0:8545; considerevm.Cancel()on a timeout, or at least documenting the exposure for the load-test deployment. - 2 suggestion(s)/nit(s) flagged inline on specific lines.
| if err := requireCurrentState(block); err != nil { | ||
| return nil, err | ||
| } | ||
| baseFee := evmtypes.DefaultMinFeePerGas.TruncateInt().BigInt() |
There was a problem hiding this comment.
[suggestion] The base fee used to build the message disagrees with the base fee the call actually executes under. evmOnlyBaseFee() (sei-tendermint/internal/evmonlyapp/app.go:30) returns 0, and EvmCall sets BlockContext.BaseFee from it, so the BASEFEE opcode inside the call sees 0. Here the args are converted against DefaultMinFeePerGas (1 gwei) — the Cosmos EVM module's minimum fee parameter, which is unrelated to this application's block base fee.
For a caller that supplies EIP-1559 fields, ToMessage derives gasPrice = min(gasTipCap + baseFee, gasFeeCap); e.g. maxFeePerGas = 2 gwei, maxPriorityFeePerGas = 1 gwei yields GASPRICE == 2 gwei where a real transaction in a block would pay 1 gwei. The buyGas deduction uses that same value. Callers that omit fee fields are unaffected (both defaults land on 0), so the impact is narrow, but the two should not be able to diverge.
Passing a zero base fee here — or plumbing the executor's base fee through Backend so there is a single source — makes the message consistent with the block context it runs against and drops the x/evm/types coupling for this value.
| PrevRandao: evmOnlyPrevRandao(state.lastBlockTime), | ||
| } | ||
| } | ||
| return executor.Call(ctx, blockCtx, msg) |
There was a problem hiding this comment.
[suggestion] The block context is captured under a.state.Lock() but Executor.Call opens the state view after the lock is released, so the two can describe different heights.
The deterministic case is the window between FinalizeBlock and Commit: ExecuteBlock already runs CommitStateChanges for block N inside FinalizeBlock (giga/evmonly/giga_store.go:97), while committedHeight and lastBlockTime are only advanced in Commit. A call landing in that window reads block N's storage but runs with NUMBER = N-1, TIMESTAMP and PrevRandao from block N-1. A view function mixing storage with block.number/block.timestamp returns an inconsistent pair.
Options: refuse the call while state.pending.IsPresent(), or advance committedHeight/lastBlockTime at the point the store write happens rather than at Commit. Either way it is worth a test that pins the behaviour in the post-FinalizeBlock, pre-Commit window — the existing tests all call after Commit.
… window - Executor.Call now runs with vm.Config.NoBaseFee, matching go-ethereum's own eth_call semantics so a caller leaving fee fields at zero isn't rejected by the fee-cap-vs-basefee check. - rpc/call.go's message-building base fee now matches the zero base fee the call actually executes under, instead of an unrelated x/evm minimum-fee constant. - EvmCall now refuses while a finalized block is pending Commit, since the store already reflects the new block while tracked NUMBER/TIMESTAMP/ PrevRandao still describe the previous one in that window.
Condense the NoBaseFee, base fee, and pending-commit comments to one line each, stating what/why without restating the surrounding code.
…y-call # Conflicts: # sei-tendermint/internal/evmonlyapp/app.go
| gasLimit: block.GasLimit, | ||
| } | ||
| state.pending = utils.Some(next) | ||
| state.pendingBlockTime = block.Time |
There was a problem hiding this comment.
Block time not persisted across restart
Medium Severity
lastBlockTime is only kept in memory. The cursor changeset still encodes height, hashes, and gas limit, so a restart reloads those fields but leaves Time and PrevRandao at zero until the next Commit. eth_call then runs against the restored height with a genesis timestamp rather than the last committed block’s real time.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 80c24fb. Configure here.
core.ApplyMessage doesn't respect ctx, so a call priced under the 10M gas cap but CPU-expensive (e.g. modexp with adversarial inputs) could hold a state view open indefinitely. Executor.Call now cancels its EVM after callTimeout (60s, matching evmrpc's simulation_evm_timeout default) via evm.Cancel(), same as go-ethereum's own eth_call. Addresses a non-blocking review finding that was missed on the initial pass.
…y-call # Conflicts: # giga/evmonly/rpc/setup_test.go
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cda82e7. Configure here.
| PrevRandao: evmOnlyPrevRandao(state.lastBlockTime), | ||
| } | ||
| } | ||
| return executor.Call(ctx, blockCtx, msg) |
There was a problem hiding this comment.
Call races past pending check
Medium Severity
EvmCall rejects calls only while pending is set, then releases the cursor lock before OpenView. A FinalizeBlock can commit the next block's writes in that window, so the call executes against newer store state while still using the previous block's number, timestamp, and PrevRandao.
Reviewed by Cursor Bugbot for commit cda82e7. Configure here.


Describe your changes and provide context
Adds a read-only
eth_callJSON-RPC method to the Autobahn EVM-only executor's RPC server, on top of the eth_getBalance/eth_getTransactionCount/eth_blockNumber/eth_chainId work already ongiga-1(#4192).Executor.Call(giga/evmonly/call.go) runs a synthetic message against a state snapshot, reusing the samevm.NewEVM/buildBlockContext/customPrecompileMapmachineryexecuteBlockSequentialalready builds for real transactionseth_callRPC method (giga/evmonly/rpc/call.go), decoding args via go-ethereum'sexport.TransactionArgs(no Cosmos/keeper dependency) and reusing the existingrequireCurrentStategateProxy.EvmCalltype-asserts against a package-localevmCallercapability interface rather than addingEvmCallto the sharedabci.Applicationinterface, since only the EVM-only app can meaningfully implement it and the real cosmosAppalready has its owneth_callviaevmrpclastBlockTime, threaded through the same choke point asappHash/parentHash, so the block context used for a call (time, prev-randao) reflects the real last-committed block instead of a placeholder; documents the two remaining gaps (coinbaseis provably always the zero address,blockhash(current-1)is unavailable)evmrpc's existingsimulation_gas_limitprecedenteth_estimateGasis explicitly out of scope for this PRThis is exploratory/sizing work (opened as a draft) — not intended to merge as-is.
Testing performed to validate your change
go build ./...go test -count=1 ./giga/evmonly/... ./sei-tendermint/internal/evmonlyapp/... ./sei-tendermint/internal/proxy/... ./sei-tendermint/internal/rpc/core/...TestExecutorCallDoesNotMutateCommittedState(in-memory store) andTestEVMOnlyApplicationEvmCallDoesNotMutateCommittedState(real disk-backed FlatKV store through a fullFinalizeBlock+Commitcycle) both assert committed state is unchanged after a call that would otherwise SSTOREProxygolangci-lint runv2.13.2 scoped to all touched packages: 0 issuesgofmt -s/goimportsclean🤖 Generated with Claude Code