diff --git a/docs/evm_jsonrpc_unsupported.md b/docs/evm_jsonrpc_unsupported.md index a005f80fdb..a9d48083fc 100644 --- a/docs/evm_jsonrpc_unsupported.md +++ b/docs/evm_jsonrpc_unsupported.md @@ -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` | @@ -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//`. diff --git a/evmrpc/AGENTS.md b/evmrpc/AGENTS.md index b725e8d783..497d17b1da 100644 --- a/evmrpc/AGENTS.md +++ b/evmrpc/AGENTS.md @@ -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. diff --git a/evmrpc/height_availability_test.go b/evmrpc/height_availability_test.go index 17c98fc9cd..f97ba91fab 100644 --- a/evmrpc/height_availability_test.go +++ b/evmrpc/height_availability_test.go @@ -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") -} diff --git a/evmrpc/state.go b/evmrpc/state.go index f379d5352c..b725658432 100644 --- a/evmrpc/state.go +++ b/evmrpc/state.go @@ -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 @@ -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) }() - 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 { diff --git a/evmrpc/state_test.go b/evmrpc/state_test.go index e53729cb04..cc9d4ac8c8 100644 --- a/evmrpc/state_test.go +++ b/evmrpc/state_test.go @@ -1,7 +1,6 @@ package evmrpc_test import ( - "context" "encoding/hex" "encoding/json" "fmt" @@ -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" ) @@ -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") } diff --git a/integration_test/evm_module/rpc_io_test/RPC_IO_README.md b/integration_test/evm_module/rpc_io_test/RPC_IO_README.md index 5abb1eb847..2568453a2b 100644 --- a/integration_test/evm_module/rpc_io_test/RPC_IO_README.md +++ b/integration_test/evm_module/rpc_io_test/RPC_IO_README.md @@ -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). @@ -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 @@ -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 | diff --git a/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/get-account-proof-blockhash.iox b/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/get-account-proof-blockhash.iox deleted file mode 100644 index fdc12c1172..0000000000 --- a/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/get-account-proof-blockhash.iox +++ /dev/null @@ -1,12 +0,0 @@ -// Data-dependent: bind block hash from latest and contract from deploy, then getProof(contractAddress, [], blockHash). Run evm_rpc_tests.sh when local. ->> {"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["__SEED__",false]} -<< {"jsonrpc":"2.0","id":1,"result":{}} -@ bind blockHash = result.hash ->> {"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["__SEED__",true]} -<< {"jsonrpc":"2.0","id":1,"result":{}} -@ bind deployTxHash = result.transactions.1.hash ->> {"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["${deployTxHash}"]} -<< {"jsonrpc":"2.0","id":1,"result":{}} -@ bind contractAddress = result.contractAddress ->> {"jsonrpc":"2.0","id":1,"method":"eth_getProof","params":["${contractAddress}",[],"${blockHash}"]} -<< {"jsonrpc":"2.0","id":1,"result":{}} diff --git a/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/get-account-proof-latest.iox b/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/get-account-proof-latest.iox deleted file mode 100644 index 5458fb4865..0000000000 --- a/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/get-account-proof-latest.iox +++ /dev/null @@ -1,9 +0,0 @@ -// Data-dependent: getProof for an account that exists. Bind contractAddress from deploy, then getProof(contractAddress, [], "__SEED__"). Run evm_rpc_tests.sh when local. ->> {"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["__SEED__",true]} -<< {"jsonrpc":"2.0","id":1,"result":{}} -@ bind deployTxHash = result.transactions.1.hash ->> {"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["${deployTxHash}"]} -<< {"jsonrpc":"2.0","id":1,"result":{}} -@ bind contractAddress = result.contractAddress ->> {"jsonrpc":"2.0","id":1,"method":"eth_getProof","params":["${contractAddress}",[],"__SEED__"]} -<< {"jsonrpc":"2.0","id":1,"result":{}} diff --git a/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/get-account-proof-with-storage.iox b/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/get-account-proof-with-storage.iox deleted file mode 100644 index ae42f2bf70..0000000000 --- a/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/get-account-proof-with-storage.iox +++ /dev/null @@ -1,9 +0,0 @@ -// Data-dependent: bind contractAddress from deploy, then getProof(contractAddress, ["0x00"], "__SEED__"). Run evm_rpc_tests.sh when local. ->> {"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["__SEED__",true]} -<< {"jsonrpc":"2.0","id":1,"result":{}} -@ bind deployTxHash = result.transactions.1.hash ->> {"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["${deployTxHash}"]} -<< {"jsonrpc":"2.0","id":1,"result":{}} -@ bind contractAddress = result.contractAddress ->> {"jsonrpc":"2.0","id":1,"method":"eth_getProof","params":["${contractAddress}",["0x00"],"__SEED__"]} -<< {"jsonrpc":"2.0","id":1,"result":{}} diff --git a/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/not-supported.iox b/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/not-supported.iox new file mode 100644 index 0000000000..bd9b21e605 --- /dev/null +++ b/integration_test/evm_module/rpc_io_test/testdata/eth_getProof/not-supported.iox @@ -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"}}