diff --git a/evmrpc/historical_debug_trace_test.go b/evmrpc/historical_debug_trace_test.go new file mode 100644 index 0000000000..f5691517bf --- /dev/null +++ b/evmrpc/historical_debug_trace_test.go @@ -0,0 +1,93 @@ +package evmrpc + +import ( + "context" + "testing" + + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/stretchr/testify/require" +) + +func TestIsHistoricalDebugTraceBlock(t *testing.T) { + tests := []struct { + name string + blockHeight int64 + latestHeight int64 + maxBlockLookback int64 + want bool + }{ + { + name: "older than configured lookback", + blockHeight: 8, + latestHeight: 10, + maxBlockLookback: 1, + want: true, + }, + { + name: "equal to configured lookback", + blockHeight: 9, + latestHeight: 10, + maxBlockLookback: 1, + want: false, + }, + { + name: "zero lookback treats previous block as historical", + blockHeight: 9, + latestHeight: 10, + maxBlockLookback: 0, + want: true, + }, + { + name: "zero lookback allows latest block", + blockHeight: 10, + latestHeight: 10, + maxBlockLookback: 0, + want: false, + }, + { + name: "negative lookback disables classification", + blockHeight: 1, + latestHeight: 10, + maxBlockLookback: -1, + want: false, + }, + { + name: "future block", + blockHeight: 11, + latestHeight: 10, + maxBlockLookback: 0, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, isHistoricalDebugTraceBlock(tt.blockHeight, tt.latestHeight, tt.maxBlockLookback)) + }) + } +} + +func TestGuardHistoricalDebugTraceHeight(t *testing.T) { + latestCtx := sdk.Context{}.WithBlockHeight(10) + api := &DebugAPI{ + ctxProvider: func(int64) sdk.Context { return latestCtx }, + connectionType: ConnectionTypeHTTP, + maxBlockLookback: -1, + } + + err := api.guardHistoricalDebugTraceHeight(context.Background(), "debug_traceBlockByNumber", 8) + require.NoError(t, err) + + api.maxBlockLookback = 1 + err = api.guardHistoricalDebugTraceHeight(context.Background(), "debug_traceBlockByNumber", 8) + require.Error(t, err) + require.Contains(t, err.Error(), "block number 8 is beyond max lookback of 1") + + err = api.guardHistoricalDebugTraceHeight(context.Background(), "debug_traceBlockByNumber", 9) + require.NoError(t, err) + + api.maxBlockLookback = 0 + err = api.guardHistoricalDebugTraceHeight(context.Background(), "debug_traceBlockByNumber", 9) + require.Error(t, err) + require.Contains(t, err.Error(), "block number 9 is beyond max lookback of 0") +} diff --git a/evmrpc/metrics.go b/evmrpc/metrics.go index 402ca5875b..ecf736d81e 100644 --- a/evmrpc/metrics.go +++ b/evmrpc/metrics.go @@ -40,9 +40,10 @@ var ( rpcTelemetryMeter = otel.Meter("evmrpc") metrics = struct { - requestLatencySeconds metric.Float64Histogram - wsConnectionCount metric.Int64Counter - redirectedRequestCount metric.Int64Counter + requestLatencySeconds metric.Float64Histogram + wsConnectionCount metric.Int64Counter + redirectedRequestCount metric.Int64Counter + historicalDebugTraceAttemptCount metric.Int64Counter }{ requestLatencySeconds: must(rpcTelemetryMeter.Float64Histogram( "evmrpc_request_latency_seconds", @@ -63,6 +64,11 @@ var ( metric.WithDescription("Number of EVM RPC requests forwarded to another validator"), metric.WithUnit("{count}"), )), + historicalDebugTraceAttemptCount: must(rpcTelemetryMeter.Int64Counter( + "evmrpc_historical_debug_trace_attempts_total", + metric.WithDescription("Number of debug_trace* requests targeting historical blocks"), + metric.WithUnit("{count}"), + )), } ) @@ -140,3 +146,12 @@ func recordRedirectedRequest(ctx context.Context, endpoint, connection string) { ), ) } + +func recordHistoricalDebugTraceAttempt(ctx context.Context, endpoint, connection string) { + metrics.historicalDebugTraceAttemptCount.Add(ctx, 1, + metric.WithAttributes( + attribute.String(endpointKey, endpoint), + attribute.String(connectionKey, connection), + ), + ) +} diff --git a/evmrpc/trace_profile.go b/evmrpc/trace_profile.go index 159fd14262..68262bcbbe 100644 --- a/evmrpc/trace_profile.go +++ b/evmrpc/trace_profile.go @@ -48,6 +48,10 @@ func (api *DebugAPI) TraceTransactionProfile(ctx context.Context, hash common.Ha recordMetricsWithError(ctx, "debug_traceTransactionProfile", api.connectionType, startTime, returnErr, recover()) }() + if returnErr = api.guardHistoricalDebugTraceByTxHash(ctx, "debug_traceTransactionProfile", hash); returnErr != nil { + return nil, returnErr + } + ctx, done, err := api.prepareTraceContext(ctx) if err != nil { return nil, err diff --git a/evmrpc/tracers.go b/evmrpc/tracers.go index a2c22cc6fa..c3760bddda 100644 --- a/evmrpc/tracers.go +++ b/evmrpc/tracers.go @@ -92,6 +92,80 @@ func (api *DebugAPI) prepareTraceContext(ctx context.Context) (context.Context, }, nil } +func (api *DebugAPI) guardHistoricalDebugTraceByTxHash(ctx context.Context, endpoint string, hash common.Hash) error { + if api.keeper == nil { + return nil + } + receipt, err := api.keeper.GetReceipt(api.ctxProvider(LatestCtxHeight), hash) + if err != nil || receipt == nil { + return nil + } + return api.guardHistoricalDebugTraceHeight(ctx, endpoint, int64(receipt.BlockNumber)) //nolint:gosec +} + +func (api *DebugAPI) guardHistoricalDebugTraceByNumber(ctx context.Context, endpoint string, number rpc.BlockNumber) error { + height, err := api.resolveDebugTraceBlockNumber(ctx, number) + if err != nil { + return err + } + return api.guardHistoricalDebugTraceHeight(ctx, endpoint, height) +} + +func (api *DebugAPI) guardHistoricalDebugTraceByHash(ctx context.Context, endpoint string, hash common.Hash) error { + if api.backend == nil { + return nil + } + block, _, err := api.backend.BlockByHash(ctx, hash) + if err != nil || block == nil { + return nil + } + return api.guardHistoricalDebugTraceHeight(ctx, endpoint, int64(block.NumberU64())) //nolint:gosec +} + +func (api *DebugAPI) guardHistoricalDebugTraceByNumberOrHash(ctx context.Context, endpoint string, blockNrOrHash rpc.BlockNumberOrHash) error { + if number, ok := blockNrOrHash.Number(); ok { + return api.guardHistoricalDebugTraceByNumber(ctx, endpoint, number) + } + if hash, ok := blockNrOrHash.Hash(); ok { + return api.guardHistoricalDebugTraceByHash(ctx, endpoint, hash) + } + return api.guardHistoricalDebugTraceHeight(ctx, endpoint, api.ctxProvider(LatestCtxHeight).BlockHeight()) +} + +func (api *DebugAPI) resolveDebugTraceBlockNumber(ctx context.Context, number rpc.BlockNumber) (int64, error) { + switch number { + case rpc.SafeBlockNumber, rpc.FinalizedBlockNumber, rpc.LatestBlockNumber, rpc.PendingBlockNumber: + return api.ctxProvider(LatestCtxHeight).BlockHeight(), nil + case rpc.EarliestBlockNumber: + if api.tmClient == nil { + return 0, errors.New("tendermint client is not configured") + } + genesisRes, err := api.tmClient.Genesis(ctx) + if err != nil { + return 0, err + } + return genesisRes.Genesis.InitialHeight, nil + default: + return number.Int64(), nil + } +} + +func (api *DebugAPI) guardHistoricalDebugTraceHeight(ctx context.Context, endpoint string, blockHeight int64) error { + latest := api.ctxProvider(LatestCtxHeight).BlockHeight() + if !isHistoricalDebugTraceBlock(blockHeight, latest, api.maxBlockLookback) { + return nil + } + recordHistoricalDebugTraceAttempt(ctx, endpoint, string(api.connectionType)) + return fmt.Errorf("block number %d is beyond max lookback of %d", blockHeight, api.maxBlockLookback) +} + +func isHistoricalDebugTraceBlock(blockHeight, latestHeight, maxBlockLookback int64) bool { + if maxBlockLookback < 0 || blockHeight < 0 || latestHeight < blockHeight { + return false + } + return blockHeight < latestHeight-maxBlockLookback +} + type SeiDebugAPI struct { *DebugAPI } @@ -187,6 +261,10 @@ func (api *DebugAPI) TraceTransaction(ctx context.Context, hash common.Hash, con recordMetricsWithError(ctx, "debug_traceTransaction", api.connectionType, startTime, returnErr, recover()) }() + if returnErr = api.guardHistoricalDebugTraceByTxHash(ctx, "debug_traceTransaction", hash); returnErr != nil { + return nil, returnErr + } + if cached, ok := api.tryTraceCache(hash, config); ok { return cached, nil } @@ -540,17 +618,16 @@ func (api *DebugAPI) TraceBlockByNumber(ctx context.Context, number rpc.BlockNum recordMetricsWithError(ctx, "debug_traceBlockByNumber", api.connectionType, startTime, returnErr, recover()) }() + if returnErr = api.guardHistoricalDebugTraceByNumber(ctx, "debug_traceBlockByNumber", number); returnErr != nil { + return nil, returnErr + } + ctx, done, err := api.prepareTraceContext(ctx) if err != nil { return nil, err } defer done() - 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) - } - if cached, ok := api.tryBlockTraceCacheByNumber(ctx, number, config); ok { return cached, nil } @@ -569,6 +646,10 @@ func (api *DebugAPI) TraceBlockByHash(ctx context.Context, hash common.Hash, con recordMetricsWithError(ctx, "debug_traceBlockByHash", api.connectionType, startTime, returnErr, recover()) }() + if returnErr = api.guardHistoricalDebugTraceByHash(ctx, "debug_traceBlockByHash", hash); returnErr != nil { + return nil, returnErr + } + ctx, done, err := api.prepareTraceContext(ctx) if err != nil { return nil, err @@ -593,6 +674,10 @@ func (api *DebugAPI) TraceCall(ctx context.Context, args export.TransactionArgs, recordMetricsWithError(ctx, "debug_traceCall", api.connectionType, startTime, returnErr, recover()) }() + if returnErr = api.guardHistoricalDebugTraceByNumberOrHash(ctx, "debug_traceCall", blockNrOrHash); returnErr != nil { + return nil, returnErr + } + ctx, done, err := api.prepareTraceContext(ctx) if err != nil { return nil, err @@ -649,6 +734,9 @@ func (api *DebugAPI) TraceStateAccess(ctx context.Context, hash common.Hash) (re returnErr = fmt.Errorf("panic occurred: %v, could not trace tx state: %s", r, hash.Hex()) } }() + if returnErr = api.guardHistoricalDebugTraceByTxHash(ctx, "debug_traceStateAccess", hash); returnErr != nil { + return nil, returnErr + } tendermintTraces := &TendermintTraces{Traces: []TendermintTrace{}} ctx = WithTendermintTraces(ctx, tendermintTraces) receiptTraces := &ReceiptTraces{Traces: []RawResponseReceipt{}}