batching_test.go 6.04 KB
Newer Older
1 2 3 4 5 6 7
package integration_tests

import (
	"net/http"
	"os"
	"testing"

8
	"github.com/ethereum-optimism/optimism/proxyd"
9 10 11 12 13 14 15 16 17 18 19 20
	"github.com/stretchr/testify/require"
)

func TestBatching(t *testing.T) {
	config := ReadConfig("batching")

	chainIDResponse1 := `{"jsonrpc": "2.0", "result": "hello1", "id": 1}`
	chainIDResponse2 := `{"jsonrpc": "2.0", "result": "hello2", "id": 2}`
	chainIDResponse3 := `{"jsonrpc": "2.0", "result": "hello3", "id": 3}`
	netVersionResponse1 := `{"jsonrpc": "2.0", "result": "1.0", "id": 1}`
	callResponse1 := `{"jsonrpc": "2.0", "result": "ekans1", "id": 1}`

21 22
	ethAccountsResponse2 := `{"jsonrpc": "2.0", "result": [], "id": 2}`

23 24 25
	backendResTooLargeResponse1 := `{"error":{"code":-32020,"message":"backend response too large"},"id":1,"jsonrpc":"2.0"}`
	backendResTooLargeResponse2 := `{"error":{"code":-32020,"message":"backend response too large"},"id":2,"jsonrpc":"2.0"}`

26 27 28 29 30 31 32 33 34 35 36 37 38
	type mockResult struct {
		method string
		id     string
		result interface{}
	}

	chainIDMock1 := mockResult{"eth_chainId", "1", "hello1"}
	chainIDMock2 := mockResult{"eth_chainId", "2", "hello2"}
	chainIDMock3 := mockResult{"eth_chainId", "3", "hello3"}
	netVersionMock1 := mockResult{"net_version", "1", "1.0"}
	callMock1 := mockResult{"eth_call", "1", "ekans1"}

	tests := []struct {
39 40 41 42 43 44 45
		name                 string
		handler              http.Handler
		mocks                []mockResult
		reqs                 []*proxyd.RPCReq
		expectedRes          string
		maxUpstreamBatchSize int
		numExpectedForwards  int
46
		maxResponseSizeBytes int64
47 48 49 50 51 52 53 54 55
	}{
		{
			name:  "backend returns batches out of order",
			mocks: []mockResult{chainIDMock1, chainIDMock2, chainIDMock3},
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "eth_chainId", nil),
				NewRPCReq("2", "eth_chainId", nil),
				NewRPCReq("3", "eth_chainId", nil),
			},
56 57 58
			expectedRes:          asArray(chainIDResponse1, chainIDResponse2, chainIDResponse3),
			maxUpstreamBatchSize: 2,
			numExpectedForwards:  2,
59 60 61 62 63 64 65 66 67 68 69 70 71
		},
		{
			// infura behavior
			name:    "backend returns single RPC response object as error",
			handler: SingleResponseHandler(500, `{"jsonrpc":"2.0","error":{"code":-32001,"message":"internal server error"},"id":1}`),
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "eth_chainId", nil),
				NewRPCReq("2", "eth_chainId", nil),
			},
			expectedRes: asArray(
				`{"error":{"code":-32011,"message":"no backends available for method"},"id":1,"jsonrpc":"2.0"}`,
				`{"error":{"code":-32011,"message":"no backends available for method"},"id":2,"jsonrpc":"2.0"}`,
			),
72 73
			maxUpstreamBatchSize: 10,
			numExpectedForwards:  1,
74 75 76 77 78 79 80 81 82 83 84 85
		},
		{
			name:    "backend returns single RPC response object for minibatches",
			handler: SingleResponseHandler(500, `{"jsonrpc":"2.0","error":{"code":-32001,"message":"internal server error"},"id":1}`),
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "eth_chainId", nil),
				NewRPCReq("2", "eth_chainId", nil),
			},
			expectedRes: asArray(
				`{"error":{"code":-32011,"message":"no backends available for method"},"id":1,"jsonrpc":"2.0"}`,
				`{"error":{"code":-32011,"message":"no backends available for method"},"id":2,"jsonrpc":"2.0"}`,
			),
86 87
			maxUpstreamBatchSize: 1,
			numExpectedForwards:  2,
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
		},
		{
			name: "duplicate request ids are on distinct batches",
			mocks: []mockResult{
				netVersionMock1,
				chainIDMock2,
				chainIDMock1,
				callMock1,
			},
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "net_version", nil),
				NewRPCReq("2", "eth_chainId", nil),
				NewRPCReq("1", "eth_chainId", nil),
				NewRPCReq("1", "eth_call", nil),
			},
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
			expectedRes:          asArray(netVersionResponse1, chainIDResponse2, chainIDResponse1, callResponse1),
			maxUpstreamBatchSize: 2,
			numExpectedForwards:  3,
		},
		{
			name:  "over max size",
			mocks: []mockResult{},
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "net_version", nil),
				NewRPCReq("2", "eth_chainId", nil),
				NewRPCReq("3", "eth_chainId", nil),
				NewRPCReq("4", "eth_call", nil),
				NewRPCReq("5", "eth_call", nil),
				NewRPCReq("6", "eth_call", nil),
			},
			expectedRes:          "{\"error\":{\"code\":-32014,\"message\":\"over batch size custom message\"},\"id\":null,\"jsonrpc\":\"2.0\"}",
			maxUpstreamBatchSize: 2,
			numExpectedForwards:  0,
121
		},
122 123 124 125 126 127 128 129 130
		{
			name: "eth_accounts does not get forwarded",
			mocks: []mockResult{
				callMock1,
			},
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "eth_call", nil),
				NewRPCReq("2", "eth_accounts", nil),
			},
131 132 133
			expectedRes:          asArray(callResponse1, ethAccountsResponse2),
			maxUpstreamBatchSize: 2,
			numExpectedForwards:  1,
134
		},
135 136 137 138 139 140 141 142 143 144 145 146
		{
			name:  "large upstream response gets dropped",
			mocks: []mockResult{chainIDMock1, chainIDMock2},
			reqs: []*proxyd.RPCReq{
				NewRPCReq("1", "eth_chainId", nil),
				NewRPCReq("2", "eth_chainId", nil),
			},
			expectedRes:          asArray(backendResTooLargeResponse1, backendResTooLargeResponse2),
			maxUpstreamBatchSize: 2,
			numExpectedForwards:  1,
			maxResponseSizeBytes: 1,
		},
147 148 149 150
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
151
			config.Server.MaxUpstreamBatchSize = tt.maxUpstreamBatchSize
152
			config.BackendOptions.MaxResponseSizeBytes = tt.maxResponseSizeBytes
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167

			handler := tt.handler
			if handler == nil {
				router := NewBatchRPCResponseRouter()
				for _, mock := range tt.mocks {
					router.SetRoute(mock.method, mock.id, mock.result)
				}
				handler = router
			}

			goodBackend := NewMockBackend(handler)
			defer goodBackend.Close()
			require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL", goodBackend.URL()))

			client := NewProxydClient("http://127.0.0.1:8545")
168
			_, shutdown, err := proxyd.Start(config)
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
			require.NoError(t, err)
			defer shutdown()

			res, statusCode, err := client.SendBatchRPC(tt.reqs...)
			require.NoError(t, err)
			require.Equal(t, http.StatusOK, statusCode)
			RequireEqualJSON(t, []byte(tt.expectedRes), res)

			if tt.numExpectedForwards != 0 {
				require.Equal(t, tt.numExpectedForwards, len(goodBackend.Requests()))
			}

			if handler, ok := handler.(*BatchRPCResponseRouter); ok {
				for i, mock := range tt.mocks {
					require.Equal(t, 1, handler.GetNumCalls(mock.method, mock.id), i)
				}
			}
		})
	}
}