Skip to content

Commit f3314bb

Browse files
mmsqefjlholiman
authored
rpc: add limit for batch request items and response size (#26681)
This PR adds server-side limits for JSON-RPC batch requests. Before this change, batches were limited only by processing time. The server would pick calls from the batch and answer them until the response timeout occurred, then stop processing the remaining batch items. Here, we are adding two additional limits which can be configured: - the 'item limit': batches can have at most N items - the 'response size limit': batches can contain at most X response bytes These limits are optional in package rpc. In Geth, we set a default limit of 1000 items and 25MB response size. When a batch goes over the limit, an error response is returned to the client. However, doing this correctly isn't always possible. In JSON-RPC, only method calls with a valid `id` can be responded to. Since batches may also contain non-call messages or notifications, the best effort thing we can do to report an error with the batch itself is reporting the limit violation as an error for the first method call in the batch. If a batch is too large, but contains only notifications and responses, the error will be reported with a null `id`. The RPC client was also changed so it can deal with errors resulting from too large batches. An older client connected to the server code in this PR could get stuck until the request timeout occurred when the batch is too large. **Upgrading to a version of the RPC client containing this change is strongly recommended to avoid timeout issues.** For some weird reason, when writing the original client implementation, @fjl worked off of the assumption that responses could be distributed across batches arbitrarily. So for a batch request containing requests `[A B C]`, the server could respond with `[A B C]` but also with `[A B] [C]` or even `[A] [B] [C]` and it wouldn't make a difference to the client. So in the implementation of BatchCallContext, the client waited for all requests in the batch individually. If the server didn't respond to some of the requests in the batch, the client would eventually just time out (if a context was used). With the addition of batch limits into the server, we anticipate that people will hit this kind of error way more often. To handle this properly, the client now waits for a single response batch and expects it to contain all responses to the requests. --------- Co-authored-by: Felix Lange <[email protected]> Co-authored-by: Martin Holst Swende <[email protected]>
1 parent 5ac4da3 commit f3314bb

22 files changed

+554
-226
lines changed

cmd/clef/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,7 @@ func signer(c *cli.Context) error {
732732
cors := utils.SplitAndTrim(c.String(utils.HTTPCORSDomainFlag.Name))
733733

734734
srv := rpc.NewServer()
735+
srv.SetBatchLimits(node.DefaultConfig.BatchRequestLimit, node.DefaultConfig.BatchResponseMaxSize)
735736
err := node.RegisterApis(rpcAPI, []string{"account"}, srv)
736737
if err != nil {
737738
utils.Fatalf("Could not register API: %w", err)

cmd/geth/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ var (
168168
utils.RPCGlobalEVMTimeoutFlag,
169169
utils.RPCGlobalTxFeeCapFlag,
170170
utils.AllowUnprotectedTxs,
171+
utils.BatchRequestLimit,
172+
utils.BatchResponseMaxSize,
171173
}
172174

173175
metricsFlags = []cli.Flag{

cmd/utils/flags.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,18 @@ var (
713713
Usage: "Allow for unprotected (non EIP155 signed) transactions to be submitted via RPC",
714714
Category: flags.APICategory,
715715
}
716+
BatchRequestLimit = &cli.IntFlag{
717+
Name: "rpc.batch-request-limit",
718+
Usage: "Maximum number of requests in a batch",
719+
Value: node.DefaultConfig.BatchRequestLimit,
720+
Category: flags.APICategory,
721+
}
722+
BatchResponseMaxSize = &cli.IntFlag{
723+
Name: "rpc.batch-response-max-size",
724+
Usage: "Maximum number of bytes returned from a batched call",
725+
Value: node.DefaultConfig.BatchResponseMaxSize,
726+
Category: flags.APICategory,
727+
}
716728
EnablePersonal = &cli.BoolFlag{
717729
Name: "rpc.enabledeprecatedpersonal",
718730
Usage: "Enables the (deprecated) personal namespace",
@@ -1130,6 +1142,14 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
11301142
if ctx.IsSet(AllowUnprotectedTxs.Name) {
11311143
cfg.AllowUnprotectedTxs = ctx.Bool(AllowUnprotectedTxs.Name)
11321144
}
1145+
1146+
if ctx.IsSet(BatchRequestLimit.Name) {
1147+
cfg.BatchRequestLimit = ctx.Int(BatchRequestLimit.Name)
1148+
}
1149+
1150+
if ctx.IsSet(BatchResponseMaxSize.Name) {
1151+
cfg.BatchResponseMaxSize = ctx.Int(BatchResponseMaxSize.Name)
1152+
}
11331153
}
11341154

11351155
// setGraphQL creates the GraphQL listener interface string from the set

node/api.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ func (api *adminAPI) StartHTTP(host *string, port *int, cors *string, apis *stri
176176
CorsAllowedOrigins: api.node.config.HTTPCors,
177177
Vhosts: api.node.config.HTTPVirtualHosts,
178178
Modules: api.node.config.HTTPModules,
179+
rpcEndpointConfig: rpcEndpointConfig{
180+
batchItemLimit: api.node.config.BatchRequestLimit,
181+
batchResponseSizeLimit: api.node.config.BatchResponseMaxSize,
182+
},
179183
}
180184
if cors != nil {
181185
config.CorsAllowedOrigins = nil
@@ -250,6 +254,10 @@ func (api *adminAPI) StartWS(host *string, port *int, allowedOrigins *string, ap
250254
Modules: api.node.config.WSModules,
251255
Origins: api.node.config.WSOrigins,
252256
// ExposeAll: api.node.config.WSExposeAll,
257+
rpcEndpointConfig: rpcEndpointConfig{
258+
batchItemLimit: api.node.config.BatchRequestLimit,
259+
batchResponseSizeLimit: api.node.config.BatchResponseMaxSize,
260+
},
253261
}
254262
if apis != nil {
255263
config.Modules = nil

node/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ type Config struct {
197197
// AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC.
198198
AllowUnprotectedTxs bool `toml:",omitempty"`
199199

200+
// BatchRequestLimit is the maximum number of requests in a batch.
201+
BatchRequestLimit int `toml:",omitempty"`
202+
203+
// BatchResponseMaxSize is the maximum number of bytes returned from a batched rpc call.
204+
BatchResponseMaxSize int `toml:",omitempty"`
205+
200206
// JWTSecret is the path to the hex-encoded jwt secret.
201207
JWTSecret string `toml:",omitempty"`
202208

node/defaults.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,19 @@ var (
4646

4747
// DefaultConfig contains reasonable default settings.
4848
var DefaultConfig = Config{
49-
DataDir: DefaultDataDir(),
50-
HTTPPort: DefaultHTTPPort,
51-
AuthAddr: DefaultAuthHost,
52-
AuthPort: DefaultAuthPort,
53-
AuthVirtualHosts: DefaultAuthVhosts,
54-
HTTPModules: []string{"net", "web3"},
55-
HTTPVirtualHosts: []string{"localhost"},
56-
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
57-
WSPort: DefaultWSPort,
58-
WSModules: []string{"net", "web3"},
59-
GraphQLVirtualHosts: []string{"localhost"},
49+
DataDir: DefaultDataDir(),
50+
HTTPPort: DefaultHTTPPort,
51+
AuthAddr: DefaultAuthHost,
52+
AuthPort: DefaultAuthPort,
53+
AuthVirtualHosts: DefaultAuthVhosts,
54+
HTTPModules: []string{"net", "web3"},
55+
HTTPVirtualHosts: []string{"localhost"},
56+
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
57+
WSPort: DefaultWSPort,
58+
WSModules: []string{"net", "web3"},
59+
BatchRequestLimit: 1000,
60+
BatchResponseMaxSize: 25 * 1000 * 1000,
61+
GraphQLVirtualHosts: []string{"localhost"},
6062
P2P: p2p.Config{
6163
ListenAddr: ":30303",
6264
MaxPeers: 50,

node/node.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ func New(conf *Config) (*Node, error) {
101101
if strings.HasSuffix(conf.Name, ".ipc") {
102102
return nil, errors.New(`Config.Name cannot end in ".ipc"`)
103103
}
104-
104+
server := rpc.NewServer()
105+
server.SetBatchLimits(conf.BatchRequestLimit, conf.BatchResponseMaxSize)
105106
node := &Node{
106107
config: conf,
107-
inprocHandler: rpc.NewServer(),
108+
inprocHandler: server,
108109
eventmux: new(event.TypeMux),
109110
log: conf.Logger,
110111
stop: make(chan struct{}),
@@ -403,6 +404,11 @@ func (n *Node) startRPC() error {
403404
openAPIs, allAPIs = n.getAPIs()
404405
)
405406

407+
rpcConfig := rpcEndpointConfig{
408+
batchItemLimit: n.config.BatchRequestLimit,
409+
batchResponseSizeLimit: n.config.BatchResponseMaxSize,
410+
}
411+
406412
initHttp := func(server *httpServer, port int) error {
407413
if err := server.setListenAddr(n.config.HTTPHost, port); err != nil {
408414
return err
@@ -412,6 +418,7 @@ func (n *Node) startRPC() error {
412418
Vhosts: n.config.HTTPVirtualHosts,
413419
Modules: n.config.HTTPModules,
414420
prefix: n.config.HTTPPathPrefix,
421+
rpcEndpointConfig: rpcConfig,
415422
}); err != nil {
416423
return err
417424
}
@@ -425,9 +432,10 @@ func (n *Node) startRPC() error {
425432
return err
426433
}
427434
if err := server.enableWS(openAPIs, wsConfig{
428-
Modules: n.config.WSModules,
429-
Origins: n.config.WSOrigins,
430-
prefix: n.config.WSPathPrefix,
435+
Modules: n.config.WSModules,
436+
Origins: n.config.WSOrigins,
437+
prefix: n.config.WSPathPrefix,
438+
rpcEndpointConfig: rpcConfig,
431439
}); err != nil {
432440
return err
433441
}
@@ -441,26 +449,29 @@ func (n *Node) startRPC() error {
441449
if err := server.setListenAddr(n.config.AuthAddr, port); err != nil {
442450
return err
443451
}
452+
sharedConfig := rpcConfig
453+
sharedConfig.jwtSecret = secret
444454
if err := server.enableRPC(allAPIs, httpConfig{
445455
CorsAllowedOrigins: DefaultAuthCors,
446456
Vhosts: n.config.AuthVirtualHosts,
447457
Modules: DefaultAuthModules,
448458
prefix: DefaultAuthPrefix,
449-
jwtSecret: secret,
459+
rpcEndpointConfig: sharedConfig,
450460
}); err != nil {
451461
return err
452462
}
453463
servers = append(servers, server)
464+
454465
// Enable auth via WS
455466
server = n.wsServerForPort(port, true)
456467
if err := server.setListenAddr(n.config.AuthAddr, port); err != nil {
457468
return err
458469
}
459470
if err := server.enableWS(allAPIs, wsConfig{
460-
Modules: DefaultAuthModules,
461-
Origins: DefaultAuthOrigins,
462-
prefix: DefaultAuthPrefix,
463-
jwtSecret: secret,
471+
Modules: DefaultAuthModules,
472+
Origins: DefaultAuthOrigins,
473+
prefix: DefaultAuthPrefix,
474+
rpcEndpointConfig: sharedConfig,
464475
}); err != nil {
465476
return err
466477
}

node/rpcstack.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,21 @@ type httpConfig struct {
4141
CorsAllowedOrigins []string
4242
Vhosts []string
4343
prefix string // path prefix on which to mount http handler
44-
jwtSecret []byte // optional JWT secret
44+
rpcEndpointConfig
4545
}
4646

4747
// wsConfig is the JSON-RPC/Websocket configuration
4848
type wsConfig struct {
49-
Origins []string
50-
Modules []string
51-
prefix string // path prefix on which to mount ws handler
52-
jwtSecret []byte // optional JWT secret
49+
Origins []string
50+
Modules []string
51+
prefix string // path prefix on which to mount ws handler
52+
rpcEndpointConfig
53+
}
54+
55+
type rpcEndpointConfig struct {
56+
jwtSecret []byte // optional JWT secret
57+
batchItemLimit int
58+
batchResponseSizeLimit int
5359
}
5460

5561
type rpcHandler struct {
@@ -297,6 +303,7 @@ func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error {
297303

298304
// Create RPC server and handler.
299305
srv := rpc.NewServer()
306+
srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit)
300307
if err := RegisterApis(apis, config.Modules, srv); err != nil {
301308
return err
302309
}
@@ -328,6 +335,7 @@ func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error {
328335
}
329336
// Create RPC server and handler.
330337
srv := rpc.NewServer()
338+
srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit)
331339
if err := RegisterApis(apis, config.Modules, srv); err != nil {
332340
return err
333341
}

node/rpcstack_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,10 @@ func TestJWT(t *testing.T) {
339339
ss, _ := jwt.NewWithClaims(method, testClaim(input)).SignedString(secret)
340340
return ss
341341
}
342-
srv := createAndStartServer(t, &httpConfig{jwtSecret: []byte("secret")},
343-
true, &wsConfig{Origins: []string{"*"}, jwtSecret: []byte("secret")}, nil)
342+
cfg := rpcEndpointConfig{jwtSecret: []byte("secret")}
343+
httpcfg := &httpConfig{rpcEndpointConfig: cfg}
344+
wscfg := &wsConfig{Origins: []string{"*"}, rpcEndpointConfig: cfg}
345+
srv := createAndStartServer(t, httpcfg, true, wscfg, nil)
344346
wsUrl := fmt.Sprintf("ws://%v", srv.listenAddr())
345347
htUrl := fmt.Sprintf("http://%v", srv.listenAddr())
346348

0 commit comments

Comments
 (0)