feat(evmonly): add eth_getTransactionByHash RPC to the EVM-only executor - #4205
Conversation
Fixes the Foundry happy path (`cast send` with manual gas/nonce, then `cast tx <hash>`), which currently hard-fails with method-not-found. - txAPI.GetTransactionByHash decodes the raw transaction bytes already present in the committed block's Data.Txs (the same bytes eth_sendRawTransaction accepted) via export.NewRPCTransaction, reusing GetTransactionReceipt's receipt+block lookup chain (factored out as lookupFinalizedTx). - Adds EvmChainConfig to the Backend interface, plumbed from evmOnlyApplication's existing chainConfig field through proxy.Proxy (mirroring the EvmCall type-assertion pattern) and rpc/core.Environment, since NewRPCTransaction needs a real *params.ChainConfig to recover the sender. - Scoped to finalized transactions only, matching eth_getTransactionReceipt: this server has no mempool-query path, so a pending or unknown hash returns null rather than a pending-shaped result. - Documents the new method in integration_test/autobahn/README.md and narrows its "still missing" caveat accordingly.
PR SummaryMedium Risk Overview Lookups follow the same rules as The RPC Autobahn README documents the new method and Reviewed by Cursor Bugbot for commit 3898d3b. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
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 #4205 +/- ##
==========================================
- Coverage 65.55% 65.54% -0.01%
==========================================
Files 2081 2082 +1
Lines 157460 157272 -188
==========================================
- Hits 103222 103089 -133
+ Misses 54097 54042 -55
Partials 141 141
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Adds eth_getTransactionByHash to the EVM-only RPC server by reusing the receipt-store + Block() lookup (correctly extracted into a shared lookupFinalizedTx with no behavior change to GetTransactionReceipt) and decoding the raw tx from the committed block. The plumbing, index semantics, signer/fork inputs, and test coverage all check out; two non-blocking robustness/maintainability suggestions.
Findings: 0 blocking | 2 non-blocking | 2 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- None at the file/PR level.
- 2 suggestion(s)/nit(s) flagged inline on specific lines.
| stored.TransactionIndex, stored.BlockNumber, len(block.Block.Txs)) | ||
| } | ||
| ethtx := new(ethtypes.Transaction) | ||
| if err := ethtx.UnmarshalBinary(block.Block.Txs[stored.TransactionIndex]); err != nil { |
There was a problem hiding this comment.
[suggestion] The response is assembled entirely from stored.TransactionIndex without ever confirming the decoded transaction is the one that was asked for. Today the index is trustworthy — evmOnlyApplication.FinalizeBlock passes req.Txs straight through and executeBlockSequential increments txIndexUint 1:1 with that slice, so receipt index == Data.Txs index — but that invariant lives two packages away and nothing here asserts it. If the receipt store and block store ever disagree at a height (stale receipts surviving a rollback and re-execution with different ordering, a future filtering step in the prepare path), this returns a different transaction's full field set under the caller's hash rather than an error. A one-line guard after the decode makes the invariant local and turns silent wrong data into a diagnosable failure:
if ethtx.Hash() != hash {
return nil, fmt.Errorf("block %d index %d holds transaction %s, not %s", stored.BlockNumber, stored.TransactionIndex, ethtx.Hash(), hash)
}ethtx.Hash() is memoized, so the cost is one keccak on a path that already does a store read and a block fetch.
There was a problem hiding this comment.
Let's leave open design space where TxId may actually be an ID and not a hash of the body. Thus don't verify but trust.
| return nil, fmt.Errorf("block %d time is negative: %s", stored.BlockNumber, block.Block.Time) | ||
| } | ||
| // Must match the base fee EvmCall executes under (evmOnlyBaseFee). | ||
| baseFee := new(big.Int) |
There was a problem hiding this comment.
[suggestion] This hardcodes a second copy of the block base fee; the real one is evmOnlyBaseFee() in sei-tendermint/internal/evmonlyapp/app.go, and the comment is the only thing connecting them. For a DynamicFeeTx, NewRPCTransaction derives gasPrice as min(tip+baseFee, feeCap) — the same formula the receipt's effectiveGasPrice comes from via evmonly.EffectiveGasPrice(tx, baseFee). So if evmOnlyBaseFee() ever stops returning zero, eth_getTransactionByHash and eth_getTransactionReceipt start reporting different prices for the same transaction, and nothing fails: every test in tx_test.go builds its own backend, so no test compares the two endpoints against a shared base fee.
Since this PR already establishes the pattern for exactly this shape of dependency, consider plumbing it the same way — an EvmBaseFee() *big.Int on Backend backed by evmOnlyBaseFee() — so the value has one definition instead of a comment asking the next reader to keep two in sync.
Give the base fee EvmCall executes under a single definition instead of a comment linking two hardcoded copies in tx.go and call.go. - Add evmOnlyApplication.EvmBaseFee(), proxied through Proxy and Environment the same way EvmChainConfig is. - GetTransactionByHash and Call now read it from Backend instead of hardcoding new(big.Int). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Describe your changes and provide context
Adds
eth_getTransactionByHashto the Autobahn EVM-only executor's RPC server, on top ofeth_call(#4194). Validated directly against a real happy-path failure:cast sendwith fully manual nonce/gas +--async, followed bycast tx <hash>to inspect the submitted transaction, currently hard-fails withthe method eth_getTransactionByHash does not exist.txAPI.GetTransactionByHash(giga/evmonly/rpc/tx.go) reuses the same receipt-store +Block()lookup chainGetTransactionReceiptalready has (refactored into a sharedlookupFinalizedTxhelper, behavior unchanged), then decodes the raw transaction bytes out of the committed block'sData.Txs[index]and builds the response via go-ethereum'sexport.NewRPCTransaction(the sameexportpackageeth_call'sTransactionArgsalready uses, zero Cosmos dependency)EvmChainConfigplumbed throughProxy/Environment/evmOnlyApplication, mirroring the existingEvmCalltype-assertion pattern rather than widening the sharedabci.Applicationinterface —NewRPCTransactionneeds a full*params.ChainConfig, whichevmOnlyApplicationalready builds once and now just exposesGetTransactionReceipt: finalized only,nullfor pending/unknown hashes; no mempool lookup (no such plumbing exists in this server'sBackend)evmrpcreference implementation:Fromis patched from the stored receipt when sender recovery fails (e.g. a legacy tx signed for a different chain ID than the node's), covered by a dedicated testcast txusage example in the Autobahn READMETesting performed to validate your change
go build ./...go test -count=1 ./giga/evmonly/... ./sei-tendermint/internal/proxy/... ./sei-tendermint/internal/evmonlyapp/... ./sei-tendermint/internal/rpc/core/... ./sei-tendermint/node/...tx_test.go: full field-accurate decode for both aDynamicFeeTxand aLegacyTxsent through a real committed block, the sender-recovery-failure/From-patch edge case, null-for-unknown-hash, null-for-not-yet-committed, and a full JSON-RPC round trip viahttptestgolangci-lint runv2.13.2 scoped to all touched packages: 0 issuesgofmt -s/goimportsclean🤖 Generated with Claude Code