From 9a1eb2436ccb74ecae96c40b4978ee0abdb49d02 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Tue, 26 Aug 2025 17:16:03 -0400 Subject: [PATCH 1/6] Fix for latest block --- evmrpc/tracers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evmrpc/tracers.go b/evmrpc/tracers.go index f7bde8bf42..dd007cb462 100644 --- a/evmrpc/tracers.go +++ b/evmrpc/tracers.go @@ -153,7 +153,7 @@ func (api *SeiDebugAPI) TraceBlockByNumberExcludeTraceFail(ctx context.Context, defer cancel() latest := api.ctxProvider(LatestCtxHeight).BlockHeight() - if api.maxBlockLookback >= 0 && number.Int64() < latest-api.maxBlockLookback { + if api.maxBlockLookback >= 0 && number != rpc.LatestBlockNumber && number != rpc.FinalizedBlockNumber && number.Int64() < latest-api.maxBlockLookback { return nil, fmt.Errorf("block number %d is beyond max lookback of %d", number.Int64(), api.maxBlockLookback) } @@ -267,7 +267,7 @@ func (api *DebugAPI) TraceBlockByNumber(ctx context.Context, number rpc.BlockNum defer cancel() latest := api.ctxProvider(LatestCtxHeight).BlockHeight() - if api.maxBlockLookback >= 0 && number.Int64() < latest-api.maxBlockLookback { + if api.maxBlockLookback >= 0 && number != rpc.LatestBlockNumber && number != rpc.FinalizedBlockNumber && number.Int64() < latest-api.maxBlockLookback { return nil, fmt.Errorf("block number %d is beyond max lookback of %d", number.Int64(), api.maxBlockLookback) } From 685e2ddcbcea66ebe3cb4c532365c3b30bae727d Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Tue, 26 Aug 2025 18:33:07 -0400 Subject: [PATCH 2/6] State earliest version --- evmrpc/tracers.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/evmrpc/tracers.go b/evmrpc/tracers.go index dd007cb462..5e4e3d2289 100644 --- a/evmrpc/tracers.go +++ b/evmrpc/tracers.go @@ -157,6 +157,12 @@ func (api *SeiDebugAPI) TraceBlockByNumberExcludeTraceFail(ctx context.Context, return nil, fmt.Errorf("block number %d is beyond max lookback of %d", number.Int64(), api.maxBlockLookback) } + // Verify app state contains history. + earliestVersion := api.backend.app.CommitMultiStore().GetEarliestVersion() + if earliestVersion > number.Int64() { + return nil, fmt.Errorf("height not available (requested height: %d, base height: %d)", number.Int64(), earliestVersion) + } + startTime := time.Now() defer recordMetricsWithError("sei_traceBlockByNumberExcludeTraceFail", api.connectionType, startTime, returnErr) // Accessing tracersAPI from the embedded DebugAPI @@ -271,6 +277,12 @@ func (api *DebugAPI) TraceBlockByNumber(ctx context.Context, number rpc.BlockNum return nil, fmt.Errorf("block number %d is beyond max lookback of %d", number.Int64(), api.maxBlockLookback) } + // Verify app state contains history. + earliestVersion := api.backend.app.CommitMultiStore().GetEarliestVersion() + if earliestVersion > number.Int64() { + return nil, fmt.Errorf("height not available (requested height: %d, base height: %d)", number.Int64(), earliestVersion) + } + startTime := time.Now() defer recordMetricsWithError("debug_traceBlockByNumber", api.connectionType, startTime, returnErr) result, returnErr = api.tracersAPI.TraceBlockByNumber(ctx, number, config) From 14fdcd6c3c647eb223cf968893844df6bc6bcfff Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Tue, 26 Aug 2025 19:16:21 -0400 Subject: [PATCH 3/6] Clean up block param check --- evmrpc/tracers.go | 60 +++++++++++++++++++++++++++++++++-------------- evmrpc/utils.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 18 deletions(-) diff --git a/evmrpc/tracers.go b/evmrpc/tracers.go index 5e4e3d2289..327331b7e4 100644 --- a/evmrpc/tracers.go +++ b/evmrpc/tracers.go @@ -139,6 +139,12 @@ func (api *DebugAPI) TraceTransaction(ctx context.Context, hash common.Hash, con ctx, cancel := context.WithTimeout(ctx, api.traceTimeout) defer cancel() + // Validate transaction access + params := api.getBlockValidationParams() + if err := ValidateTransactionAccess(ctx, api.tmClient, hash, api.keeper, api.ctxProvider, 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,15 +158,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 != rpc.LatestBlockNumber && number != rpc.FinalizedBlockNumber && number.Int64() < latest-api.maxBlockLookback { - return nil, fmt.Errorf("block number %d is beyond max lookback of %d", number.Int64(), api.maxBlockLookback) - } - - // Verify app state contains history. - earliestVersion := api.backend.app.CommitMultiStore().GetEarliestVersion() - if earliestVersion > number.Int64() { - return nil, fmt.Errorf("height not available (requested height: %d, base height: %d)", number.Int64(), earliestVersion) + // Validate block number access + params := api.getBlockValidationParams() + if err := ValidateBlockNumberAccess(ctx, api.tmClient, number, params); err != nil { + return nil, err } startTime := time.Now() @@ -191,6 +192,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 @@ -272,15 +279,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 != rpc.LatestBlockNumber && number != rpc.FinalizedBlockNumber && number.Int64() < latest-api.maxBlockLookback { - return nil, fmt.Errorf("block number %d is beyond max lookback of %d", number.Int64(), api.maxBlockLookback) - } - - // Verify app state contains history. - earliestVersion := api.backend.app.CommitMultiStore().GetEarliestVersion() - if earliestVersion > number.Int64() { - return nil, fmt.Errorf("height not available (requested height: %d, base height: %d)", number.Int64(), earliestVersion) + // Validate block number access + params := api.getBlockValidationParams() + if err := ValidateBlockNumberAccess(ctx, api.tmClient, number, params); err != nil { + return nil, err } startTime := time.Now() @@ -296,6 +298,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) @@ -360,3 +368,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(context.Background(), api.tmClient, 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/utils.go b/evmrpc/utils.go index eaccd9cc9d..e9367b900f 100644 --- a/evmrpc/utils.go +++ b/evmrpc/utils.go @@ -280,3 +280,62 @@ 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(ctx context.Context, tmClient rpcclient.Client, 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) +} + +// ValidateTransactionAccess validates access to a transaction by converting it to its block number first. +// This function requires additional parameters to look up the transaction. +func ValidateTransactionAccess(ctx context.Context, tmClient rpcclient.Client, txHash common.Hash, keeper *keeper.Keeper, ctxProvider func(int64) sdk.Context, params BlockValidationParams) error { + // Get the transaction receipt to find the block number + sdkCtx := ctxProvider(LatestCtxHeight) + receipt, err := keeper.GetReceipt(sdkCtx, txHash) + if err != nil { + return fmt.Errorf("failed to get transaction receipt: %w", err) + } + + blockNumber := int64(receipt.BlockNumber) + return ValidateBlockAccess(blockNumber, params) +} From 93a11bebff4e17ccbbcce5044de8578ca85c195e Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Tue, 26 Aug 2025 19:31:25 -0400 Subject: [PATCH 4/6] remove tmclient references --- evmrpc/tracers.go | 18 +++++++++++++----- evmrpc/utils.go | 16 +--------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/evmrpc/tracers.go b/evmrpc/tracers.go index 327331b7e4..9af8ab1023 100644 --- a/evmrpc/tracers.go +++ b/evmrpc/tracers.go @@ -139,9 +139,17 @@ func (api *DebugAPI) TraceTransaction(ctx context.Context, hash common.Hash, con ctx, cancel := context.WithTimeout(ctx, api.traceTimeout) defer cancel() - // Validate transaction access + // 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 := ValidateTransactionAccess(ctx, api.tmClient, hash, api.keeper, api.ctxProvider, params); err != nil { + if err := ValidateBlockAccess(int64(blockNumber), params); err != nil { return nil, err } @@ -160,7 +168,7 @@ func (api *SeiDebugAPI) TraceBlockByNumberExcludeTraceFail(ctx context.Context, // Validate block number access params := api.getBlockValidationParams() - if err := ValidateBlockNumberAccess(ctx, api.tmClient, number, params); err != nil { + if err := ValidateBlockNumberAccess(number, params); err != nil { return nil, err } @@ -281,7 +289,7 @@ func (api *DebugAPI) TraceBlockByNumber(ctx context.Context, number rpc.BlockNum // Validate block number access params := api.getBlockValidationParams() - if err := ValidateBlockNumberAccess(ctx, api.tmClient, number, params); err != nil { + if err := ValidateBlockNumberAccess(number, params); err != nil { return nil, err } @@ -373,7 +381,7 @@ func (api *DebugAPI) TraceStateAccess(ctx context.Context, hash common.Hash) (re // This is a convenience wrapper around ValidateBlockNumberAccess for backward compatibility. func (api *DebugAPI) checkTraceParams(number rpc.BlockNumber) error { params := api.getBlockValidationParams() - return ValidateBlockNumberAccess(context.Background(), api.tmClient, number, params) + return ValidateBlockNumberAccess(number, params) } // getBlockValidationParams creates the validation parameters needed for block access checks diff --git a/evmrpc/utils.go b/evmrpc/utils.go index e9367b900f..9dacfd51b4 100644 --- a/evmrpc/utils.go +++ b/evmrpc/utils.go @@ -305,7 +305,7 @@ func ValidateBlockAccess(blockNumber int64, params BlockValidationParams) error } // ValidateBlockNumberAccess validates access to a block by block number. -func ValidateBlockNumberAccess(ctx context.Context, tmClient rpcclient.Client, number rpc.BlockNumber, params BlockValidationParams) error { +func ValidateBlockNumberAccess(number rpc.BlockNumber, params BlockValidationParams) error { // Special block numbers are always allowed if number == rpc.LatestBlockNumber || number == rpc.FinalizedBlockNumber { return nil @@ -325,17 +325,3 @@ func ValidateBlockHashAccess(ctx context.Context, tmClient rpcclient.Client, has return ValidateBlockAccess(block.Block.Height, params) } - -// ValidateTransactionAccess validates access to a transaction by converting it to its block number first. -// This function requires additional parameters to look up the transaction. -func ValidateTransactionAccess(ctx context.Context, tmClient rpcclient.Client, txHash common.Hash, keeper *keeper.Keeper, ctxProvider func(int64) sdk.Context, params BlockValidationParams) error { - // Get the transaction receipt to find the block number - sdkCtx := ctxProvider(LatestCtxHeight) - receipt, err := keeper.GetReceipt(sdkCtx, txHash) - if err != nil { - return fmt.Errorf("failed to get transaction receipt: %w", err) - } - - blockNumber := int64(receipt.BlockNumber) - return ValidateBlockAccess(blockNumber, params) -} From 120b882543ea3fa47f3dc122da1e28f2865bbeb9 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Tue, 26 Aug 2025 21:38:46 -0400 Subject: [PATCH 5/6] Unit tests --- evmrpc/tracers_test.go | 59 ++++++++++++++++++ evmrpc/utils_test.go | 132 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) 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_test.go b/evmrpc/utils_test.go index 60ed743bfe..06cbced3c5 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: 100, + MaxBlockLookback: 10, + EarliestVersion: 80, + } + + // 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") +} From 4b640ebfbc57b59391c51223e0f55f5d648bc77b Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Tue, 26 Aug 2025 21:44:23 -0400 Subject: [PATCH 6/6] update test --- evmrpc/utils_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evmrpc/utils_test.go b/evmrpc/utils_test.go index 06cbced3c5..3e1f4115b7 100644 --- a/evmrpc/utils_test.go +++ b/evmrpc/utils_test.go @@ -143,9 +143,9 @@ func TestValidateBlockNumberAccess(t *testing.T) { func TestValidateBlockHashAccess(t *testing.T) { params := evmrpc.BlockValidationParams{ - LatestHeight: 100, + LatestHeight: MockHeight8, MaxBlockLookback: 10, - EarliestVersion: 80, + EarliestVersion: 1, } // Test with a valid hash that maps to a valid block