diff --git a/evmrpc/tracers.go b/evmrpc/tracers.go index f7bde8bf42..9af8ab1023 100644 --- a/evmrpc/tracers.go +++ b/evmrpc/tracers.go @@ -139,6 +139,20 @@ func (api *DebugAPI) TraceTransaction(ctx context.Context, hash common.Hash, con ctx, cancel := context.WithTimeout(ctx, api.traceTimeout) defer cancel() + // Validate transaction access by getting its block number + found, _, _, blockNumber, _, err := api.backend.GetTransaction(ctx, hash) + if err != nil { + return nil, fmt.Errorf("failed to get transaction: %w", err) + } + if !found { + return nil, fmt.Errorf("transaction not found") + } + + params := api.getBlockValidationParams() + if err := ValidateBlockAccess(int64(blockNumber), params); err != nil { + return nil, err + } + startTime := time.Now() defer recordMetricsWithError("debug_traceTransaction", api.connectionType, startTime, returnErr) result, returnErr = api.tracersAPI.TraceTransaction(ctx, hash, config) @@ -152,9 +166,10 @@ func (api *SeiDebugAPI) TraceBlockByNumberExcludeTraceFail(ctx context.Context, ctx, cancel := context.WithTimeout(ctx, api.traceTimeout) defer cancel() - latest := api.ctxProvider(LatestCtxHeight).BlockHeight() - if api.maxBlockLookback >= 0 && number.Int64() < latest-api.maxBlockLookback { - return nil, fmt.Errorf("block number %d is beyond max lookback of %d", number.Int64(), api.maxBlockLookback) + // Validate block number access + params := api.getBlockValidationParams() + if err := ValidateBlockNumberAccess(number, params); err != nil { + return nil, err } startTime := time.Now() @@ -185,6 +200,12 @@ func (api *SeiDebugAPI) TraceBlockByHashExcludeTraceFail(ctx context.Context, ha ctx, cancel := context.WithTimeout(ctx, api.traceTimeout) defer cancel() + // Validate block hash access + params := api.getBlockValidationParams() + if err := ValidateBlockHashAccess(ctx, api.tmClient, hash, params); err != nil { + return nil, err + } + startTime := time.Now() defer recordMetricsWithError("sei_traceBlockByHashExcludeTraceFail", api.connectionType, startTime, returnErr) // Accessing tracersAPI from the embedded DebugAPI @@ -266,9 +287,10 @@ func (api *DebugAPI) TraceBlockByNumber(ctx context.Context, number rpc.BlockNum ctx, cancel := context.WithTimeout(ctx, api.traceTimeout) defer cancel() - latest := api.ctxProvider(LatestCtxHeight).BlockHeight() - if api.maxBlockLookback >= 0 && number.Int64() < latest-api.maxBlockLookback { - return nil, fmt.Errorf("block number %d is beyond max lookback of %d", number.Int64(), api.maxBlockLookback) + // Validate block number access + params := api.getBlockValidationParams() + if err := ValidateBlockNumberAccess(number, params); err != nil { + return nil, err } startTime := time.Now() @@ -284,6 +306,12 @@ func (api *DebugAPI) TraceBlockByHash(ctx context.Context, hash common.Hash, con ctx, cancel := context.WithTimeout(ctx, api.traceTimeout) defer cancel() + // Validate block hash access + params := api.getBlockValidationParams() + if err := ValidateBlockHashAccess(ctx, api.tmClient, hash, params); err != nil { + return nil, err + } + startTime := time.Now() defer recordMetricsWithError("debug_traceBlockByHash", api.connectionType, startTime, returnErr) result, returnErr = api.tracersAPI.TraceBlockByHash(ctx, hash, config) @@ -348,3 +376,19 @@ func (api *DebugAPI) TraceStateAccess(ctx context.Context, hash common.Hash) (re } return response, nil } + +// checkTraceParams checks if the app state is available for the given block number. +// This is a convenience wrapper around ValidateBlockNumberAccess for backward compatibility. +func (api *DebugAPI) checkTraceParams(number rpc.BlockNumber) error { + params := api.getBlockValidationParams() + return ValidateBlockNumberAccess(number, params) +} + +// getBlockValidationParams creates the validation parameters needed for block access checks +func (api *DebugAPI) getBlockValidationParams() BlockValidationParams { + return BlockValidationParams{ + LatestHeight: api.ctxProvider(LatestCtxHeight).BlockHeight(), + MaxBlockLookback: api.maxBlockLookback, + EarliestVersion: api.backend.app.CommitMultiStore().GetEarliestVersion(), + } +} diff --git a/evmrpc/tracers_test.go b/evmrpc/tracers_test.go index c1dc3cdcfb..ca430092f3 100644 --- a/evmrpc/tracers_test.go +++ b/evmrpc/tracers_test.go @@ -2,6 +2,7 @@ package evmrpc_test import ( "fmt" + "strings" "testing" testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" @@ -96,3 +97,61 @@ func TestTraceBlockByNumberUnlimitedLookback(t *testing.T) { _, ok = resObj["result"] require.True(t, ok, "expected result to be present") } + +func TestTraceTransactionValidation(t *testing.T) { + // Test tracing a transaction that exists + args := map[string]interface{}{"tracer": "callTracer"} + resObj := sendRequestGoodWithNamespace(t, "debug", "traceTransaction", DebugTraceHashHex, args) + + // Should have a result, not an error + _, hasResult := resObj["result"] + _, hasError := resObj["error"] + require.True(t, hasResult, "expected successful trace result") + require.False(t, hasError, "expected no error for valid transaction") +} + +func TestTraceTransactionNotFound(t *testing.T) { + // Test tracing a non-existent transaction hash + nonExistentTxHash := "0x1111111111111111111111111111111111111111111111111111111111111111" + args := map[string]interface{}{"tracer": "callTracer"} + + resObj := sendRequestGoodWithNamespace(t, "debug", "traceTransaction", nonExistentTxHash, args) + + // Should have an error + errObj, hasError := resObj["error"].(map[string]interface{}) + require.True(t, hasError, "expected error for non-existent transaction") + + // Check that the error message indicates transaction not found + message, hasMessage := errObj["message"].(string) + require.True(t, hasMessage, "expected error message") + require.Contains(t, message, "failed to get transaction", "expected transaction not found error") +} + +func TestTraceBlockByHashValidation(t *testing.T) { + // Test with the existing debug trace block hash + args := map[string]interface{}{"tracer": "callTracer"} + resObj := sendRequestGoodWithNamespace(t, "debug", "traceBlockByHash", DebugTraceBlockHash, args) + + // Should have a result, not an error + _, hasResult := resObj["result"] + _, hasError := resObj["error"] + require.True(t, hasResult, "expected successful trace result") + require.False(t, hasError, "expected no error for valid block hash") +} + +func TestTraceBlockByHashWithStrictLimits(t *testing.T) { + // Test with strict server that has limited lookback + args := map[string]interface{}{"tracer": "callTracer"} + + // Use a block hash that would be beyond the lookback limit + resObj := sendRequestStrictWithNamespace(t, "debug", "traceBlockByHash", DebugTraceBlockHash, args) + + // This might result in an error depending on the block height and lookback settings + if errObj, hasError := resObj["error"].(map[string]interface{}); hasError { + message := errObj["message"].(string) + // Should be a lookback-related error + require.True(t, + strings.Contains(message, "beyond max lookback") || strings.Contains(message, "height not available"), + "expected lookback or availability error, got: %s", message) + } +} diff --git a/evmrpc/utils.go b/evmrpc/utils.go index eaccd9cc9d..9dacfd51b4 100644 --- a/evmrpc/utils.go +++ b/evmrpc/utils.go @@ -280,3 +280,48 @@ func recoverAndLog() { debug.PrintStack() } } + +// BlockValidationParams contains the parameters needed to validate block availability +type BlockValidationParams struct { + LatestHeight int64 + MaxBlockLookback int64 + EarliestVersion int64 +} + +// ValidateBlockAccess validates that a block is accessible based on lookback and retention policies. +// It checks both Tendermint block retention (min-retain-blocks) and app state retention (ss-keep-recent). +func ValidateBlockAccess(blockNumber int64, params BlockValidationParams) error { + // Check Tendermint block retention (min-retain-blocks) + if params.MaxBlockLookback >= 0 && blockNumber < params.LatestHeight-params.MaxBlockLookback { + return fmt.Errorf("block number %d is beyond max lookback of %d", blockNumber, params.MaxBlockLookback) + } + + // Check app state retention (ss-keep-recent and related state sync settings) + if params.EarliestVersion > blockNumber { + return fmt.Errorf("height not available (requested height: %d, base height: %d)", blockNumber, params.EarliestVersion) + } + + return nil +} + +// ValidateBlockNumberAccess validates access to a block by block number. +func ValidateBlockNumberAccess(number rpc.BlockNumber, params BlockValidationParams) error { + // Special block numbers are always allowed + if number == rpc.LatestBlockNumber || number == rpc.FinalizedBlockNumber { + return nil + } + + blockNumber := number.Int64() + return ValidateBlockAccess(blockNumber, params) +} + +// ValidateBlockHashAccess validates access to a block by block hash. +// It first converts the hash to a block number, then validates access. +func ValidateBlockHashAccess(ctx context.Context, tmClient rpcclient.Client, hash common.Hash, params BlockValidationParams) error { + block, err := blockByHash(ctx, tmClient, hash.Bytes()) + if err != nil { + return fmt.Errorf("failed to get block by hash: %w", err) + } + + return ValidateBlockAccess(block.Block.Height, params) +} diff --git a/evmrpc/utils_test.go b/evmrpc/utils_test.go index 60ed743bfe..3e1f4115b7 100644 --- a/evmrpc/utils_test.go +++ b/evmrpc/utils_test.go @@ -4,6 +4,8 @@ import ( "context" "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" "github.com/stretchr/testify/require" @@ -27,3 +29,133 @@ func TestParallelRunnerPanicRecovery(t *testing.T) { close(r.Queue) require.NotPanics(t, r.Done.Wait) } + +func TestValidateBlockAccess(t *testing.T) { + tests := []struct { + name string + blockNumber int64 + params evmrpc.BlockValidationParams + expectError bool + errorMsg string + }{ + { + name: "valid block within lookback", + blockNumber: 95, + params: evmrpc.BlockValidationParams{ + LatestHeight: 100, + MaxBlockLookback: 10, + EarliestVersion: 90, + }, + expectError: false, + }, + { + name: "block beyond max lookback", + blockNumber: 80, + params: evmrpc.BlockValidationParams{ + LatestHeight: 100, + MaxBlockLookback: 10, + EarliestVersion: 70, + }, + expectError: true, + errorMsg: "beyond max lookback", + }, + { + name: "block before earliest version", + blockNumber: 50, + params: evmrpc.BlockValidationParams{ + LatestHeight: 100, + MaxBlockLookback: 50, + EarliestVersion: 60, + }, + expectError: true, + errorMsg: "height not available", + }, + { + name: "unlimited lookback (negative value)", + blockNumber: 1, + params: evmrpc.BlockValidationParams{ + LatestHeight: 100, + MaxBlockLookback: -1, + EarliestVersion: 1, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := evmrpc.ValidateBlockAccess(tt.blockNumber, tt.params) + if tt.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMsg) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateBlockNumberAccess(t *testing.T) { + params := evmrpc.BlockValidationParams{ + LatestHeight: 100, + MaxBlockLookback: 10, + EarliestVersion: 80, + } + + tests := []struct { + name string + blockNumber rpc.BlockNumber + expectError bool + }{ + { + name: "latest block number", + blockNumber: rpc.LatestBlockNumber, + expectError: false, + }, + { + name: "finalized block number", + blockNumber: rpc.FinalizedBlockNumber, + expectError: false, + }, + { + name: "valid specific block number", + blockNumber: rpc.BlockNumber(95), + expectError: false, + }, + { + name: "block number beyond lookback", + blockNumber: rpc.BlockNumber(80), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := evmrpc.ValidateBlockNumberAccess(tt.blockNumber, params) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateBlockHashAccess(t *testing.T) { + params := evmrpc.BlockValidationParams{ + LatestHeight: MockHeight8, + MaxBlockLookback: 10, + EarliestVersion: 1, + } + + // Test with a valid hash that maps to a valid block + validHash := common.HexToHash(TestBlockHash) + err := evmrpc.ValidateBlockHashAccess(context.Background(), &MockClient{}, validHash, params) + require.NoError(t, err) + + // Test with an invalid hash + invalidHash := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000999") + err = evmrpc.ValidateBlockHashAccess(context.Background(), &MockBadClient{}, invalidHash, params) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to get block by hash") +}