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
21 changes: 21 additions & 0 deletions evmrpc/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
# EVM RPC SPECS
EVM RPCs live under `evmrpc/` folder.

## HTTP middleware order (JSON-RPC)

When JWT is configured, unauthenticated requests are rejected before the byte
budget is touched:

```
jwt → requestSizeLimiter → seiLegacyHTTPGate → gzip → vhost → cors → rpc.Server
```

Without JWT:

```
requestSizeLimiter → seiLegacyHTTPGate → gzip → vhost → cors → rpc.Server
```

`requestSizeLimiter` caps each body with `http.MaxBytesReader`, charges the
global `max_concurrent_request_bytes` budget incrementally as body bytes are
read (64 KiB batches), and enforces `body_read_idle_timeout` between body
chunks via `http.ResponseController.SetReadDeadline` (HTTP 408 on stall, HTTP
429 on mid-read budget exhaustion).

EVM RPCs prefixed by `eth_` and `debug_` on Sei generally follows [Ethereum's spec](https://www.quicknode.com/docs/ethereum/api-overview). However, there are some notable distinctions.

- **Pending** - Sei has instant finality and thus has no concept of `pending` blocks. However, the RPCs still accept `pending` for applicable parameters, and will treat it equivalent to `final`/`safe`/`latest`.
Expand Down
32 changes: 25 additions & 7 deletions evmrpc/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,17 @@ type Config struct {
MaxRequestBodyBytes int64 `mapstructure:"max_request_body_bytes"`

// MaxConcurrentRequestBytes bounds the total size, in bytes, of HTTP
// JSON-RPC request bodies admitted for processing concurrently, weighted by
// each request's Content-Length. Requests that would exceed the budget are
// rejected fast (HTTP 429) before decode, capping peak memory under load.
// Set to 0 to disable the limit.
// JSON-RPC request bodies admitted for processing concurrently, charged
// incrementally as body bytes are read. Requests that would exceed the
// budget mid-read are rejected (HTTP 429). Set to 0 to disable the limit.
MaxConcurrentRequestBytes int64 `mapstructure:"max_concurrent_request_bytes"`

// BodyReadIdleTimeout is the maximum idle time allowed between body chunks
// while reading an HTTP JSON-RPC request. Stalled body reads are cut with
// HTTP 408 and release any byte budget held so far. Zero disables the
// per-chunk idle guard (http.Server ReadTimeout remains the backstop).
BodyReadIdleTimeout time.Duration `mapstructure:"body_read_idle_timeout"`

// MaxOpenConnections caps the number of simultaneously accepted connections
// on the EVM HTTP and WebSocket listeners. The limit is applied per listener
// (HTTP and WS each get their own budget). Excess connections block in the
Expand Down Expand Up @@ -327,6 +332,7 @@ var DefaultConfig = Config{
MaxRequestBodyBytes: 5 * 1024 * 1024, // 5 MiB (matches go-ethereum rpc default body limit)
MaxConcurrentRequestBytes: 128 * 1024 * 1024, // 128 MiB of request bodies admitted concurrently
MaxOpenConnections: 2000,
BodyReadIdleTimeout: 10 * time.Second,
}

const (
Expand Down Expand Up @@ -382,6 +388,7 @@ const (
flagMaxRequestBodyBytes = "evm.max_request_body_bytes"
flagMaxConcurrentRequestBytes = "evm.max_concurrent_request_bytes"
flagMaxOpenConnections = "evm.max_open_connections"
flagBodyReadIdleTimeout = "evm.body_read_idle_timeout"
)

func ReadConfig(opts servertypes.AppOptions) (Config, error) {
Expand Down Expand Up @@ -659,6 +666,11 @@ func ReadConfig(opts servertypes.AppOptions) (Config, error) {
return cfg, fmt.Errorf("%s must be >= 0 (0 disables the limit), got %d", flagMaxOpenConnections, cfg.MaxOpenConnections)
}
}
if v := opts.Get(flagBodyReadIdleTimeout); v != nil {
if cfg.BodyReadIdleTimeout, err = cast.ToDurationE(v); err != nil {
return cfg, err
}
}
return cfg, nil
}

Expand Down Expand Up @@ -922,11 +934,17 @@ batch_response_max_size = {{ .EVM.BatchResponseMaxSize }}
max_request_body_bytes = {{ .EVM.MaxRequestBodyBytes }}

# max_concurrent_request_bytes bounds the total size, in bytes, of HTTP JSON-RPC
# request bodies admitted for processing concurrently (weighted by each request's
# Content-Length). Requests that would exceed the budget are rejected fast
# (HTTP 429) before decode, capping peak memory under load. Set to 0 to disable.
# request bodies admitted for processing concurrently, charged incrementally as
# body bytes are read. Requests that would exceed the budget mid-read are
# rejected (HTTP 429). Set to 0 to disable.
max_concurrent_request_bytes = {{ .EVM.MaxConcurrentRequestBytes }}

# body_read_idle_timeout is the maximum idle time allowed between body chunks
# while reading an HTTP JSON-RPC request. Stalled body reads return HTTP 408 and
# release any byte budget held so far. Set to 0 to disable (ReadTimeout remains
# the whole-request backstop).
body_read_idle_timeout = "{{ .EVM.BodyReadIdleTimeout }}"

# max_open_connections caps the number of simultaneously accepted connections on
# the EVM HTTP and WebSocket listeners. Set to 0 to disable the limit.
max_open_connections = {{ .EVM.MaxOpenConnections }}
Expand Down
1 change: 1 addition & 0 deletions evmrpc/config/config_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ var evmKeys = []configtest.KeySpec{
{Key: "evm.batch_response_max_size", Path: "BatchResponseMaxSize", Cast: configtest.CastInt, Checked: true},
{Key: "evm.max_request_body_bytes", Path: "MaxRequestBodyBytes", Cast: configtest.CastInt64, Checked: true},
{Key: "evm.max_concurrent_request_bytes", Path: "MaxConcurrentRequestBytes", Cast: configtest.CastInt64, Checked: true},
{Key: "evm.body_read_idle_timeout", Path: "BodyReadIdleTimeout", Cast: configtest.CastDuration, Checked: true},
}

func readEVM(opts configtest.AppOpts) (any, error) { return config.ReadConfig(opts) }
Expand Down
5 changes: 5 additions & 0 deletions evmrpc/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type opts struct {
maxRequestBodyBytes interface{}
maxConcurrentRequestBytes interface{}
maxOpenConnections interface{}
bodyReadIdleTimeout interface{}
maxTraceStructLogBytes interface{}
traceAllowedTracers interface{}
traceAllowJSTracers interface{}
Expand Down Expand Up @@ -192,6 +193,9 @@ func (o *opts) Get(k string) interface{} {
if k == "evm.max_open_connections" {
return o.maxOpenConnections
}
if k == "evm.body_read_idle_timeout" {
return o.bodyReadIdleTimeout
}
if k == "evm.max_trace_struct_log_bytes" {
return o.maxTraceStructLogBytes
}
Expand Down Expand Up @@ -253,6 +257,7 @@ func getDefaultOpts() opts {
int64(5 * 1024 * 1024),
int64(128 * 1024 * 1024),
2000,
10 * time.Second,
uint64(256 * 1024 * 1024),
[]string{"callTracer", "prestateTracer"},
false,
Expand Down
1 change: 1 addition & 0 deletions evmrpc/config/testdata/evm.golden
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@ BatchRequestLimit = int(1000)
BatchResponseMaxSize = int(25000000)
MaxRequestBodyBytes = int64(5242880)
MaxConcurrentRequestBytes = int64(134217728)
BodyReadIdleTimeout = time.Duration(10s)
MaxOpenConnections = int(2000)
7 changes: 4 additions & 3 deletions evmrpc/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ const (
jsonrpcCodeKey = "jsonrpc_code"
rejectReasonKey = "reason"
// reject reason values for requestRejectedCount.
rejectReasonOversize = "oversize" // body exceeded max_request_body_bytes
rejectReasonBusy = "busy" // max_concurrent_request_bytes budget exhausted
rejectReasonOversize = "oversize" // body exceeded max_request_body_bytes
rejectReasonBudgetMidread = "budget_midread" // global byte budget exhausted mid-body read
rejectReasonSlowBody = "slow_body" // body read idle timeout exceeded
// error_class values; empty string ("") means success.
errorClassPanic = "panic"
errorClassExecutionReverted = "execution_reverted"
Expand Down Expand Up @@ -167,7 +168,7 @@ func recordHistoricalDebugTraceAttempt(ctx context.Context, endpoint, connection
}

// recordRequestRejected counts an HTTP JSON-RPC request dropped by pre-decode
// admission control. reason is one of rejectReasonOversize / rejectReasonBusy.
// admission control. reason is one of rejectReasonOversize / rejectReasonBudgetMidread / rejectReasonSlowBody.
// No endpoint dimension is recorded: the rejection happens before the JSON-RPC
// method is decoded, so it is not yet known.
func recordRequestRejected(ctx context.Context, reason string) {
Expand Down
Loading
Loading