Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions evmrpc/tracers.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,20 @@
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)

Check failure on line 143 in evmrpc/tracers.go

View workflow job for this annotation

GitHub Actions / lint

declaration has 3 blank identifiers (dogsled)
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)
Expand All @@ -152,9 +166,10 @@
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()
Expand Down Expand Up @@ -185,6 +200,12 @@
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
Expand Down Expand Up @@ -266,9 +287,10 @@
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()
Expand All @@ -284,6 +306,12 @@
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)
Expand Down Expand Up @@ -348,3 +376,19 @@
}
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(),
}
}
59 changes: 59 additions & 0 deletions evmrpc/tracers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package evmrpc_test

import (
"fmt"
"strings"
"testing"

testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper"
Expand Down Expand Up @@ -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)
}
}
45 changes: 45 additions & 0 deletions evmrpc/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the config enough to validatae that the block exists/doesn't? what if someone increases their min-retain-blocks, but the history isn't there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, these checks are for the state level @philipsu522 and directly calls api.backend.app.CommitMultiStore().GetEarliestVersion() which has the earliest historical version.

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)
}
132 changes: 132 additions & 0 deletions evmrpc/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
}
Loading