From 1afa8c85c9c4957ac5e5f928d1b5da4c6fe35305 Mon Sep 17 00:00:00 2001 From: Robert Pirtle Date: Mon, 4 Mar 2024 11:40:45 -0800 Subject: [PATCH] add unit tests for shard routing proxies --- .env | 1 + service/proxy_test.go | 22 +++-- service/service_test.go | 8 +- service/shard_test.go | 185 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 40d7a5e..5bda30c 100644 --- a/.env +++ b/.env @@ -43,6 +43,7 @@ TEST_DATABASE_ENDPOINT_URL=localhost:5432 TEST_PROXY_BACKEND_HOST_URL_MAP=localhost:7777>http://kava-validator:8545,localhost:7778>http://kava-pruning:8545 TEST_PROXY_HEIGHT_BASED_ROUTING_ENABLED=true TEST_PROXY_PRUNING_BACKEND_HOST_URL_MAP=localhost:7777>http://kava-pruning:8545,localhost:7778>http://kava-pruning:8545 +TEST_PROXY_SHARD_BACKEND_HOST_URL_MAP=localhost:7777>10|http://kava-shard-10:8545|20|http://kava-shard-20:8545 # What level of logging to use for service objects constructed during # unit tests TEST_SERVICE_LOG_LEVEL=ERROR diff --git a/service/proxy_test.go b/service/proxy_test.go index 66bf507..fb5d2f2 100644 --- a/service/proxy_test.go +++ b/service/proxy_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/require" ) -func newConfig(t *testing.T, defaultHostMap string, pruningHostMap string) config.Config { +func newConfig(t *testing.T, defaultHostMap string, pruningHostMap string, shardHostMap string) config.Config { parsed, err := config.ParseRawProxyBackendHostURLMap(defaultHostMap) require.NoError(t, err) result := config.Config{ @@ -27,27 +27,39 @@ func newConfig(t *testing.T, defaultHostMap string, pruningHostMap string) confi result.ProxyPruningBackendHostURLMap, err = config.ParseRawProxyBackendHostURLMap(pruningHostMap) require.NoError(t, err) } + if shardHostMap != "" { + result.EnableShardedRouting = true + result.ProxyShardBackendHostURLMapRaw = shardHostMap + result.ProxyShardBackendHostURLMap, err = config.ParseRawShardRoutingBackendHostURLMap(shardHostMap) + require.NoError(t, err) + } return result } func TestUnitTest_NewProxies(t *testing.T) { t.Run("returns a HostProxies when sharding disabled", func(t *testing.T) { - config := newConfig(t, dummyConfig.ProxyBackendHostURLMapRaw, "") + config := newConfig(t, dummyConfig.ProxyBackendHostURLMapRaw, "", "") proxies := service.NewProxies(config, dummyLogger) require.IsType(t, service.HostProxies{}, proxies) }) - t.Run("returns a PruningOrDefaultProxies when sharding enabled", func(t *testing.T) { - config := newConfig(t, dummyConfig.ProxyBackendHostURLMapRaw, dummyConfig.ProxyPruningBackendHostURLMapRaw) + t.Run("returns a PruningOrDefaultProxies when height-based routing enabled", func(t *testing.T) { + config := newConfig(t, dummyConfig.ProxyBackendHostURLMapRaw, dummyConfig.ProxyPruningBackendHostURLMapRaw, "") proxies := service.NewProxies(config, dummyLogger) require.IsType(t, service.PruningOrDefaultProxies{}, proxies) }) + + t.Run("returns a ShardProxies when sharding enabled", func(t *testing.T) { + config := newConfig(t, dummyConfig.ProxyBackendHostURLMapRaw, "", dummyConfig.ProxyShardBackendHostURLMapRaw) + proxies := service.NewProxies(config, dummyLogger) + require.IsType(t, service.ShardProxies{}, proxies) + }) } func TestUnitTest_HostProxies(t *testing.T) { config := newConfig(t, "magic.kava.io>magicalbackend.kava.io,archive.kava.io>archivenode.kava.io,pruning.kava.io>pruningnode.kava.io", - "", + "", "", ) proxies := service.NewProxies(config, dummyLogger) diff --git a/service/service_test.go b/service/service_test.go index 9733509..bd2d005 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -15,6 +15,7 @@ var ( testDefaultContext = context.TODO() proxyServiceDefaultURLMapRaw = os.Getenv("TEST_PROXY_BACKEND_HOST_URL_MAP") proxyServicePruningURLMapRaw = os.Getenv("TEST_PROXY_PRUNING_BACKEND_HOST_URL_MAP") + proxyServiceShardURLMapRaw = os.Getenv("TEST_PROXY_SHARD_BACKEND_HOST_URL_MAP") databaseName = os.Getenv("DATABASE_NAME") databaseUsername = os.Getenv("DATABASE_USERNAME") databasePassword = os.Getenv("DATABASE_PASSWORD") @@ -23,7 +24,6 @@ var ( evmQueryServiceURL = os.Getenv("TEST_EVM_QUERY_SERVICE_URL") dummyConfig = func() config.Config { - proxyBackendHostURLMapParsed, err := config.ParseRawProxyBackendHostURLMap(proxyServiceDefaultURLMapRaw) if err != nil { panic(err) @@ -32,12 +32,18 @@ var ( if err != nil { panic(err) } + proxyShardBackendHostURLMapParsed, err := config.ParseRawShardRoutingBackendHostURLMap(proxyServiceShardURLMapRaw) + if err != nil { + panic(err) + } conf := config.Config{ ProxyBackendHostURLMapRaw: proxyServiceDefaultURLMapRaw, ProxyBackendHostURLMapParsed: proxyBackendHostURLMapParsed, ProxyPruningBackendHostURLMapRaw: proxyServicePruningURLMapRaw, ProxyPruningBackendHostURLMap: proxyPruningBackendHostURLMapParsed, + ProxyShardBackendHostURLMapRaw: proxyServiceShardURLMapRaw, + ProxyShardBackendHostURLMap: proxyShardBackendHostURLMapParsed, DatabaseName: databaseName, DatabaseUserName: databaseUsername, diff --git a/service/shard_test.go b/service/shard_test.go index 0fc5f58..60e36ff 100644 --- a/service/shard_test.go +++ b/service/shard_test.go @@ -15,8 +15,10 @@ func TestUnitTest_PruningOrDefaultProxies(t *testing.T) { config := newConfig(t, fmt.Sprintf("archive.kava.io>%s,pruning.kava.io>%s", archiveBackend, pruningBackend), fmt.Sprintf("archive.kava.io>%s", pruningBackend), + "", ) proxies := service.NewProxies(config, dummyLogger) + require.IsType(t, service.PruningOrDefaultProxies{}, proxies) testCases := []struct { name string @@ -160,3 +162,186 @@ func TestUnitTest_PruningOrDefaultProxies(t *testing.T) { }) } } + +// shard proxies with a pruning underlying proxy expects the same as above +// except that requests for specific heights that fall within a shard route to that shard. +func TestUnitTest_ShardProxies(t *testing.T) { + archiveBackend := "archivenode.kava.io/" + pruningBackend := "pruningnode.kava.io/" + shard1Backend := "shard-1.kava.io/" + shard2Backend := "shard-2.kava.io/" + config := newConfig(t, + fmt.Sprintf("archive.kava.io>%s,pruning.kava.io>%s", archiveBackend, pruningBackend), + fmt.Sprintf("archive.kava.io>%s", pruningBackend), + fmt.Sprintf("archive.kava.io>10|%s|20|%s", shard1Backend, shard2Backend), + ) + proxies := service.NewProxies(config, dummyLogger) + require.IsType(t, service.ShardProxies{}, proxies) + + testCases := []struct { + name string + url string + req *decode.EVMRPCRequestEnvelope + expectFound bool + expectBackend string + expectRoute string + }{ + // DEFAULT ROUTE CASES + { + name: "routes to default when not in pruning or shard map", + url: "//pruning.kava.io", + req: &decode.EVMRPCRequestEnvelope{}, + expectFound: true, + expectBackend: service.ResponseBackendDefault, + expectRoute: pruningBackend, + }, + { + name: "routes to default for specific height beyond latest shard", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_getBlockByNumber", + Params: []interface{}{"0xbaddad", false}, + }, + expectFound: true, + expectBackend: service.ResponseBackendDefault, + expectRoute: archiveBackend, + }, + { + name: "routes to default for methods that don't have block number", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_getBlockByHash", + Params: []interface{}{"0xe9bd10bc1d62b4406dd1fb3dbf3adb54f640bdb9ebbe3dd6dfc6bcc059275e54", false}, + }, + expectFound: true, + expectBackend: service.ResponseBackendDefault, + expectRoute: archiveBackend, + }, + { + name: "routes to default if it fails to decode req", + url: "//archive.kava.io", + req: nil, + expectFound: true, + expectBackend: service.ResponseBackendDefault, + expectRoute: archiveBackend, + }, + { + name: "routes to default if it fails to parse block number", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_getBlockByNumber", + Params: []interface{}{"not-a-block-tag", false}, + }, + expectFound: true, + expectBackend: service.ResponseBackendDefault, + expectRoute: archiveBackend, + }, + { + // TODO: should it do this? if shards exist, route to first shard? + name: "routes to default for 'earliest' block", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_getBlockByNumber", + Params: []interface{}{"earliest", false}, + }, + expectFound: true, + expectBackend: service.ResponseBackendDefault, + expectRoute: archiveBackend, + }, + + // PRUNING ROUTE CASES + { + name: "routes to pruning for 'latest' block", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_getBlockByNumber", + Params: []interface{}{"latest", false}, + }, + expectFound: true, + expectBackend: service.ResponseBackendPruning, + expectRoute: pruningBackend, + }, + { + name: "routes to pruning when block number empty", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_getBlockByNumber", + Params: []interface{}{nil, false}, + }, + expectFound: true, + expectBackend: service.ResponseBackendPruning, + expectRoute: pruningBackend, + }, + { + name: "routes to pruning for no-history methods", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_chainId", + }, + expectFound: true, + expectBackend: service.ResponseBackendPruning, + expectRoute: pruningBackend, + }, + { + // this is just another example of the above, but worth pointing out! + name: "routes to pruning when sending txs", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_sendTransaction", + Params: []interface{}{ + map[string]string{ + "from": "0xdeadbeef00000000000000000000000000000123", + "to": "0xbaddad0000000000000000000000000000000123", + "value": "0x1", + "gas": "0xeeee", + "gasPrice": "0x12345678900", + "nonce": "0x0", + }, + }, + }, + expectFound: true, + expectBackend: service.ResponseBackendPruning, + expectRoute: pruningBackend, + }, + + // SHARD ROUTE CASES + { + name: "routes to shard 1 for specific height in shard 1", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_getBlockByNumber", + Params: []interface{}{"0x5", false}, // block 5 + }, + expectFound: true, + expectBackend: service.ResponseBackendShard, + expectRoute: shard1Backend, + }, + { + name: "routes to shard 2 for specific height in shard 2", + url: "//archive.kava.io", + req: &decode.EVMRPCRequestEnvelope{ + Method: "eth_getBlockByNumber", + Params: []interface{}{"0xF", false}, // block 15 + }, + expectFound: true, + expectBackend: service.ResponseBackendShard, + expectRoute: shard2Backend, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := mockJsonRpcReqToUrl(tc.url, tc.req) + proxy, metadata, found := proxies.ProxyForRequest(req) + if !tc.expectFound { + require.False(t, found, "expected proxy not to be found") + return + } + require.True(t, found, "expected proxy to be found") + require.NotNil(t, proxy) + require.Equal(t, metadata.BackendName, tc.expectBackend) + require.Equal(t, metadata.BackendRoute.String(), tc.expectRoute) + requireProxyRoutesToUrl(t, proxy, req, tc.expectRoute) + }) + } +}