diff --git a/cmd/frozen-rpc-router/config.go b/cmd/frozen-rpc-router/config.go index d2c56f6abe..03ba0c1a5f 100644 --- a/cmd/frozen-rpc-router/config.go +++ b/cmd/frozen-rpc-router/config.go @@ -12,17 +12,19 @@ import ( ) const ( - defaultListenAddress = "127.0.0.1:8545" - defaultMaxRequestBodySize = int64(5 << 20) - defaultShutdownTimeout = 10 * time.Second + defaultListenAddress = "127.0.0.1:8545" + defaultMaxRequestBodySize = int64(5 << 20) + defaultMaxBlockReferenceDepth = 16 + defaultShutdownTimeout = 10 * time.Second ) type config struct { - listenAddress string - liveNode string - frozenNodes frozenNodeFlags - maxRequestBodySize int64 - shutdownTimeout time.Duration + listenAddress string + liveNode string + frozenNodes frozenNodeFlags + maxRequestBodySize int64 + maxBlockReferenceDepth int + shutdownTimeout time.Duration } type frozenNodeConfig struct { @@ -49,6 +51,7 @@ func parseConfig(args []string, output io.Writer) (config, error) { flags.StringVar(&cfg.liveNode, "live-node", "", "HTTP RPC address of the live node (required)") flags.Var(&cfg.frozenNodes, "frozen-node", "freeze-height=ip:port pair; repeat once per frozen node") flags.Int64Var(&cfg.maxRequestBodySize, "max-request-body-bytes", defaultMaxRequestBodySize, "maximum JSON-RPC request body size") + flags.IntVar(&cfg.maxBlockReferenceDepth, "max-block-reference-depth", defaultMaxBlockReferenceDepth, "maximum nested block reference depth") flags.DurationVar(&cfg.shutdownTimeout, "shutdown-timeout", defaultShutdownTimeout, "graceful shutdown timeout") if err := flags.Parse(args); err != nil { return config{}, err @@ -62,6 +65,9 @@ func parseConfig(args []string, output io.Writer) (config, error) { if cfg.maxRequestBodySize <= 0 { return config{}, errors.New("--max-request-body-bytes must be positive") } + if cfg.maxBlockReferenceDepth <= 0 { + return config{}, errors.New("--max-block-reference-depth must be positive") + } if cfg.shutdownTimeout <= 0 { return config{}, errors.New("--shutdown-timeout must be positive") } diff --git a/cmd/frozen-rpc-router/config_test.go b/cmd/frozen-rpc-router/config_test.go index f15cf6c741..f7151208ec 100644 --- a/cmd/frozen-rpc-router/config_test.go +++ b/cmd/frozen-rpc-router/config_test.go @@ -13,10 +13,12 @@ func TestParseConfig(t *testing.T) { "--live-node", "localhost:8545", "--frozen-node", "200=localhost:8547", "--frozen-node", "100=localhost:8546", + "--max-block-reference-depth", "32", }, io.Discard) require.NoError(t, err) require.Equal(t, "0.0.0.0:9000", cfg.listenAddress) require.Equal(t, "localhost:8545", cfg.liveNode) + require.Equal(t, 32, cfg.maxBlockReferenceDepth) nodes, err := parseFrozenNodes(cfg.frozenNodes) require.NoError(t, err) @@ -31,6 +33,11 @@ func TestParseConfigRejectsMissingLiveNode(t *testing.T) { require.EqualError(t, err, "--live-node is required") } +func TestParseConfigRejectsNonPositiveBlockReferenceDepth(t *testing.T) { + _, err := parseConfig([]string{"--live-node", "localhost:8545", "--max-block-reference-depth", "0"}, io.Discard) + require.EqualError(t, err, "--max-block-reference-depth must be positive") +} + func TestParseFrozenNodesRejectsInvalidPairs(t *testing.T) { for _, value := range []string{"100", "=localhost:8545", "0=localhost:8545", "abc=localhost:8545", "9223372036854775808=localhost:8545", "100="} { t.Run(value, func(t *testing.T) { diff --git a/cmd/frozen-rpc-router/main.go b/cmd/frozen-rpc-router/main.go index eb4f70072d..a18ab64868 100644 --- a/cmd/frozen-rpc-router/main.go +++ b/cmd/frozen-rpc-router/main.go @@ -32,7 +32,7 @@ func run() error { if err != nil { return err } - router, err := newRouter(cfg.liveNode, frozenNodes, nil, cfg.maxRequestBodySize) + router, err := newRouter(cfg.liveNode, frozenNodes, nil, cfg.maxRequestBodySize, cfg.maxBlockReferenceDepth) if err != nil { return err } diff --git a/cmd/frozen-rpc-router/router.go b/cmd/frozen-rpc-router/router.go index 226c3df69c..2cc6f58ca2 100644 --- a/cmd/frozen-rpc-router/router.go +++ b/cmd/frozen-rpc-router/router.go @@ -49,11 +49,12 @@ var blockParameterIndexes = map[string]int{ } type router struct { - live *upstream - frozen []*upstream - client *http.Client - maxRequestBodySize int64 - liveProxy *httputil.ReverseProxy + live *upstream + frozen []*upstream + client *http.Client + maxRequestBodySize int64 + maxBlockReferenceDepth int + liveProxy *httputil.ReverseProxy } type upstream struct { @@ -94,7 +95,7 @@ type batchGroup struct { err error } -func newRouter(liveAddress string, frozenConfigs []frozenNodeConfig, client *http.Client, maxRequestBodySize int64) (*router, error) { +func newRouter(liveAddress string, frozenConfigs []frozenNodeConfig, client *http.Client, maxRequestBodySize int64, maxBlockReferenceDepth int) (*router, error) { liveURL, err := parseEndpoint(liveAddress) if err != nil { return nil, fmt.Errorf("invalid live node: %w", err) @@ -105,6 +106,9 @@ func newRouter(liveAddress string, frozenConfigs []frozenNodeConfig, client *htt if maxRequestBodySize <= 0 { return nil, errors.New("maximum request body size must be positive") } + if maxBlockReferenceDepth <= 0 { + return nil, errors.New("maximum block reference depth must be positive") + } live := &upstream{endpoint: liveURL} frozen := make([]*upstream, 0, len(frozenConfigs)) @@ -130,11 +134,12 @@ func newRouter(liveAddress string, frozenConfigs []frozenNodeConfig, client *htt liveProxy := httputil.NewSingleHostReverseProxy(liveURL) liveProxy.Transport = client.Transport return &router{ - live: live, - frozen: frozen, - client: client, - maxRequestBodySize: maxRequestBodySize, - liveProxy: liveProxy, + live: live, + frozen: frozen, + client: client, + maxRequestBodySize: maxRequestBodySize, + maxBlockReferenceDepth: maxBlockReferenceDepth, + liveProxy: liveProxy, }, nil } @@ -377,7 +382,7 @@ func (r *router) route(call rpcCall) (*upstream, *rpcError) { if !ok { return r.live, nil } - return r.upstreamForReference(parseBlockReference(parameter)), nil + return r.upstreamForReference(r.parseBlockReference(parameter)), nil } } @@ -403,10 +408,10 @@ func (r *router) routeGetLogs(params json.RawMessage) (*upstream, *rpcError) { from := blockReference{live: true} to := blockReference{live: true} if hasFrom { - from = parseBlockReference(fromRaw) + from = r.parseBlockReference(fromRaw) } if hasTo { - to = parseBlockReference(toRaw) + to = r.parseBlockReference(toRaw) } return r.routeRange(from, to) } @@ -421,7 +426,7 @@ func (r *router) routeFeeHistory(params json.RawMessage) (*upstream, *rpcError) if !ok { return r.live, nil } - last := parseBlockReference(lastRaw) + last := r.parseBlockReference(lastRaw) if !last.known || last.live { return r.live, nil } @@ -466,38 +471,45 @@ func (r *router) upstreamForReference(reference blockReference) *upstream { return r.live } -func parseBlockReference(raw json.RawMessage) blockReference { - trimmed := bytes.TrimSpace(raw) - if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { - return blockReference{} - } - if trimmed[0] == '{' { - var object map[string]json.RawMessage - if json.Unmarshal(trimmed, &object) != nil { +func (r *router) parseBlockReference(raw json.RawMessage) blockReference { + for depth := 0; depth <= r.maxBlockReferenceDepth; depth++ { + trimmed := bytes.TrimSpace(raw) + if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { return blockReference{} } - blockNumber, ok := object["blockNumber"] - if !ok { - return blockReference{} + if trimmed[0] == '{' { + if depth == r.maxBlockReferenceDepth { + return blockReference{} + } + var object map[string]json.RawMessage + if json.Unmarshal(trimmed, &object) != nil { + return blockReference{} + } + blockNumber, ok := object["blockNumber"] + if !ok { + return blockReference{} + } + raw = blockNumber + continue } - return parseBlockReference(blockNumber) - } - var value string - if json.Unmarshal(trimmed, &value) != nil { - return blockReference{} - } - switch value { - case "earliest": - return blockReference{height: 0, known: true} - case "latest", "pending", "safe", "finalized": - return blockReference{live: true} - } - height, err := strconv.ParseUint(strings.TrimPrefix(value, "0x"), 16, 64) - if err != nil || !strings.HasPrefix(value, "0x") || height > math.MaxInt64 { - return blockReference{} + var value string + if json.Unmarshal(trimmed, &value) != nil { + return blockReference{} + } + switch value { + case "earliest": + return blockReference{height: 0, known: true} + case "latest", "pending", "safe", "finalized": + return blockReference{live: true} + } + height, err := strconv.ParseUint(strings.TrimPrefix(value, "0x"), 16, 64) + if err != nil || !strings.HasPrefix(value, "0x") || height > math.MaxInt64 { + return blockReference{} + } + return blockReference{height: height, known: true} } - return blockReference{height: height, known: true} + return blockReference{} } func parseQuantity(raw json.RawMessage) (uint64, bool) { diff --git a/cmd/frozen-rpc-router/router_test.go b/cmd/frozen-rpc-router/router_test.go index 9c5960cf14..dd592d3a2c 100644 --- a/cmd/frozen-rpc-router/router_test.go +++ b/cmd/frozen-rpc-router/router_test.go @@ -46,6 +46,18 @@ func TestRouteBlockParameters(t *testing.T) { } } +func TestParseBlockReferenceDepthLimit(t *testing.T) { + r := &router{maxBlockReferenceDepth: 2} + raw := json.RawMessage(`"0x64"`) + for range r.maxBlockReferenceDepth { + raw = json.RawMessage(`{"blockNumber":` + string(raw) + `}`) + } + require.Equal(t, blockReference{height: 100, known: true}, r.parseBlockReference(raw)) + + raw = json.RawMessage(`{"blockNumber":` + string(raw) + `}`) + require.Equal(t, blockReference{}, r.parseBlockReference(raw)) +} + func TestRouteRanges(t *testing.T) { r := newTestRouter(t) testCases := []struct { @@ -87,7 +99,7 @@ func TestRouteRanges(t *testing.T) { func TestRouterForwardsSingleRequest(t *testing.T) { live := newRPCBackend(t, "live") frozen := newRPCBackend(t, "frozen") - r, err := newRouter(live.server.URL, []frozenNodeConfig{{freezeHeight: 100, address: frozen.server.URL}}, live.server.Client(), defaultMaxRequestBodySize) + r, err := newRouter(live.server.URL, []frozenNodeConfig{{freezeHeight: 100, address: frozen.server.URL}}, live.server.Client(), defaultMaxRequestBodySize, defaultMaxBlockReferenceDepth) require.NoError(t, err) recorder := httptest.NewRecorder() @@ -109,7 +121,7 @@ func TestRouterSplitsMixedBatch(t *testing.T) { r, err := newRouter(live.server.URL, []frozenNodeConfig{ {freezeHeight: 200, address: frozen200.server.URL}, {freezeHeight: 100, address: frozen100.server.URL}, - }, live.server.Client(), defaultMaxRequestBodySize) + }, live.server.Client(), defaultMaxRequestBodySize, defaultMaxBlockReferenceDepth) require.NoError(t, err) body := `[ @@ -162,7 +174,7 @@ func TestRouterOmitsErrorForUnsupportedNotification(t *testing.T) { } func TestRouterRejectsOversizedRequest(t *testing.T) { - r, err := newRouter("live:8545", nil, nil, 8) + r, err := newRouter("live:8545", nil, nil, 8, defaultMaxBlockReferenceDepth) require.NoError(t, err) recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodPost, "http://router/", bytes.NewReader([]byte("123456789"))) @@ -178,8 +190,11 @@ func TestNewRouterSortsAndValidatesFrozenNodes(t *testing.T) { _, err := newRouter("live:8545", []frozenNodeConfig{ {freezeHeight: 100, address: "one:8545"}, {freezeHeight: 100, address: "two:8545"}, - }, nil, defaultMaxRequestBodySize) + }, nil, defaultMaxRequestBodySize, defaultMaxBlockReferenceDepth) require.EqualError(t, err, "duplicate freeze height 100") + + _, err = newRouter("live:8545", nil, nil, defaultMaxRequestBodySize, 0) + require.EqualError(t, err, "maximum block reference depth must be positive") } func newTestRouter(t *testing.T) *router { @@ -187,7 +202,7 @@ func newTestRouter(t *testing.T) *router { r, err := newRouter("live:8545", []frozenNodeConfig{ {freezeHeight: 200, address: "frozen-200:8545"}, {freezeHeight: 100, address: "frozen-100:8545"}, - }, nil, defaultMaxRequestBodySize) + }, nil, defaultMaxRequestBodySize, defaultMaxBlockReferenceDepth) require.NoError(t, err) return r }