Skip to content
Open
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
4 changes: 4 additions & 0 deletions docker/localnode/config/app.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions docker/rpcnode/config/app.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions evmrpc/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
104 changes: 103 additions & 1 deletion evmrpc/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"fmt"
"runtime"
"strings"
"time"

"github.com/ethereum/go-ethereum/rpc"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"`

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = `
###############################################################################
Expand Down Expand Up @@ -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 }}

Expand All @@ -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).
Expand Down
90 changes: 89 additions & 1 deletion evmrpc/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{} {
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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")
}

Expand Down Expand Up @@ -230,6 +241,9 @@ func getDefaultOpts() opts {
int64(128 * 1024 * 1024),
2000,
uint64(256 * 1024 * 1024),
[]string{"callTracer", "prestateTracer"},
false,
nil,
}
}

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