From e4f1ad1f2b61c2907eaf5dce9adf74b3340f1840 Mon Sep 17 00:00:00 2001 From: Mojtaba Date: Fri, 13 Mar 2026 12:01:58 +0000 Subject: [PATCH 1/3] chore(evmrpc): return null for unknown/empty block hash in eth_getBlockByHash --- evmrpc/block.go | 8 ++++++ evmrpc/height_availability_test.go | 41 ++++++++++++++++++++++++++++++ evmrpc/utils.go | 7 ++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/evmrpc/block.go b/evmrpc/block.go index e896bbe622..745d29edbb 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "errors" "fmt" "math/big" "strings" @@ -158,7 +159,14 @@ func (a *BlockAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fu func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool, includeSyntheticTxs bool, isPanicTx func(ctx context.Context, hash common.Hash) (bool, error)) (result map[string]interface{}, returnErr error) { startTime := time.Now() defer recordMetricsWithError(fmt.Sprintf("%s_getBlockByHash", a.namespace), a.connectionType, startTime, returnErr) + // Ethereum spec: empty or non-existent block hash returns result=null, not error. + if blockHash == (common.Hash{}) { + return nil, nil + } block, err := blockByHashRespectingWatermarks(ctx, a.tmClient, a.watermarks, blockHash[:], 1) + if errors.Is(err, ErrBlockNotFoundByHash) { + return nil, nil + } if err != nil { return nil, err } diff --git a/evmrpc/height_availability_test.go b/evmrpc/height_availability_test.go index e8f2d2ec2f..311d41e42c 100644 --- a/evmrpc/height_availability_test.go +++ b/evmrpc/height_availability_test.go @@ -72,6 +72,19 @@ func (c *heightTestClient) Status(context.Context) (*coretypes.ResultStatus, err }, nil } +// blockNotFoundTestClient returns ResultBlock{Block: nil} for a specific hash to simulate Tendermint "block not found". +type blockNotFoundTestClient struct { + *heightTestClient + notFoundHash bytes.HexBytes +} + +func (c *blockNotFoundTestClient) BlockByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultBlock, error) { + if hash.String() == c.notFoundHash.String() { + return &coretypes.ResultBlock{Block: nil}, nil + } + return c.heightTestClient.BlockByHash(ctx, hash) +} + func mustDecodeHex(h string) []byte { bz, err := hex.DecodeString(h) if err != nil { @@ -99,6 +112,34 @@ func TestBlockAPIEnsureHeightUnavailable(t *testing.T) { require.Contains(t, err.Error(), "requested height") } +// TestGetBlockByHashNotFoundReturnsNull verifies Ethereum-compatible behavior: empty or non-existent block hash +// returns (nil, nil) so RPC responds with result: null, not an error (see get-block-by-empty-hash.iox, get-block-by-notfound-hash.iox). +func TestGetBlockByHashNotFoundReturnsNull(t *testing.T) { + t.Parallel() + + earliest := int64(1) + latest := int64(100) + base := newHeightTestClient(latest+5, earliest, latest) + notFoundHashHex := "0x00000000000000000000000000000000000000000000000000000000deadbeef" + client := &blockNotFoundTestClient{ + heightTestClient: base, + notFoundHash: bytes.HexBytes(mustDecodeHex(notFoundHashHex[2:])), + } + watermarks := NewWatermarkManager(client, testCtxProvider, nil, nil) + api := NewBlockAPI(client, nil, testCtxProvider, testTxConfigProvider, ConnectionTypeHTTP, watermarks, nil, nil) + ctx := context.Background() + + // Empty hash: short-circuit, result null + result, err := api.GetBlockByHash(ctx, common.Hash{}, false) + require.NoError(t, err) + require.Nil(t, result) + + // Non-existent hash (client returns Block: nil): result null + result, err = api.GetBlockByHash(ctx, common.HexToHash(notFoundHashHex), false) + require.NoError(t, err) + require.Nil(t, result) +} + func TestLogFetcherSkipsUnavailableCachedBlock(t *testing.T) { t.Parallel() diff --git a/evmrpc/utils.go b/evmrpc/utils.go index 25042e763a..a6bc0dd8a4 100644 --- a/evmrpc/utils.go +++ b/evmrpc/utils.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/sha256" "encoding/hex" + "errors" "fmt" "math/big" "runtime/debug" @@ -41,6 +42,10 @@ const LatestCtxHeight int64 = -1 // EVM launch block heights for different chains const Pacific1EVMLaunchHeight int64 = 79123881 +// ErrBlockNotFoundByHash is returned when no block exists for the given hash (e.g. empty or unknown hash). +// Ethereum-compatible RPCs should return result: null for this case instead of an error. +var ErrBlockNotFoundByHash = errors.New("block not found by hash") + // GetBlockNumberByNrOrHash returns the height of the block with the given number or hash. func GetBlockNumberByNrOrHash(ctx context.Context, tmClient rpcclient.Client, wm *WatermarkManager, blockNrOrHash rpc.BlockNumberOrHash) (*int64, error) { if blockNrOrHash.BlockHash != nil { @@ -186,7 +191,7 @@ func blockByHashWithRetry(ctx context.Context, client rpcclient.Client, hash byt return nil, err } if blockRes.Block == nil { - return nil, fmt.Errorf("could not find block for hash %s", hash.String()) + return nil, ErrBlockNotFoundByHash } TraceTendermintIfApplicable(ctx, "BlockByHash", []string{hash.String()}, blockRes) return blockRes, err From 2dd7b8542e0094730e2f52782fe3b2c9a1087474 Mon Sep 17 00:00:00 2001 From: Moji Date: Mon, 16 Mar 2026 10:05:26 +0330 Subject: [PATCH 2/3] chore(evmrpc): eth_getBlockReceipts not-found behavior parity (#3068) ## Describe your changes and provide context Closes PLT-163 Ref [PLT-163](https://linear.app/seilabs/issue/PLT-163/fix-eth-getblockreceipts-not-found-behavior-parity-null-expected) ## Testing performed to validate your change ``` === RUN TestEVMRPCSpec/eth_getBlockReceipts/get-block-receipts-empty.iox rpc_io_test.go:79: [DEBUG] SEI_EVM_IO_SEED_BLOCK="" rpc_io_test.go:114: [DEBUG] pair 1: placeholders=[] bindings=map[] rpc_io_test.go:129: [DEBUG] pair 1: request {"jsonrpc":"2.0","id":1,"method":"eth_getBlockReceipts","params":["0x0000000000000000000000000000000000000000000000000000000000000000"]} rpc_io_test.go:144: [DEBUG] pair 1: bindings after apply: map[] === RUN TestEVMRPCSpec/eth_getBlockReceipts/get-block-receipts-not-found.iox rpc_io_test.go:79: [DEBUG] SEI_EVM_IO_SEED_BLOCK="" rpc_io_test.go:114: [DEBUG] pair 1: placeholders=[] bindings=map[] rpc_io_test.go:129: [DEBUG] pair 1: request {"jsonrpc":"2.0","id":1,"method":"eth_getBlockReceipts","params":["0x00000000000000000000000000000000000000000000000000000000deadbeef"]} rpc_io_test.go:144: [DEBUG] pair 1: bindings after apply: map[] --- PASS: TestEVMRPCSpec (0.00s) --- PASS: TestEVMRPCSpec/eth_getBlockReceipts/get-block-receipts-empty.iox (0.00s) --- PASS: TestEVMRPCSpec/eth_getBlockReceipts/get-block-receipts-not-found.iox (0.00s) ``` Co-authored-by: Mojtaba --- evmrpc/block.go | 7 +++++++ evmrpc/height_availability_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/evmrpc/block.go b/evmrpc/block.go index 745d29edbb..afa2dc2fab 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -249,8 +249,15 @@ func (a *BlockAPI) getBlockByNumber( func (a *BlockAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (result []map[string]interface{}, returnErr error) { startTime := time.Now() defer recordMetricsWithError(fmt.Sprintf("%s_getBlockReceipts", a.namespace), a.connectionType, startTime, returnErr) + // Ethereum spec: empty or non-existent block hash returns result=null, not error. + if blockNrOrHash.BlockHash != nil && *blockNrOrHash.BlockHash == (common.Hash{}) { + return nil, nil + } // Get height from params heightPtr, err := GetBlockNumberByNrOrHash(ctx, a.tmClient, a.watermarks, blockNrOrHash) + if errors.Is(err, ErrBlockNotFoundByHash) { + return nil, nil + } if err != nil { return nil, err } diff --git a/evmrpc/height_availability_test.go b/evmrpc/height_availability_test.go index 311d41e42c..114894df8d 100644 --- a/evmrpc/height_availability_test.go +++ b/evmrpc/height_availability_test.go @@ -140,6 +140,34 @@ func TestGetBlockByHashNotFoundReturnsNull(t *testing.T) { require.Nil(t, result) } +// TestGetBlockReceiptsNotFoundReturnsNull verifies Ethereum-compatible behavior: empty or non-existent block hash +// returns (nil, nil) so RPC responds with result: null (see get-block-receipts-empty.iox, get-block-receipts-not-found.iox). +func TestGetBlockReceiptsNotFoundReturnsNull(t *testing.T) { + t.Parallel() + + earliest := int64(1) + latest := int64(100) + base := newHeightTestClient(latest+5, earliest, latest) + notFoundHashHex := "0x00000000000000000000000000000000000000000000000000000000deadbeef" + client := &blockNotFoundTestClient{ + heightTestClient: base, + notFoundHash: bytes.HexBytes(mustDecodeHex(notFoundHashHex[2:])), + } + watermarks := NewWatermarkManager(client, testCtxProvider, nil, nil) + api := NewBlockAPI(client, nil, testCtxProvider, testTxConfigProvider, ConnectionTypeHTTP, watermarks, nil, nil) + ctx := context.Background() + + // Empty hash: short-circuit, result null + receipts, err := api.GetBlockReceipts(ctx, rpc.BlockNumberOrHashWithHash(common.Hash{}, true)) + require.NoError(t, err) + require.Nil(t, receipts) + + // Non-existent hash (client returns Block: nil): result null + receipts, err = api.GetBlockReceipts(ctx, rpc.BlockNumberOrHashWithHash(common.HexToHash(notFoundHashHex), true)) + require.NoError(t, err) + require.Nil(t, receipts) +} + func TestLogFetcherSkipsUnavailableCachedBlock(t *testing.T) { t.Parallel() From e6931411a2733067245043222971f90325783e15 Mon Sep 17 00:00:00 2001 From: Mojtaba Date: Tue, 17 Mar 2026 17:44:18 +0000 Subject: [PATCH 3/3] chore: gofmted --- evmrpc/block.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evmrpc/block.go b/evmrpc/block.go index 6441f2dd74..f052542911 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -200,7 +200,7 @@ func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fu // Ethereum spec: empty or non-existent block hash returns result=null, not error. if blockHash == (common.Hash{}) { return nil, nil - } + } if blockHash == genesisBlockHash { return encodeGenesisBlock(), nil }