Skip to content
Merged
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
22 changes: 14 additions & 8 deletions cmd/frozen-rpc-router/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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")
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/frozen-rpc-router/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/frozen-rpc-router/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
96 changes: 54 additions & 42 deletions cmd/frozen-rpc-router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
}

Expand All @@ -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)
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down
25 changes: 20 additions & 5 deletions cmd/frozen-rpc-router/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand All @@ -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 := `[
Expand Down Expand Up @@ -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")))
Expand All @@ -178,16 +190,19 @@ 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 {
t.Helper()
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
}
Expand Down
Loading