Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/evm_jsonrpc_unsupported.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Some Ethereum JSON-RPC methods are **registered** on Sei’s EVM endpoint but re
| `eth_blobBaseFee` | `blobs not supported on this chain` |
| `eth_syncing` | `eth_syncing is not supported on Sei EVM RPC` |
| `eth_newPendingTransactionFilter` | `eth_newPendingTransactionFilter is not supported on Sei EVM RPC` |
| `eth_getProof` | `eth_getProof is not supported yet on Sei EVM RPC; please reach out to the Sei team if you need this endpoint` |
| `debug_getRawBlock` | `debug_getRawBlock is not supported on Sei EVM RPC` |
| `debug_getRawHeader` | `debug_getRawHeader is not supported on Sei EVM RPC` |
| `debug_getRawReceipts` | `debug_getRawReceipts is not supported on Sei EVM RPC` |
Expand All @@ -19,6 +20,7 @@ Some Ethereum JSON-RPC methods are **registered** on Sei’s EVM endpoint but re
- **`eth_syncing`** — Sei’s consensus model differs from Ethereum’s sync semantics; callers should not rely on this method.
- **`eth_newPendingTransactionFilter`** — Sei has instant finality and does not expose Ethereum-style pending tx filters on this RPC.
- **`debug_getRaw*`** — Raw RLP block/header/receipt/tx payloads are not served on this surface.
- **`eth_getProof`** — Deprecated pending further work; unlike the other entries here it is not a permanent incompatibility, so the message asks callers who need proofs to contact the Sei team.

Integration coverage: each unsupported method has a dedicated `not-supported.iox` under `integration_test/evm_module/rpc_io_test/testdata/<method>/`.

Expand Down
1 change: 1 addition & 0 deletions evmrpc/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ EVM RPCs prefixed by `eth_` and `debug_` on Sei generally follows [Ethereum's sp
- `debug_getRawBlock`, `debug_getRawHeader`, `debug_getRawReceipts`, `debug_getRawTransaction`
- `eth_newPendingTransactionFilter`
- `eth_syncing`
- `eth_getProof` — deprecated rather than permanently incompatible; the message directs callers who need proofs to the Sei team.

## `sei_` prefixed endpoints
Several `eth_` prefixed endpoints have a `sei_` prefixed counterpart. `eth_` endpoints only have visibility into EVM transactions, whereas `sei_` endpoints have visibility into EVM transactions plus Cosmos transactions that have synthetic EVM receipts.
Expand Down
16 changes: 0 additions & 16 deletions evmrpc/height_availability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,19 +405,3 @@ func TestGetBlockReceiptsReceiptsPruned(t *testing.T) {
require.Error(t, err)
require.Contains(t, err.Error(), "receipts have been pruned")
}

func TestStateAPIGetProofUnavailableHeight(t *testing.T) {
t.Parallel()

earliest := int64(2)
latest := int64(80)
highHeight := latest + 4
client := newHeightTestClient(highHeight, earliest, latest)
watermarks := newHeightTestWatermarks(client, latest)
api := NewStateAPI(client, nil, testCtxProvider, ConnectionTypeHTTP, watermarks)

blockParam := rpc.BlockNumberOrHashWithHash(common.HexToHash(highBlockHashHex), true)
_, err := api.GetProof(context.Background(), common.Address{}, []string{}, blockParam)
require.Error(t, err)
require.Contains(t, err.Error(), "requested height")
}
114 changes: 5 additions & 109 deletions evmrpc/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,13 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
gigacachekv "github.com/sei-protocol/sei-chain/giga/deps/store"
"github.com/sei-protocol/sei-chain/sei-cosmos/client"
"github.com/sei-protocol/sei-chain/sei-cosmos/store/cachekv"
"github.com/sei-protocol/sei-chain/sei-cosmos/store/prefix"
"github.com/sei-protocol/sei-chain/sei-cosmos/store/tracekv"
storetypes "github.com/sei-protocol/sei-chain/sei-cosmos/store/types"
sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types"
abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types"
"github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/crypto"
"github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes"
"github.com/sei-protocol/sei-chain/x/evm/keeper"
"github.com/sei-protocol/sei-chain/x/evm/state"
"github.com/sei-protocol/sei-chain/x/evm/types"
)

var errNoProofCapableQueryableKVStore = errors.New("cannot find a proof-capable queryable KV store")

const MaxStorageKeysPerProof = 1024

type StateAPI struct {
tmClient client.LocalClient
keeper *keeper.Keeper
Expand Down Expand Up @@ -107,106 +95,14 @@ type ProofResult struct {
StorageProof []*crypto.ProofOps `json:"storageProof"`
}

func (a *StateAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (result *ProofResult, returnErr error) {
// GetProof is registered but deliberately unimplemented, so callers get the
// documented -32000 rather than a -32601 "method not found".
func (a *StateAPI) GetProof(ctx context.Context, _ common.Address, _ []string, _ rpc.BlockNumberOrHash) (_ *ProofResult, returnErr error) {
startTime := time.Now()
defer func() {
recordMetricsWithError(ctx, "eth_getProof", a.connectionType, startTime, returnErr, recover())
recordMetrics(ctx, "eth_getProof", a.connectionType, startTime)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetProof metrics recorded as success

Medium Severity

GetProof always returns an error but records metrics via recordMetrics, which treats the call as success. Other unsupported stubs pass returnErr into recordMetricsWithError, so eth_getProof will show as healthy and skip error-class counters.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8158cb8. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] recordMetrics delegates to recordMetricsWithError(..., nil, nil), so success is hardcoded to true — every eth_getProof call will now be counted as a success even though the method always returns an error, and the ErrEVMNotSupported error class / -32000 code bucket is never emitted. Every other intentionally-unsupported endpoint (eth_blobBaseFee, eth_syncing, eth_newPendingTransactionFilter, debug_getRaw*) uses the error-aware variant. Suggest matching them:

recordMetricsWithError(ctx, "eth_getProof", a.connectionType, startTime, returnErr, recover())

(Also flagged by Codex.)

}()
var block *coretypes.ResultBlock
var err error
if blockNr, ok := blockNrOrHash.Number(); ok {
blockNumber, blockNumErr := getBlockNumber(ctx, a.tmClient, blockNr)
if blockNumErr != nil {
return nil, blockNumErr
}
block, err = blockByNumberRespectingWatermarks(ctx, a.tmClient, a.watermarks, blockNumber, 1)
} else {
block, err = blockByHashRespectingWatermarks(ctx, a.tmClient, a.watermarks, blockNrOrHash.BlockHash[:], 1)
}
if err != nil {
return nil, err
}
sdkCtx := a.ctxProvider(block.Block.Height)
if err := CheckVersion(sdkCtx, a.keeper); err != nil {
return nil, err
}
queryStore, err := findQueryableKVStore(sdkCtx.MultiStore().GetKVStore(a.keeper.GetStoreKey()))
if err != nil {
return nil, err
}
if len(storageKeys) > MaxStorageKeysPerProof {
return nil, fmt.Errorf("too many storage keys: got %d, max %d", len(storageKeys), MaxStorageKeysPerProof)
}
paddedKeys := make([]common.Hash, len(storageKeys))
for i, key := range storageKeys {
paddedKey, _, err := decodeHash(key)
if err != nil {
return nil, fmt.Errorf("invalid storage key %q: %w", key, err)
}
paddedKeys[i] = paddedKey
}
proofResult := ProofResult{Address: address}
for _, paddedKey := range paddedKeys {
formattedKey := append(types.StateKey(address), paddedKey[:]...)
qres := queryStore.Query(ctx, abci.RequestQuery{
Path: "/key",
Data: formattedKey,
Height: block.Block.Height,
Prove: true,
})
proofResult.HexValues = append(proofResult.HexValues, hex.EncodeToString(qres.Value))
proofResult.StorageProof = append(proofResult.StorageProof, qres.ProofOps)
}

return &proofResult, nil
}

// findQueryableKVStore unwraps known KVStore wrappers until it reaches a types.Queryable
// (classic IAVL, store/v2 memiavl commitment, or future proof-capable roots).
// Go only allows `x := s.(type)` inside a type switch, not before it. Nil parents are
// handled by the `s == nil` check on the next iteration; nil *Store receivers are
// guarded in each pointer case so we never call methods on nil.
func findQueryableKVStore(s sdk.KVStore) (storetypes.Queryable, error) {
const maxDepth = 64
for range maxDepth {
if s == nil {
return nil, errNoProofCapableQueryableKVStore
}
switch cast := s.(type) {
case *cachekv.Store:
if cast == nil {
return nil, errNoProofCapableQueryableKVStore
}
s = cast.GetParent()
continue
case *gigacachekv.Store:
if cast == nil {
return nil, errNoProofCapableQueryableKVStore
}
s = cast.GetParent()
continue
case *tracekv.Store:
if cast == nil {
return nil, errNoProofCapableQueryableKVStore
}
s = cast.Parent()
continue
case prefix.Store:
s = cast.Parent()
continue
case *prefix.Store:
if cast == nil {
return nil, errNoProofCapableQueryableKVStore
}
s = cast.Parent()
continue
}
if q, ok := s.(storetypes.Queryable); ok {
return q, nil
}
return nil, errNoProofCapableQueryableKVStore
}
return nil, fmt.Errorf("%w: exceeded unwrap depth", errNoProofCapableQueryableKVStore)
return nil, &ErrEVMNotSupported{Msg: "eth_getProof is not supported yet; please reach out to the Sei Labs if you need this endpoint"}
}

func (a *StateAPI) GetNonce(ctx context.Context, address common.Address) uint64 {
Expand Down
87 changes: 6 additions & 81 deletions evmrpc/state_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package evmrpc_test

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
Expand All @@ -11,12 +10,7 @@ import (
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"
"github.com/sei-protocol/sei-chain/app"
"github.com/sei-protocol/sei-chain/evmrpc"
sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types"
abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types"
tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types"
testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -206,80 +200,11 @@ func TestGetStorageAt(t *testing.T) {
Ctx = Ctx.WithBlockHeight(8)
}

func TestGetProof(t *testing.T) {
testApp := app.Setup(t, false, false, false)
func TestGetProofNotSupported(t *testing.T) {
_, evmAddr := testkeeper.MockAddressPair()
key, val := []byte("test"), []byte("abc")
testApp.EvmKeeper.SetState(testApp.GetContextForDeliverTx([]byte{}), evmAddr, common.BytesToHash(key), common.BytesToHash(val))
for i := 0; i < MockHeight8; i++ {
_, err := testApp.FinalizeBlock(context.Background(), &abci.RequestFinalizeBlock{Header: &tmproto.Header{ChainID: testApp.ChainID, Height: int64(i + 1)}})
require.NoError(t, err)
testApp.SetDeliverStateToCommit()
_, err = testApp.Commit(context.Background())
require.NoError(t, err)
}
if store := testApp.EvmKeeper.ReceiptStore(); store != nil {
require.NoError(t, store.SetLatestVersion(MockHeight8))
require.NoError(t, store.SetEarliestVersion(1))
require.Equal(t, int64(1), store.EarliestVersion())
}
client := &MockClient{}
ctxProvider := func(height int64) sdk.Context {
ctx := testApp.GetCheckCtx()
switch {
case height == evmrpc.LatestCtxHeight || height <= 0:
return ctx.WithBlockHeight(MockHeight8)
default:
return ctx.WithBlockHeight(height)
}
}
watermarks := evmrpc.NewWatermarkManager(client, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore())
stateAPI := evmrpc.NewStateAPI(client, &testApp.EvmKeeper, ctxProvider, evmrpc.ConnectionTypeHTTP, watermarks)
require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000616263", testApp.EvmKeeper.GetState(testApp.GetCheckCtx(), evmAddr, common.BytesToHash(key)).Hex())
// hex-encode the storage slot as eth_getProof requires
hexKey := common.BytesToHash(key).Hex()
tests := []struct {
key string
blockNr rpc.BlockNumber
expectedVal []byte
}{
{
key: hexKey,
blockNr: rpc.BlockNumber(-2),
expectedVal: val,
},
{
key: hexKey,
blockNr: rpc.BlockNumber(8),
expectedVal: val,
},
{
// valid hex slot that has no state set
key: "0x0000000000000000000000000000000000000000000000000000000000000001",
blockNr: rpc.BlockNumber(-2),
expectedVal: []byte{},
},
}
for _, test := range tests {
bptr := &rpc.BlockNumberOrHash{BlockNumber: &test.blockNr}
res, err := stateAPI.GetProof(t.Context(), evmAddr, []string{test.key}, *bptr)
require.Nil(t, err)
vals := res.HexValues
require.Equal(t, common.BytesToHash(test.expectedVal), common.HexToHash(vals[0]))
proofs := res.StorageProof
require.Equal(t, "ics23:iavl", proofs[0].Ops[0].Type)
}

// malformed key must be rejected
bptr := &rpc.BlockNumberOrHash{BlockNumber: func() *rpc.BlockNumber { n := rpc.BlockNumber(-2); return &n }()}
_, err := stateAPI.GetProof(t.Context(), evmAddr, []string{"not-hex"}, *bptr)
require.Error(t, err)

// too many keys must be rejected
tooManyKeys := make([]string, evmrpc.MaxStorageKeysPerProof+1)
for i := range tooManyKeys {
tooManyKeys[i] = hexKey
}
_, err = stateAPI.GetProof(t.Context(), evmAddr, tooManyKeys, *bptr)
require.Error(t, err)
resObj := sendRequestGood(t, "getProof", evmAddr.Hex(), []string{}, "latest")
require.Contains(t, resObj, "error")
errObj := resObj["error"].(map[string]interface{})
require.Equal(t, float64(evmrpc.ErrCodeEVMNotSupported), errObj["code"])
require.Contains(t, errObj["message"].(string), "eth_getProof is not supported yet")
}
12 changes: 5 additions & 7 deletions integration_test/evm_module/rpc_io_test/RPC_IO_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ For a fair comparison, both endpoints should serve the **same chain** (same gene

| Kind | Count | Description |
| --------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **.io** | 97 | Request/response fixtures; curated from [ethereum/execution-apis](https://github.com/ethereum/execution-apis) plus Sei-added. |
| **.iox** | 63 | Sei-generated; use `@ bind` and optional `@ ref_pair N` so data comes from a first request; includes `not-supported.iox`, `sei_legacy_deprecation/*.iox`. |
| **Total** | 160 | All under `testdata/`; runner executes every .io and .iox file. |
| **.io** | 96 | Request/response fixtures; curated from [ethereum/execution-apis](https://github.com/ethereum/execution-apis) plus Sei-added. |
| **.iox** | 60 | Sei-generated; use `@ bind` and optional `@ ref_pair N` so data comes from a first request; includes `not-supported.iox`, `sei_legacy_deprecation/*.iox`. |
| **Total** | 156 | All under `testdata/`; runner executes every .io and .iox file. |


Fixtures live in `testdata/`; see `testdata/README.md` (do not overwrite with a raw copy from execution-apis).
Expand All @@ -88,7 +88,7 @@ The following fixtures were **removed** (no longer in the suite) because they de
| `eth_estimateGas/estimate-call-abi-error.io` | Same fixed address, expects revert error | `eth_estimateGas/estimate-call-abi-error-sei.iox` (uses `__REVERTER__`) |
| `eth_estimateGas/estimate-failed-call.io` | Fixed address `0x17e7ee...`, expects revert error | Revert (Error) and panic covered by `estimate-call-abi-error-sei.iox` and `estimate-call-abi-panic-sei.iox` (same `__REVERTER__`, input `0x01` / `0x02`) |

The total count reflects the current `.io`/`.iox` set under `testdata/` (160 files: main baseline plus three sei deprecation `.iox`, including batch regression).
The total count reflects the current `.io`/`.iox` set under `testdata/` (156 files: main baseline plus three sei deprecation `.iox`, including batch regression).

## What is checked

Expand Down Expand Up @@ -214,9 +214,7 @@ So "seed" = a known-good block (and deploy tx) that the script creates and the r
| eth_getLogs | no-topics.io | Eth exec api |
| eth_getLogs | topic-exact-match.io | Eth exec api |
| eth_getLogs | topic-wildcard.io | Eth exec api |
| eth_getProof | get-account-proof-blockhash.iox | Sei |
| eth_getProof | get-account-proof-latest.iox | Sei |
| eth_getProof | get-account-proof-with-storage.iox | Sei |
| eth_getProof | not-supported.iox | Sei |
| eth_getStorageAt | get-storage-invalid-key-too-large.io | Eth exec api |
| eth_getStorageAt | get-storage-invalid-key.io | Eth exec api |
| eth_getStorageAt | get-storage-unknown-account.io | Eth exec api |
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// eth_getProof is not supported yet on Sei EVM RPC (-32000).
>> {"jsonrpc":"2.0","id":1,"method":"eth_getProof","params":["0x1234567890123456789012345678901234567890",[],"latest"]}
<< {"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"eth_getProof is not supported yet on Sei EVM RPC; please reach out to the Sei team if you need this endpoint"}}
Loading