diff --git a/docker/localnode/config/app.toml b/docker/localnode/config/app.toml index 89ed5e9b22..dadff7f527 100644 --- a/docker/localnode/config/app.toml +++ b/docker/localnode/config/app.toml @@ -278,6 +278,10 @@ enable_test_api = true # Set to 0 to disable request limiter, otherwise this limits the number of concurrent simulation calls. max_concurrent_simulation_calls = 0 +# Native debug tracers allowed in TraceConfig.Tracer. JavaScript tracer source is disabled. +trace_allowed_tracers = ["callTracer", "prestateTracer", "flatCallTracer", "4byteTracer", "noopTracer", "muxTracer"] +trace_allow_js_tracers = false + # Legacy sei_* / sei2_* JSON-RPC (EVM HTTP only). # DEPRECATION: All sei_* and sei2_* methods are deprecated and scheduled for removal - no new integrations. # HTTP 200; gate errors: JSON-RPC error (data legacy_sei_deprecated). Success: unchanged body; optional header Sei-Legacy-RPC-Deprecation. diff --git a/docker/rpcnode/config/app.toml b/docker/rpcnode/config/app.toml index c8134e320a..d4a6556c20 100644 --- a/docker/rpcnode/config/app.toml +++ b/docker/rpcnode/config/app.toml @@ -266,6 +266,10 @@ enable_test_api = true # Set to 0 to disable request limiter, otherwise this limits the number of concurrent simulation calls. max_concurrent_simulation_calls = 0 +# Native debug tracers allowed in TraceConfig.Tracer. JavaScript tracer source is disabled. +trace_allowed_tracers = ["callTracer", "prestateTracer", "flatCallTracer", "4byteTracer", "noopTracer", "muxTracer"] +trace_allow_js_tracers = false + # Legacy sei_* / sei2_* JSON-RPC (EVM HTTP only). # DEPRECATION: All sei_* and sei2_* methods are deprecated and scheduled for removal - no new integrations. # HTTP 200; gate errors: JSON-RPC error (data legacy_sei_deprecated). Success: unchanged body; optional header Sei-Legacy-RPC-Deprecation. diff --git a/evmrpc/AGENTS.md b/evmrpc/AGENTS.md index 666a655992..315eef3f5b 100644 --- a/evmrpc/AGENTS.md +++ b/evmrpc/AGENTS.md @@ -23,5 +23,7 @@ Legacy **`sei_*` and `sei2_*`** JSON-RPC (EVM HTTP only) are **gated** by the sa ## `debug_` prefixed endpoints `debug_trace*` endpoints should faithfully replay historical execution. If a transaction encountered an error during its actual execution, a `debug_trace*` call for it should reflect so. If a transction consumed X amount of gas during its actual execution, a `debug_trace*` call should show that exact amount as well. +**Tracer gating (deviation from geth defaults):** caller-supplied `TraceConfig.Tracer` values on `debug_traceCall` / `debug_traceTransaction` / `debug_traceBlockBy*` / `debug_traceTransactionProfile` are gated by `[evm]` config in `app.toml`. `trace_allowed_tracers` lists the native geth tracer names callers may request (validated native-only at startup; `muxTracer` nested tracer names are validated recursively with a bounded depth). `trace_allow_js_tracers` (default `false`) is a separate explicit opt-in for request-supplied JavaScript tracer source — upstream geth accepts JS tracers by default, Sei does not. Enabling JS does **not** widen the native allowlist. Validation runs in `validateTraceTracer` (`tracers.go`) before trace-cache lookups and before any tracer is constructed; the default struct logger (no `tracer` field) is always available. `trace_bake_tracers` is held to the same native-only rule at startup. + ## Consistency RPC responses for historical heights should never change as the blockchain progresses, or as the blockchain code gets upgraded. diff --git a/evmrpc/config/config.go b/evmrpc/config/config.go index e4a3d07f44..c0960a341d 100644 --- a/evmrpc/config/config.go +++ b/evmrpc/config/config.go @@ -3,6 +3,7 @@ package config import ( "fmt" "runtime" + "strings" "time" "github.com/ethereum/go-ethereum/rpc" @@ -31,8 +32,43 @@ const ( // goroutine creation on high-core machines. Tasks are primarily I/O bound // (fetching and processing block logs), so 2x CPU cores can be excessive. MaxWorkerPoolSize = 64 + + TraceTracer4Byte = "4byteTracer" + TraceTracerCall = "callTracer" + TraceTracerFlatCall = "flatCallTracer" + TraceTracerMux = "muxTracer" + TraceTracerNoop = "noopTracer" + TraceTracerPrestate = "prestateTracer" ) +var nativeTraceTracers = map[string]struct{}{ + TraceTracer4Byte: {}, + TraceTracerCall: {}, + TraceTracerFlatCall: {}, + TraceTracerMux: {}, + TraceTracerNoop: {}, + TraceTracerPrestate: {}, +} + +// DefaultTraceAllowedTracers returns the native debug tracers allowed by default. +func DefaultTraceAllowedTracers() []string { + return []string{ + TraceTracerCall, + TraceTracerPrestate, + TraceTracerFlatCall, + TraceTracer4Byte, + TraceTracerNoop, + TraceTracerMux, + } +} + +// IsNativeTraceTracer reports whether name is a registered native geth tracer +// name that can be safely allowlisted without enabling request-supplied JS. +func IsNativeTraceTracer(name string) bool { + _, ok := nativeTraceTracers[name] + return ok +} + // EVMRPC Config defines configurations for EVM RPC server on this node type Config struct { // controls whether an HTTP EVM server is enabled @@ -141,6 +177,17 @@ type Config struct { // (matches upstream geth behavior). MaxTraceStructLogBytes uint64 `mapstructure:"max_trace_struct_log_bytes"` + // TraceAllowedTracers lists native debug tracer names that callers may + // request with TraceConfig.Tracer. The default struct logger remains + // available when Tracer is omitted. Request-supplied JavaScript tracers are + // never allowed through this list. + TraceAllowedTracers []string `mapstructure:"trace_allowed_tracers"` + + // TraceAllowJSTracers permits callers to supply JavaScript tracer source in + // TraceConfig.Tracer. This executes request-supplied code in-process and is + // disabled by default. + TraceAllowJSTracers bool `mapstructure:"trace_allow_js_tracers"` + // EnableParallelizedBlockTrace enables the parallelized default debug_traceBlock* path. EnableParallelizedBlockTrace bool `mapstructure:"enable_parallelized_block_trace"` @@ -238,6 +285,8 @@ var DefaultConfig = Config{ MaxTraceLookbackBlocks: 10000, TraceTimeout: 30 * time.Second, MaxTraceStructLogBytes: 32 * 1024 * 1024, // 32 MiB + TraceAllowedTracers: DefaultTraceAllowedTracers(), + TraceAllowJSTracers: false, EnableParallelizedBlockTrace: false, RPCStatsInterval: 10 * time.Second, WorkerPoolSize: min(MaxWorkerPoolSize, runtime.NumCPU()*2), // Default: min(64, CPU cores × 2) @@ -292,6 +341,8 @@ const ( flagMaxTraceLookbackBlocks = "evm.max_trace_lookback_blocks" flagTraceTimeout = "evm.trace_timeout" flagMaxTraceStructLogBytes = "evm.max_trace_struct_log_bytes" + flagTraceAllowedTracers = "evm.trace_allowed_tracers" + flagTraceAllowJSTracers = "evm.trace_allow_js_tracers" flagEnableParallelizedBlockTrace = "evm.enable_parallelized_block_trace" flagRPCStatsInterval = "evm.rpc_stats_interval" flagWorkerPoolSize = "evm.worker_pool_size" @@ -456,6 +507,19 @@ func ReadConfig(opts servertypes.AppOptions) (Config, error) { return cfg, err } } + if v := opts.Get(flagTraceAllowedTracers); v != nil { + if cfg.TraceAllowedTracers, err = cast.ToStringSliceE(v); err != nil { + return cfg, err + } + } + if cfg.TraceAllowedTracers, err = normalizeNativeTracerNames(flagTraceAllowedTracers, cfg.TraceAllowedTracers); err != nil { + return cfg, err + } + if v := opts.Get(flagTraceAllowJSTracers); v != nil { + if cfg.TraceAllowJSTracers, err = cast.ToBoolE(v); err != nil { + return cfg, err + } + } if v := opts.Get(flagEnableParallelizedBlockTrace); v != nil { if cfg.EnableParallelizedBlockTrace, err = cast.ToBoolE(v); err != nil { return cfg, err @@ -501,6 +565,12 @@ func ReadConfig(opts servertypes.AppOptions) (Config, error) { return cfg, err } } + // The baker feeds these names straight into geth tracing, so hold them to + // the same native-only rule as request-supplied tracers: a typo'd name must + // fail at startup instead of being evaluated as JS source on every block. + if cfg.TraceBakeTracers, err = normalizeNativeTracerNames(flagTraceBakeTracers, cfg.TraceBakeTracers); err != nil { + return cfg, err + } if v := opts.Get(flagTraceBakeWindowBlocks); v != nil { if cfg.TraceBakeWindowBlocks, err = cast.ToInt64E(v); err != nil { return cfg, err @@ -557,6 +627,26 @@ func ReadConfig(opts servertypes.AppOptions) (Config, error) { return cfg, nil } +func normalizeNativeTracerNames(flagName string, names []string) ([]string, error) { + out := make([]string, 0, len(names)) + seen := make(map[string]struct{}, len(names)) + for _, name := range names { + trimmed := strings.TrimSpace(name) + if trimmed == "" { + return nil, fmt.Errorf("%s entries must not be empty", flagName) + } + if !IsNativeTraceTracer(trimmed) { + return nil, fmt.Errorf("%s contains non-native tracer %q", flagName, trimmed) + } + if _, ok := seen[trimmed]; ok { + continue + } + seen[trimmed] = struct{}{} + out = append(out, trimmed) + } + return out, nil +} + // ConfigTemplate defines the TOML configuration template for EVM RPC const ConfigTemplate = ` ############################################################################### @@ -709,6 +799,18 @@ trace_timeout = "{{ .EVM.TraceTimeout }}" # upstream geth behavior). max_trace_struct_log_bytes = {{ .EVM.MaxTraceStructLogBytes }} +# Native debug tracers that may be requested with TraceConfig.Tracer. The default +# struct logger remains available when Tracer is omitted. Request-supplied +# JavaScript tracer source is disabled and cannot be enabled through this list. +# Set to [] to disable all named tracers. +trace_allowed_tracers = [{{- range $i, $t := .EVM.TraceAllowedTracers }}{{- if $i }}, {{ end }}"{{ $t }}"{{- end }}] + +# Allow request-supplied JavaScript tracer source in TraceConfig.Tracer. This +# executes untrusted code in-process; keep disabled on public/default RPC nodes. +# Enabling this does not widen trace_allowed_tracers: native tracer names must +# still be listed there to be usable. +trace_allow_js_tracers = {{ .EVM.TraceAllowJSTracers }} + # Enable the parallelized default debug_traceBlock* path. enable_parallelized_block_trace = {{ .EVM.EnableParallelizedBlockTrace }} @@ -734,7 +836,7 @@ trace_bake_workers = {{ .EVM.TraceBakeWorkers }} # Bounded in-flight height queue. Drops on full so consensus never blocks. trace_bake_queue_size = {{ .EVM.TraceBakeQueueSize }} -# Which tracers to bake per block; only standard named tracers are eligible. +# Which tracers to bake per block; must be native tracer names (validated at startup). trace_bake_tracers = [{{- range $i, $t := .EVM.TraceBakeTracers }}{{- if $i }}, {{ end }}"{{ $t }}"{{- end }}] # Rolling cache window: prune blocks older than (latest - this). diff --git a/evmrpc/config/config_test.go b/evmrpc/config/config_test.go index 6e41dd607d..9da73d5ebe 100644 --- a/evmrpc/config/config_test.go +++ b/evmrpc/config/config_test.go @@ -49,6 +49,9 @@ type opts struct { maxConcurrentRequestBytes interface{} maxOpenConnections interface{} maxTraceStructLogBytes interface{} + traceAllowedTracers interface{} + traceAllowJSTracers interface{} + traceBakeTracers interface{} } func (o *opts) Get(k string) interface{} { @@ -151,10 +154,12 @@ func (o *opts) Get(k string) interface{} { if k == "evm.enabled_legacy_sei_apis" { return nil } + if k == "evm.trace_bake_tracers" { + return o.traceBakeTracers + } if k == "evm.trace_bake_enabled" || k == "evm.trace_bake_workers" || k == "evm.trace_bake_queue_size" || - k == "evm.trace_bake_tracers" || k == "evm.trace_bake_window_blocks" || k == "evm.trace_bake_use_snapshot" || k == "evm.trace_bake_snapshot_window" { @@ -184,6 +189,12 @@ func (o *opts) Get(k string) interface{} { if k == "evm.max_trace_struct_log_bytes" { return o.maxTraceStructLogBytes } + if k == "evm.trace_allowed_tracers" { + return o.traceAllowedTracers + } + if k == "evm.trace_allow_js_tracers" { + return o.traceAllowJSTracers + } panic("unknown key") } @@ -230,6 +241,9 @@ func getDefaultOpts() opts { int64(128 * 1024 * 1024), 2000, uint64(256 * 1024 * 1024), + []string{"callTracer", "prestateTracer"}, + false, + nil, } } @@ -240,6 +254,8 @@ func TestReadConfig(t *testing.T) { require.False(t, cfg.EnableParallelizedBlockTrace) // Round-trip: an explicitly-supplied value overrides the default. require.Equal(t, uint64(256*1024*1024), cfg.MaxTraceStructLogBytes) + require.Equal(t, []string{"callTracer", "prestateTracer"}, cfg.TraceAllowedTracers) + require.False(t, cfg.TraceAllowJSTracers) // The shipped default (used when the operator supplies no value). require.Equal(t, uint64(32*1024*1024), config.DefaultConfig.MaxTraceStructLogBytes) badOpts := goodOpts @@ -343,6 +359,21 @@ func TestReadConfig(t *testing.T) { _, err = config.ReadConfig(&badOpts) require.NotNil(t, err) + badOpts = goodOpts + badOpts.traceAllowedTracers = map[string]interface{}{} + _, err = config.ReadConfig(&badOpts) + require.NotNil(t, err) + + badOpts = goodOpts + badOpts.traceAllowedTracers = []string{"callTracer", "function() { return {}; }"} + _, err = config.ReadConfig(&badOpts) + require.NotNil(t, err) + + badOpts = goodOpts + badOpts.traceAllowJSTracers = "bad" + _, err = config.ReadConfig(&badOpts) + require.NotNil(t, err) + // Test bad types for worker pool config badOpts = goodOpts badOpts.workerPoolSize = "bad" @@ -502,6 +533,63 @@ func TestReadConfigEnableParallelizedBlockTrace(t *testing.T) { require.True(t, cfg.EnableParallelizedBlockTrace) } +func TestReadConfigTraceAllowedTracers(t *testing.T) { + cfg, err := config.ReadConfig(&opts{}) + require.NoError(t, err) + require.Equal(t, config.DefaultTraceAllowedTracers(), cfg.TraceAllowedTracers) + + opts := getDefaultOpts() + opts.traceAllowedTracers = []string{ + "callTracer", + "callTracer", + " flatCallTracer ", + } + cfg, err = config.ReadConfig(&opts) + require.NoError(t, err) + require.Equal(t, []string{"callTracer", "flatCallTracer"}, cfg.TraceAllowedTracers) + + opts.traceAllowedTracers = []string{"callTracer", ""} + _, err = config.ReadConfig(&opts) + require.Error(t, err) + + opts.traceAllowedTracers = []string{"callTracer", "badTracer"} + _, err = config.ReadConfig(&opts) + require.Error(t, err) + + opts.traceAllowedTracers = []string{"callTracer", "function() { return {}; }"} + opts.traceAllowJSTracers = true + _, err = config.ReadConfig(&opts) + require.Error(t, err, "trace_allowed_tracers remains native-only even when JS tracers are allowed") + + opts.traceAllowedTracers = []string{"callTracer"} + opts.traceAllowJSTracers = true + cfg, err = config.ReadConfig(&opts) + require.NoError(t, err) + require.True(t, cfg.TraceAllowJSTracers) +} + +func TestReadConfigTraceBakeTracers(t *testing.T) { + cfg, err := config.ReadConfig(&opts{}) + require.NoError(t, err) + require.Equal(t, []string{"callTracer"}, cfg.TraceBakeTracers) + + o := getDefaultOpts() + o.traceBakeTracers = []string{"prestateTracer", " callTracer ", "prestateTracer"} + cfg, err = config.ReadConfig(&o) + require.NoError(t, err) + require.Equal(t, []string{"prestateTracer", "callTracer"}, cfg.TraceBakeTracers) + + o.traceBakeTracers = []string{"callTracer", "badTracer"} + _, err = config.ReadConfig(&o) + require.Error(t, err) + + // A JS-looking name must fail at startup rather than being evaluated as + // JS source by the baker on every committed block. + o.traceBakeTracers = []string{"function() { return {}; }"} + _, err = config.ReadConfig(&o) + require.Error(t, err) +} + func TestReadConfigMaxSubscriptionsLogs(t *testing.T) { opts := getDefaultOpts() opts.maxSubscriptionsLogs = uint64(42) diff --git a/evmrpc/trace_profile.go b/evmrpc/trace_profile.go index f061808ecd..244836c5c6 100644 --- a/evmrpc/trace_profile.go +++ b/evmrpc/trace_profile.go @@ -48,6 +48,9 @@ func (api *DebugAPI) TraceTransactionProfile(ctx context.Context, hash common.Ha recordMetricsWithError(ctx, "debug_traceTransactionProfile", api.connectionType, startTime, returnErr, recover()) }() + if returnErr = api.validateTraceTracer(config); returnErr != nil { + return nil, returnErr + } if returnErr = api.guardHistoricalDebugTraceByTxHash(ctx, "debug_traceTransactionProfile", hash); returnErr != nil { return nil, returnErr } diff --git a/evmrpc/tracers.go b/evmrpc/tracers.go index 807ed72094..21054a7a85 100644 --- a/evmrpc/tracers.go +++ b/evmrpc/tracers.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "runtime/debug" + "strings" "sync" "time" @@ -33,9 +34,12 @@ const ( IsPanicCacheSize = 5000 IsPanicCacheTTL = 1 * time.Minute - callTracerName = "callTracer" - prestateTracerName = "prestateTracer" - flatCallTracerName = "flatCallTracer" + callTracerName = evmrpcconfig.TraceTracerCall + prestateTracerName = evmrpcconfig.TraceTracerPrestate + flatCallTracerName = evmrpcconfig.TraceTracerFlatCall + muxTracerName = evmrpcconfig.TraceTracerMux + + maxMuxTracerNestingDepth = 16 ) var errTraceConcurrencyLimit = errors.New("trace request rejected due to concurrency limit: server busy") @@ -53,6 +57,8 @@ type DebugAPI struct { maxBlockLookback int64 traceTimeout time.Duration maxStructLogBytes int // per-call cap on retained default struct-logger output; 0 = unlimited + allowedTracers map[string]struct{} + allowJSTracers bool profiledBlockTrace bool } @@ -207,6 +213,8 @@ func NewDebugAPI( maxBlockLookback: debugCfg.MaxTraceLookbackBlocks, traceTimeout: debugCfg.TraceTimeout, maxStructLogBytes: clampUint64ToInt(debugCfg.MaxTraceStructLogBytes), + allowedTracers: buildAllowedTracerSet(debugCfg.TraceAllowedTracers), + allowJSTracers: debugCfg.TraceAllowJSTracers, profiledBlockTrace: debugCfg.EnableParallelizedBlockTrace, } } @@ -237,12 +245,92 @@ func (api *DebugAPI) clampDefaultStructLogLimit(config *tracers.TraceConfig) { } } +func buildAllowedTracerSet(names []string) map[string]struct{} { + allowed := make(map[string]struct{}, len(names)) + for _, name := range names { + allowed[name] = struct{}{} + } + return allowed +} + +func (api *DebugAPI) validateTraceTracer(config *tracers.TraceConfig) error { + if config == nil || config.Tracer == nil { + return nil + } + name := strings.TrimSpace(*config.Tracer) + if name == "" { + return errors.New("debug tracer name must not be empty") + } + // Write the trimmed name back so geth resolves the exact tracer that was + // validated; otherwise a padded native name (e.g. " callTracer") would pass + // validation here yet be treated as JS source by the tracer directory. + *config.Tracer = name + if _, ok := api.allowedTracers[name]; !ok { + if api.allowJSTracers && !evmrpcconfig.IsNativeTraceTracer(name) { + return nil + } + if api.allowJSTracers { + return fmt.Errorf("debug native tracer %q is not listed in evm.trace_allowed_tracers", name) + } + return fmt.Errorf("debug tracer %q is not allowed; JavaScript tracers are disabled and only native tracers listed in evm.trace_allowed_tracers may be used", name) + } + if name == muxTracerName { + if err := validateMuxTraceConfig(config.TracerConfig, api.allowedTracers, api.allowJSTracers, 1); err != nil { + return fmt.Errorf("invalid muxTracer config: %w", err) + } + } + return nil +} + +func validateMuxTraceConfig(raw json.RawMessage, allowed map[string]struct{}, allowJS bool, depth int) error { + if len(raw) == 0 { + return nil + } + if depth > maxMuxTracerNestingDepth { + return fmt.Errorf("muxTracer nesting depth exceeds maximum of %d", maxMuxTracerNestingDepth) + } + var nested map[string]json.RawMessage + if err := json.Unmarshal(raw, &nested); err != nil { + return err + } + for name, cfg := range nested { + if strings.TrimSpace(name) == "" { + return errors.New("nested debug tracer name must not be empty") + } + // Nested names live inside caller-supplied JSON that is forwarded to + // geth verbatim, so a padded name cannot be rewritten the way the + // top-level tracer is. Reject it instead of validating a name geth + // would resolve differently. + if name != strings.TrimSpace(name) { + return fmt.Errorf("nested debug tracer name %q must not have leading or trailing whitespace", name) + } + if _, ok := allowed[name]; !ok { + if allowJS && !evmrpcconfig.IsNativeTraceTracer(name) { + continue + } + if allowJS { + return fmt.Errorf("nested native debug tracer %q is not listed in evm.trace_allowed_tracers", name) + } + return fmt.Errorf("nested debug tracer %q is not allowed; JavaScript tracers are disabled", name) + } + if name == muxTracerName { + if err := validateMuxTraceConfig(cfg, allowed, allowJS, depth+1); err != nil { + return err + } + } + } + return nil +} + func (api *DebugAPI) TraceTransaction(ctx context.Context, hash common.Hash, config *tracers.TraceConfig) (result interface{}, returnErr error) { startTime := time.Now() defer func() { recordMetricsWithError(ctx, "debug_traceTransaction", api.connectionType, startTime, returnErr, recover()) }() + if returnErr = api.validateTraceTracer(config); returnErr != nil { + return nil, returnErr + } if returnErr = api.guardHistoricalDebugTraceByTxHash(ctx, "debug_traceTransaction", hash); returnErr != nil { return nil, returnErr } @@ -450,6 +538,9 @@ func (api *DebugAPI) TraceBlockByNumber(ctx context.Context, number rpc.BlockNum recordMetricsWithError(ctx, "debug_traceBlockByNumber", api.connectionType, startTime, returnErr, recover()) }() + if returnErr = api.validateTraceTracer(config); returnErr != nil { + return nil, returnErr + } if returnErr = api.guardHistoricalDebugTraceByNumber(ctx, "debug_traceBlockByNumber", number); returnErr != nil { return nil, returnErr } @@ -482,6 +573,10 @@ func (api *DebugAPI) TraceBlockByHash(ctx context.Context, hash common.Hash, con recordMetricsWithError(ctx, "debug_traceBlockByHash", api.connectionType, startTime, returnErr, recover()) }() + if returnErr = api.validateTraceTracer(config); returnErr != nil { + return nil, returnErr + } + ctx, done, err := api.prepareTraceContext(ctx) if err != nil { return nil, err @@ -514,6 +609,13 @@ func (api *DebugAPI) TraceCall(ctx context.Context, args export.TransactionArgs, recordMetricsWithError(ctx, "debug_traceCall", api.connectionType, startTime, returnErr, recover()) }() + if config == nil { + config = &tracers.TraceCallConfig{} + } + if returnErr = api.validateTraceTracer(&config.TraceConfig); returnErr != nil { + return nil, returnErr + } + ctx, done, err := api.prepareTraceContext(ctx) if err != nil { return nil, err @@ -524,9 +626,6 @@ func (api *DebugAPI) TraceCall(ctx context.Context, args export.TransactionArgs, return nil, returnErr } - if config == nil { - config = &tracers.TraceCallConfig{} - } api.clampDefaultStructLogLimit(&config.TraceConfig) result, returnErr = api.tracersAPI.TraceCall(ctx, args, blockNrOrHash, config) return diff --git a/evmrpc/tracers_allowlist_test.go b/evmrpc/tracers_allowlist_test.go new file mode 100644 index 0000000000..f74b373791 --- /dev/null +++ b/evmrpc/tracers_allowlist_test.go @@ -0,0 +1,166 @@ +package evmrpc + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/eth/tracers" + evmrpcconfig "github.com/sei-protocol/sei-chain/evmrpc/config" + "github.com/stretchr/testify/require" +) + +// TestDefaultAllowedTracersAreNativeInGethRegistry pins the hardcoded native +// tracer list to the actual go-ethereum registry (whose init()s this package +// imports): every default-allowlisted name must be registered and non-JS, so a +// geth upgrade that changes the registry fails CI instead of silently +// weakening the JS-tracer gate. +func TestDefaultAllowedTracersAreNativeInGethRegistry(t *testing.T) { + for _, name := range evmrpcconfig.DefaultTraceAllowedTracers() { + require.False(t, tracers.DefaultDirectory.IsJS(name), "tracer %q must be registered as a native (non-JS) tracer in go-ethereum", name) + } +} + +func TestValidateTraceTracerAllowlist(t *testing.T) { + api := &DebugAPI{ + allowedTracers: buildAllowedTracerSet([]string{ + callTracerName, + prestateTracerName, + muxTracerName, + }), + } + + require.NoError(t, api.validateTraceTracer(nil)) + require.NoError(t, api.validateTraceTracer(&tracers.TraceConfig{})) + + name := callTracerName + require.NoError(t, api.validateTraceTracer(&tracers.TraceConfig{Tracer: &name})) + + name = " callTracer " + paddedCfg := &tracers.TraceConfig{Tracer: &name} + require.NoError(t, api.validateTraceTracer(paddedCfg)) + require.Equal(t, callTracerName, *paddedCfg.Tracer, "validated name must be written back trimmed so geth resolves the native tracer") + + name = flatCallTracerName + require.ErrorContains(t, api.validateTraceTracer(&tracers.TraceConfig{Tracer: &name}), "is not allowed") + + name = "function() { return {}; }" + require.ErrorContains(t, api.validateTraceTracer(&tracers.TraceConfig{Tracer: &name}), "JavaScript tracers are disabled") + + name = "" + require.ErrorContains(t, api.validateTraceTracer(&tracers.TraceConfig{Tracer: &name}), "must not be empty") +} + +func TestValidateTraceTracerAllowsJSWhenConfigured(t *testing.T) { + api := &DebugAPI{ + allowedTracers: buildAllowedTracerSet([]string{ + callTracerName, + muxTracerName, + }), + allowJSTracers: true, + } + + name := "function() { return {}; }" + require.NoError(t, api.validateTraceTracer(&tracers.TraceConfig{Tracer: &name})) + + name = flatCallTracerName + require.ErrorContains(t, api.validateTraceTracer(&tracers.TraceConfig{Tracer: &name}), "native tracer") + + name = "" + require.ErrorContains(t, api.validateTraceTracer(&tracers.TraceConfig{Tracer: &name}), "must not be empty") +} + +func TestValidateMuxTraceConfig(t *testing.T) { + api := &DebugAPI{ + allowedTracers: buildAllowedTracerSet([]string{ + callTracerName, + prestateTracerName, + muxTracerName, + }), + } + + name := muxTracerName + require.NoError(t, api.validateTraceTracer(&tracers.TraceConfig{ + Tracer: &name, + TracerConfig: json.RawMessage(`{"callTracer":{},"prestateTracer":{}}`), + })) + require.NoError(t, api.validateTraceTracer(&tracers.TraceConfig{ + Tracer: &name, + TracerConfig: json.RawMessage(`{"muxTracer":{"callTracer":{}}}`), + })) + + err := api.validateTraceTracer(&tracers.TraceConfig{ + Tracer: &name, + TracerConfig: json.RawMessage(`{"function() { return {}; }":{}}`), + }) + require.ErrorContains(t, err, "nested debug tracer") + require.ErrorContains(t, err, "JavaScript tracers are disabled") + + err = api.validateTraceTracer(&tracers.TraceConfig{ + Tracer: &name, + TracerConfig: json.RawMessage(`{"muxTracer":{"badTracer":{}}}`), + }) + require.ErrorContains(t, err, "nested debug tracer") + require.ErrorContains(t, err, "badTracer") + + // Nested names are forwarded to geth inside the raw JSON, so a padded + // native name cannot be rewritten and must be rejected outright. + err = api.validateTraceTracer(&tracers.TraceConfig{ + Tracer: &name, + TracerConfig: json.RawMessage(`{" callTracer":{}}`), + }) + require.ErrorContains(t, err, "must not have leading or trailing whitespace") +} + +func TestValidateMuxTraceConfigRejectsDeepNesting(t *testing.T) { + api := &DebugAPI{ + allowedTracers: buildAllowedTracerSet([]string{ + callTracerName, + muxTracerName, + }), + } + + name := muxTracerName + require.NoError(t, api.validateTraceTracer(&tracers.TraceConfig{ + Tracer: &name, + TracerConfig: nestedMuxTraceConfig(maxMuxTracerNestingDepth - 1), + })) + + err := api.validateTraceTracer(&tracers.TraceConfig{ + Tracer: &name, + TracerConfig: nestedMuxTraceConfig(maxMuxTracerNestingDepth), + }) + require.ErrorContains(t, err, "muxTracer nesting depth exceeds maximum") +} + +func TestValidateMuxTraceConfigAllowsJSWhenConfigured(t *testing.T) { + api := &DebugAPI{ + allowedTracers: buildAllowedTracerSet([]string{ + callTracerName, + muxTracerName, + }), + allowJSTracers: true, + } + + name := muxTracerName + require.NoError(t, api.validateTraceTracer(&tracers.TraceConfig{ + Tracer: &name, + TracerConfig: json.RawMessage(`{"function() { return {}; }":{}}`), + })) + + err := api.validateTraceTracer(&tracers.TraceConfig{ + Tracer: &name, + TracerConfig: json.RawMessage(`{"flatCallTracer":{}}`), + }) + require.ErrorContains(t, err, "nested native debug tracer") +} + +func nestedMuxTraceConfig(nestedMuxTracers int) json.RawMessage { + var builder strings.Builder + for range nestedMuxTracers { + builder.WriteString(`{"muxTracer":`) + } + builder.WriteString(`{"callTracer":{}}`) + builder.WriteString(strings.Repeat("}", nestedMuxTracers)) + return json.RawMessage(builder.String()) +}