Skip to content
Merged
16 changes: 16 additions & 0 deletions evmrpc/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"math/big"
"strings"
Expand Down Expand Up @@ -195,10 +196,18 @@ 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
}
if blockHash == genesisBlockHash {
return encodeGenesisBlock(), 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
}
Expand Down Expand Up @@ -260,8 +269,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
}
Expand Down
69 changes: 69 additions & 0 deletions evmrpc/height_availability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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 {
Expand Down Expand Up @@ -100,6 +113,62 @@ 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)
}

// 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)
}

// TestGetBlockTransactionCountByHashGenesis verifies that the genesis block hash returned by
// eth_getBlockByNumber("0x0") is accepted by eth_getBlockTransactionCountByHash (consistency).
func TestGetBlockTransactionCountByHashGenesis(t *testing.T) {
Expand Down
7 changes: 6 additions & 1 deletion evmrpc/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/ecdsa"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"math/big"
"runtime/debug"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading