From db0b0e79ce6a529de4a614851f8fab04a8c4f231 Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 11:37:15 +0300 Subject: [PATCH 01/16] feat: add NodeType config option --- internal/config/config.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index c7dfc8a7..56b20866 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,6 +8,13 @@ import ( "gopkg.in/yaml.v3" ) +type NodeType string + +const ( + Archive NodeType = "archive" + Full NodeType = "full" +) + type UpstreamConfig struct { BasicAuthConfig BasicAuthConfig `yaml:"basicAuth"` HealthCheckConfig HealthCheckConfig `yaml:"healthCheck"` @@ -15,6 +22,7 @@ type UpstreamConfig struct { HTTPURL string `yaml:"httpURL"` WSURL string `yaml:"wsURL"` GroupID string `yaml:"group"` + NodeType NodeType `yaml:"nodeType"` } func (c *UpstreamConfig) isValid(groups []GroupConfig) bool { From e11f94dfd599b34ded77fd471cff8cff2eaef5f2 Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 11:40:40 +0300 Subject: [PATCH 02/16] feat: add IsStatePresent filter for stateful requests --- internal/route/node_filter.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/route/node_filter.go b/internal/route/node_filter.go index 86cfa9d4..caf530d2 100644 --- a/internal/route/node_filter.go +++ b/internal/route/node_filter.go @@ -6,7 +6,9 @@ import ( "github.com/satsuma-data/node-gateway/internal/metadata" ) -type RequestMetadata struct{} +type RequestMetadata struct { + IsStateRequired bool +} type NodeFilter interface { Apply(requestMetadata *RequestMetadata, upstreamConfig *config.UpstreamConfig) bool @@ -65,6 +67,19 @@ func (f *IsAtMaxHeightForGroup) Apply(_ *RequestMetadata, upstreamConfig *config return upstreamStatus.BlockHeightCheck.IsPassing(maxHeightForGroup) } +type SimpleIsStatePresentFilter struct{} + +func (f *SimpleIsStatePresentFilter) Apply( + requestMetadata *RequestMetadata, + upstreamConfig *config.UpstreamConfig, +) bool { + if requestMetadata.IsStateRequired { + return upstreamConfig.NodeType == config.Archive + } + + return true +} + func CreateNodeFilter( filterNames []NodeFilterType, manager checks.HealthCheckManager, From 59fd145e9573abf6e9a4c14c8b1f72ab570fee8c Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 11:45:24 +0300 Subject: [PATCH 03/16] chore: move RequestMetadata to metadata package --- internal/metadata/request_metadata.go | 5 +++++ internal/route/node_filter.go | 16 ++++++---------- internal/route/node_filter_test.go | 7 ++++--- 3 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 internal/metadata/request_metadata.go diff --git a/internal/metadata/request_metadata.go b/internal/metadata/request_metadata.go new file mode 100644 index 00000000..c8278c18 --- /dev/null +++ b/internal/metadata/request_metadata.go @@ -0,0 +1,5 @@ +package metadata + +type RequestMetadata struct { + IsStateRequired bool +} diff --git a/internal/route/node_filter.go b/internal/route/node_filter.go index caf530d2..8dd1c460 100644 --- a/internal/route/node_filter.go +++ b/internal/route/node_filter.go @@ -6,19 +6,15 @@ import ( "github.com/satsuma-data/node-gateway/internal/metadata" ) -type RequestMetadata struct { - IsStateRequired bool -} - type NodeFilter interface { - Apply(requestMetadata *RequestMetadata, upstreamConfig *config.UpstreamConfig) bool + Apply(requestMetadata *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool } type AndFilter struct { filters []NodeFilter } -func (a *AndFilter) Apply(requestMetadata *RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { +func (a *AndFilter) Apply(requestMetadata *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { var result = true for filterIndex := range a.filters { @@ -36,7 +32,7 @@ type IsHealthy struct { healthCheckManager checks.HealthCheckManager } -func (f *IsHealthy) Apply(_ *RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { +func (f *IsHealthy) Apply(_ *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { var upstreamStatus = f.healthCheckManager.GetUpstreamStatus(upstreamConfig.ID) return upstreamStatus.PeerCheck.IsPassing() && upstreamStatus.SyncingCheck.IsPassing() } @@ -46,7 +42,7 @@ type IsAtGlobalMaxHeight struct { chainMetadataStore *metadata.ChainMetadataStore } -func (f *IsAtGlobalMaxHeight) Apply(_ *RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { +func (f *IsAtGlobalMaxHeight) Apply(_ *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { maxHeight := f.chainMetadataStore.GetGlobalMaxHeight() upstreamStatus := f.healthCheckManager.GetUpstreamStatus(upstreamConfig.ID) @@ -59,7 +55,7 @@ type IsAtMaxHeightForGroup struct { chainMetadataStore *metadata.ChainMetadataStore } -func (f *IsAtMaxHeightForGroup) Apply(_ *RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { +func (f *IsAtMaxHeightForGroup) Apply(_ *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { maxHeightForGroup := f.chainMetadataStore.GetMaxHeightForGroup(upstreamConfig.GroupID) upstreamStatus := f.healthCheckManager.GetUpstreamStatus(upstreamConfig.ID) @@ -70,7 +66,7 @@ func (f *IsAtMaxHeightForGroup) Apply(_ *RequestMetadata, upstreamConfig *config type SimpleIsStatePresentFilter struct{} func (f *SimpleIsStatePresentFilter) Apply( - requestMetadata *RequestMetadata, + requestMetadata *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig, ) bool { if requestMetadata.IsStateRequired { diff --git a/internal/route/node_filter_test.go b/internal/route/node_filter_test.go index 74412a42..e4839d12 100644 --- a/internal/route/node_filter_test.go +++ b/internal/route/node_filter_test.go @@ -4,18 +4,19 @@ import ( "testing" "github.com/satsuma-data/node-gateway/internal/config" + "github.com/satsuma-data/node-gateway/internal/metadata" "github.com/stretchr/testify/assert" ) type AlwaysPass struct{} -func (AlwaysPass) Apply(_ *RequestMetadata, _ *config.UpstreamConfig) bool { +func (AlwaysPass) Apply(_ *metadata.RequestMetadata, _ *config.UpstreamConfig) bool { return true } type AlwaysFail struct{} -func (AlwaysFail) Apply(_ *RequestMetadata, _ *config.UpstreamConfig) bool { +func (AlwaysFail) Apply(_ *metadata.RequestMetadata, _ *config.UpstreamConfig) bool { return false } @@ -25,7 +26,7 @@ func TestAndFilter_Apply(t *testing.T) { } type args struct { - requestMetadata *RequestMetadata + requestMetadata *metadata.RequestMetadata upstreamConfig *config.UpstreamConfig } From c73eb6cfcca48efb1f5922908f83eb43761a7a12 Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 11:46:47 +0300 Subject: [PATCH 04/16] feat: add RequestMetadataParser --- internal/metadata/request_metadata_parser.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 internal/metadata/request_metadata_parser.go diff --git a/internal/metadata/request_metadata_parser.go b/internal/metadata/request_metadata_parser.go new file mode 100644 index 00000000..a4cbdf1a --- /dev/null +++ b/internal/metadata/request_metadata_parser.go @@ -0,0 +1,20 @@ +package metadata + +import ( + "github.com/satsuma-data/node-gateway/internal/jsonrpc" +) + +type RequestMetadataParser struct{} + +func (p *RequestMetadataParser) Parse(requestBody jsonrpc.RequestBody) RequestMetadata { + result := RequestMetadata{} + + switch requestBody.Method { + case "eth_getBalance", "eth_getStorageAt", "eth_getTransactionCount", "eth_getCode", "eth_call", "eth_estimateGas": + result.IsStateRequired = true + default: + result.IsStateRequired = false + } + + return result +} From 12f914fca5f59a5d4a39292af8edb3d6e8d278b2 Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 11:49:46 +0300 Subject: [PATCH 05/16] feat: add metadataParser field in Router and call it --- internal/route/router.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/route/router.go b/internal/route/router.go index dd66db29..21e374ea 100644 --- a/internal/route/router.go +++ b/internal/route/router.go @@ -39,6 +39,7 @@ type SimpleRouter struct { requestExecutor RequestExecutor // Map from Priority => UpstreamIDs priorityToUpstreams types.PriorityToUpstreamsMap + metadataParser metadata.RequestMetadataParser upstreamConfigs []config.UpstreamConfig } @@ -56,6 +57,7 @@ func NewRouter( priorityToUpstreams: groupUpstreamsByPriority(upstreamConfigs, groupConfigs), routingStrategy: routingStrategy, requestExecutor: RequestExecutor{httpClient: &http.Client{}}, + metadataParser: metadata.RequestMetadataParser{}, } return r @@ -95,6 +97,7 @@ func (r *SimpleRouter) Route( ctx context.Context, requestBody jsonrpc.RequestBody, ) (*jsonrpc.ResponseBody, *http.Response, error) { + _ = r.metadataParser.Parse(requestBody) id, err := r.routingStrategy.RouteNextRequest(r.priorityToUpstreams) if err != nil { switch { From c15fc35326144312ba6727e16d431664c47b56ca Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 11:54:02 +0300 Subject: [PATCH 06/16] feat: add RequestMetadata param to RouteNextRequest --- internal/mocks/RoutingStrategy.go | 6 +++++- internal/route/filtering_strategy.go | 8 ++++++-- internal/route/router.go | 4 ++-- internal/route/routing_strategy.go | 11 +++++++++-- internal/route/routing_strategy_test.go | 11 ++++++----- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/internal/mocks/RoutingStrategy.go b/internal/mocks/RoutingStrategy.go index 4cbf4a73..f2167dca 100644 --- a/internal/mocks/RoutingStrategy.go +++ b/internal/mocks/RoutingStrategy.go @@ -3,6 +3,7 @@ package mocks import ( + "github.com/satsuma-data/node-gateway/internal/metadata" mock "github.com/stretchr/testify/mock" types "github.com/satsuma-data/node-gateway/internal/types" @@ -22,7 +23,10 @@ func (_m *MockRoutingStrategy) EXPECT() *MockRoutingStrategy_Expecter { } // RouteNextRequest provides a mock function with given fields: upstreamsByPriority -func (_m *MockRoutingStrategy) RouteNextRequest(upstreamsByPriority types.PriorityToUpstreamsMap) (string, error) { +func (_m *MockRoutingStrategy) RouteNextRequest( + upstreamsByPriority types.PriorityToUpstreamsMap, + requestMetadata metadata.RequestMetadata, +) (string, error) { ret := _m.Called(upstreamsByPriority) var r0 string diff --git a/internal/route/filtering_strategy.go b/internal/route/filtering_strategy.go index 5b71309b..08319710 100644 --- a/internal/route/filtering_strategy.go +++ b/internal/route/filtering_strategy.go @@ -2,6 +2,7 @@ package route import ( "github.com/satsuma-data/node-gateway/internal/config" + "github.com/satsuma-data/node-gateway/internal/metadata" "github.com/satsuma-data/node-gateway/internal/types" "go.uber.org/zap" ) @@ -11,9 +12,12 @@ type FilteringRoutingStrategy struct { BackingStrategy RoutingStrategy } -func (s *FilteringRoutingStrategy) RouteNextRequest(upstreamsByPriority types.PriorityToUpstreamsMap) (string, error) { +func (s *FilteringRoutingStrategy) RouteNextRequest( + upstreamsByPriority types.PriorityToUpstreamsMap, + requestMetadata metadata.RequestMetadata, +) (string, error) { filteredUpstreams := s.filter(upstreamsByPriority) - return s.BackingStrategy.RouteNextRequest(filteredUpstreams) + return s.BackingStrategy.RouteNextRequest(filteredUpstreams, requestMetadata) } func (s *FilteringRoutingStrategy) filter(upstreamsByPriority types.PriorityToUpstreamsMap) types.PriorityToUpstreamsMap { diff --git a/internal/route/router.go b/internal/route/router.go index 21e374ea..c44fc8b6 100644 --- a/internal/route/router.go +++ b/internal/route/router.go @@ -97,8 +97,8 @@ func (r *SimpleRouter) Route( ctx context.Context, requestBody jsonrpc.RequestBody, ) (*jsonrpc.ResponseBody, *http.Response, error) { - _ = r.metadataParser.Parse(requestBody) - id, err := r.routingStrategy.RouteNextRequest(r.priorityToUpstreams) + requestMetadata := r.metadataParser.Parse(requestBody) + id, err := r.routingStrategy.RouteNextRequest(r.priorityToUpstreams, requestMetadata) if err != nil { switch { case errors.Is(err, ErrNoHealthyUpstreams): diff --git a/internal/route/routing_strategy.go b/internal/route/routing_strategy.go index 97fbe96c..9ec30137 100644 --- a/internal/route/routing_strategy.go +++ b/internal/route/routing_strategy.go @@ -6,6 +6,7 @@ import ( "sort" "sync/atomic" + "github.com/satsuma-data/node-gateway/internal/metadata" "github.com/satsuma-data/node-gateway/internal/types" "go.uber.org/zap" "golang.org/x/exp/maps" @@ -14,7 +15,10 @@ import ( //go:generate mockery --output ../mocks --name RoutingStrategy --structname MockRoutingStrategy --with-expecter type RoutingStrategy interface { // Returns the next UpstreamID a request should route to. - RouteNextRequest(upstreamsByPriority types.PriorityToUpstreamsMap) (string, error) + RouteNextRequest( + upstreamsByPriority types.PriorityToUpstreamsMap, + requestMetadata metadata.RequestMetadata, + ) (string, error) } type PriorityRoundRobinStrategy struct { counter uint64 @@ -28,7 +32,10 @@ func NewPriorityRoundRobinStrategy() *PriorityRoundRobinStrategy { var ErrNoHealthyUpstreams = errors.New("no healthy upstreams") -func (s *PriorityRoundRobinStrategy) RouteNextRequest(upstreamsByPriority types.PriorityToUpstreamsMap) (string, error) { +func (s *PriorityRoundRobinStrategy) RouteNextRequest( + upstreamsByPriority types.PriorityToUpstreamsMap, + _ metadata.RequestMetadata, +) (string, error) { prioritySorted := maps.Keys(upstreamsByPriority) sort.Ints(prioritySorted) diff --git a/internal/route/routing_strategy_test.go b/internal/route/routing_strategy_test.go index 8c648a17..cb293140 100644 --- a/internal/route/routing_strategy_test.go +++ b/internal/route/routing_strategy_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/satsuma-data/node-gateway/internal/config" + "github.com/satsuma-data/node-gateway/internal/metadata" "github.com/satsuma-data/node-gateway/internal/types" "github.com/stretchr/testify/assert" ) @@ -18,10 +19,10 @@ func TestPriorityStrategy_HighPriority(t *testing.T) { strategy := NewPriorityRoundRobinStrategy() for i := 0; i < 10; i++ { - firstUpstreamID, _ := strategy.RouteNextRequest(upstreams) + firstUpstreamID, _ := strategy.RouteNextRequest(upstreams, metadata.RequestMetadata{}) assert.Equal(t, "something-else", firstUpstreamID) - secondUpstreamID, _ := strategy.RouteNextRequest(upstreams) + secondUpstreamID, _ := strategy.RouteNextRequest(upstreams, metadata.RequestMetadata{}) assert.Equal(t, "geth", secondUpstreamID) } } @@ -41,10 +42,10 @@ func TestPriorityStrategy_LowerPriority(t *testing.T) { strategy := NewPriorityRoundRobinStrategy() for i := 0; i < 10; i++ { - firstUpstreamID, _ := strategy.RouteNextRequest(upstreams) + firstUpstreamID, _ := strategy.RouteNextRequest(upstreams, metadata.RequestMetadata{}) assert.Equal(t, "fallback2", firstUpstreamID) - secondUpstreamID, _ := strategy.RouteNextRequest(upstreams) + secondUpstreamID, _ := strategy.RouteNextRequest(upstreams, metadata.RequestMetadata{}) assert.Equal(t, "fallback1", secondUpstreamID) } } @@ -58,7 +59,7 @@ func TestPriorityStrategy_NoUpstreams(t *testing.T) { strategy := NewPriorityRoundRobinStrategy() for i := 0; i < 10; i++ { - upstreamID, err := strategy.RouteNextRequest(upstreams) + upstreamID, err := strategy.RouteNextRequest(upstreams, metadata.RequestMetadata{}) assert.Equal(t, "", upstreamID) assert.True(t, errors.Is(err, ErrNoHealthyUpstreams)) } From de4d30b7f30e39914708cfd9a1e040c59debcb09 Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 11:56:47 +0300 Subject: [PATCH 07/16] refactor: pass RequestMetadata into NodeFilters --- internal/route/filtering_strategy.go | 9 ++++++--- internal/route/node_filter.go | 10 +++++----- internal/route/node_filter_test.go | 6 +++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/internal/route/filtering_strategy.go b/internal/route/filtering_strategy.go index 08319710..39773841 100644 --- a/internal/route/filtering_strategy.go +++ b/internal/route/filtering_strategy.go @@ -16,11 +16,14 @@ func (s *FilteringRoutingStrategy) RouteNextRequest( upstreamsByPriority types.PriorityToUpstreamsMap, requestMetadata metadata.RequestMetadata, ) (string, error) { - filteredUpstreams := s.filter(upstreamsByPriority) + filteredUpstreams := s.filter(upstreamsByPriority, requestMetadata) return s.BackingStrategy.RouteNextRequest(filteredUpstreams, requestMetadata) } -func (s *FilteringRoutingStrategy) filter(upstreamsByPriority types.PriorityToUpstreamsMap) types.PriorityToUpstreamsMap { +func (s *FilteringRoutingStrategy) filter( + upstreamsByPriority types.PriorityToUpstreamsMap, + requestMetadata metadata.RequestMetadata, +) types.PriorityToUpstreamsMap { priorityToHealthyUpstreams := make(types.PriorityToUpstreamsMap) for priority, upstreamConfigs := range upstreamsByPriority { @@ -29,7 +32,7 @@ func (s *FilteringRoutingStrategy) filter(upstreamsByPriority types.PriorityToUp filteredUpstreams := make([]*config.UpstreamConfig, 0) for _, upstreamConfig := range upstreamConfigs { - if s.NodeFilter.Apply(nil, upstreamConfig) { + if s.NodeFilter.Apply(requestMetadata, upstreamConfig) { filteredUpstreams = append(filteredUpstreams, upstreamConfig) } } diff --git a/internal/route/node_filter.go b/internal/route/node_filter.go index 8dd1c460..47fa0897 100644 --- a/internal/route/node_filter.go +++ b/internal/route/node_filter.go @@ -7,14 +7,14 @@ import ( ) type NodeFilter interface { - Apply(requestMetadata *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool + Apply(requestMetadata metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool } type AndFilter struct { filters []NodeFilter } -func (a *AndFilter) Apply(requestMetadata *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { +func (a *AndFilter) Apply(requestMetadata metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { var result = true for filterIndex := range a.filters { @@ -42,7 +42,7 @@ type IsAtGlobalMaxHeight struct { chainMetadataStore *metadata.ChainMetadataStore } -func (f *IsAtGlobalMaxHeight) Apply(_ *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { +func (f *IsAtGlobalMaxHeight) Apply(_ metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { maxHeight := f.chainMetadataStore.GetGlobalMaxHeight() upstreamStatus := f.healthCheckManager.GetUpstreamStatus(upstreamConfig.ID) @@ -55,7 +55,7 @@ type IsAtMaxHeightForGroup struct { chainMetadataStore *metadata.ChainMetadataStore } -func (f *IsAtMaxHeightForGroup) Apply(_ *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { +func (f *IsAtMaxHeightForGroup) Apply(_ metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { maxHeightForGroup := f.chainMetadataStore.GetMaxHeightForGroup(upstreamConfig.GroupID) upstreamStatus := f.healthCheckManager.GetUpstreamStatus(upstreamConfig.ID) @@ -66,7 +66,7 @@ func (f *IsAtMaxHeightForGroup) Apply(_ *metadata.RequestMetadata, upstreamConfi type SimpleIsStatePresentFilter struct{} func (f *SimpleIsStatePresentFilter) Apply( - requestMetadata *metadata.RequestMetadata, + requestMetadata metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig, ) bool { if requestMetadata.IsStateRequired { diff --git a/internal/route/node_filter_test.go b/internal/route/node_filter_test.go index e4839d12..ab07b93b 100644 --- a/internal/route/node_filter_test.go +++ b/internal/route/node_filter_test.go @@ -10,13 +10,13 @@ import ( type AlwaysPass struct{} -func (AlwaysPass) Apply(_ *metadata.RequestMetadata, _ *config.UpstreamConfig) bool { +func (AlwaysPass) Apply(metadata.RequestMetadata, *config.UpstreamConfig) bool { return true } type AlwaysFail struct{} -func (AlwaysFail) Apply(_ *metadata.RequestMetadata, _ *config.UpstreamConfig) bool { +func (AlwaysFail) Apply(metadata.RequestMetadata, *config.UpstreamConfig) bool { return false } @@ -26,7 +26,7 @@ func TestAndFilter_Apply(t *testing.T) { } type args struct { - requestMetadata *metadata.RequestMetadata + requestMetadata metadata.RequestMetadata upstreamConfig *config.UpstreamConfig } From e9990bd55341c459e03cd5d1ca71585d8370b186 Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 12:00:12 +0300 Subject: [PATCH 08/16] feat: enable SimpleIsStatePresent filter --- internal/route/node_filter.go | 11 +++++++---- internal/server/web_server.go | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/route/node_filter.go b/internal/route/node_filter.go index 47fa0897..441dbbc9 100644 --- a/internal/route/node_filter.go +++ b/internal/route/node_filter.go @@ -32,7 +32,7 @@ type IsHealthy struct { healthCheckManager checks.HealthCheckManager } -func (f *IsHealthy) Apply(_ *metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { +func (f *IsHealthy) Apply(_ metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig) bool { var upstreamStatus = f.healthCheckManager.GetUpstreamStatus(upstreamConfig.ID) return upstreamStatus.PeerCheck.IsPassing() && upstreamStatus.SyncingCheck.IsPassing() } @@ -107,6 +107,8 @@ func CreateSingleNodeFilter( healthCheckManager: manager, chainMetadataStore: store, } + case SimpleIsStatePresent: + return &SimpleIsStatePresentFilter{} default: panic("Unknown filter type " + filterName + "!") } @@ -115,7 +117,8 @@ func CreateSingleNodeFilter( type NodeFilterType string const ( - Healthy NodeFilterType = "healthy" - GlobalMaxHeight NodeFilterType = "globalMaxHeight" - MaxHeightForGroup NodeFilterType = "maxHeightForGroup" + Healthy NodeFilterType = "healthy" + GlobalMaxHeight NodeFilterType = "globalMaxHeight" + MaxHeightForGroup NodeFilterType = "maxHeightForGroup" + SimpleIsStatePresent NodeFilterType = "simpleIsStatePresent" ) diff --git a/internal/server/web_server.go b/internal/server/web_server.go index fb2c29f8..12d991de 100644 --- a/internal/server/web_server.go +++ b/internal/server/web_server.go @@ -68,7 +68,8 @@ func wireRouter(config conf.Config) route.Router { ticker := time.NewTicker(checks.PeriodicHealthCheckInterval) healthCheckManager := checks.NewHealthCheckManager(client.NewEthClient, config.Upstreams, chainMetadataStore, ticker) - nodeFilter := route.CreateNodeFilter([]route.NodeFilterType{route.Healthy, route.MaxHeightForGroup}, healthCheckManager, chainMetadataStore) + enabledNodeFilters := []route.NodeFilterType{route.Healthy, route.MaxHeightForGroup, route.SimpleIsStatePresent} + nodeFilter := route.CreateNodeFilter(enabledNodeFilters, healthCheckManager, chainMetadataStore) routingStrategy := route.FilteringRoutingStrategy{ NodeFilter: nodeFilter, BackingStrategy: route.NewPriorityRoundRobinStrategy(), From ba08f05f69c0e284385212155a4b75ae689be7da Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 12:07:18 +0300 Subject: [PATCH 09/16] test: add tests for SimpleIsStatePresentFilter --- internal/route/node_filter_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/route/node_filter_test.go b/internal/route/node_filter_test.go index ab07b93b..fad5a44c 100644 --- a/internal/route/node_filter_test.go +++ b/internal/route/node_filter_test.go @@ -51,3 +51,32 @@ func TestAndFilter_Apply(t *testing.T) { }) } } + +func TestSimpleIsStatePresentFilter_Apply(t *testing.T) { + fullNodeConfig := config.UpstreamConfig{NodeType: config.Full} + archiveNodeConfig := config.UpstreamConfig{NodeType: config.Archive} + + stateMethodMetadata := metadata.RequestMetadata{IsStateRequired: true} + nonStateMethodMetadata := metadata.RequestMetadata{IsStateRequired: false} + + type args struct { + requestMetadata metadata.RequestMetadata + upstreamConfig *config.UpstreamConfig + } + tests := []struct { + name string + args args + want bool + }{ + {"stateMethodFullNode", args{stateMethodMetadata, &fullNodeConfig}, false}, + {"stateMethodArchiveNode", args{stateMethodMetadata, &archiveNodeConfig}, true}, + {"nonStateMethodFullNode", args{nonStateMethodMetadata, &fullNodeConfig}, true}, + {"nonStateMethodArchiveNode", args{nonStateMethodMetadata, &archiveNodeConfig}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &SimpleIsStatePresentFilter{} + assert.Equalf(t, tt.want, f.Apply(tt.args.requestMetadata, tt.args.upstreamConfig), "Apply(%v, %v)", tt.args.requestMetadata, tt.args.upstreamConfig) + }) + } +} From fa6106119ad1ae55d6031f76eb94bb61e5bd1503 Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 12:19:35 +0300 Subject: [PATCH 10/16] test: add tests for RequestMetadataParser --- .../metadata/request_metadata_parser_test.go | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 internal/metadata/request_metadata_parser_test.go diff --git a/internal/metadata/request_metadata_parser_test.go b/internal/metadata/request_metadata_parser_test.go new file mode 100644 index 00000000..30d2d729 --- /dev/null +++ b/internal/metadata/request_metadata_parser_test.go @@ -0,0 +1,30 @@ +package metadata + +import ( + "testing" + + "github.com/satsuma-data/node-gateway/internal/jsonrpc" + "github.com/stretchr/testify/assert" +) + +func TestRequestMetadataParser_Parse(t *testing.T) { + type args struct { + requestBody jsonrpc.RequestBody + } + tests := []struct { + name string + args args + want RequestMetadata + }{ + {"eth_call", args{jsonrpc.RequestBody{Method: "eth_call"}}, RequestMetadata{IsStateRequired: true}}, + {"eth_getBalance", args{jsonrpc.RequestBody{Method: "eth_getBalance"}}, RequestMetadata{IsStateRequired: true}}, + {"eth_getBlockByNumber", args{jsonrpc.RequestBody{Method: "eth_getBlockByNumber"}}, RequestMetadata{IsStateRequired: false}}, + {"eth_getTransactionReceipt", args{jsonrpc.RequestBody{Method: "eth_getTransactionReceipt"}}, RequestMetadata{IsStateRequired: false}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &RequestMetadataParser{} + assert.Equalf(t, tt.want, p.Parse(tt.args.requestBody), "Parse(%v)", tt.args.requestBody) + }) + } +} From a59a38001949f687f0adb02f243c12e551acf76f Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 12:22:24 +0300 Subject: [PATCH 11/16] style: make linter happy --- internal/metadata/request_metadata_parser_test.go | 2 ++ internal/route/node_filter_test.go | 8 +++++--- internal/route/router.go | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/metadata/request_metadata_parser_test.go b/internal/metadata/request_metadata_parser_test.go index 30d2d729..adc816b7 100644 --- a/internal/metadata/request_metadata_parser_test.go +++ b/internal/metadata/request_metadata_parser_test.go @@ -11,6 +11,7 @@ func TestRequestMetadataParser_Parse(t *testing.T) { type args struct { requestBody jsonrpc.RequestBody } + tests := []struct { name string args args @@ -21,6 +22,7 @@ func TestRequestMetadataParser_Parse(t *testing.T) { {"eth_getBlockByNumber", args{jsonrpc.RequestBody{Method: "eth_getBlockByNumber"}}, RequestMetadata{IsStateRequired: false}}, {"eth_getTransactionReceipt", args{jsonrpc.RequestBody{Method: "eth_getTransactionReceipt"}}, RequestMetadata{IsStateRequired: false}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &RequestMetadataParser{} diff --git a/internal/route/node_filter_test.go b/internal/route/node_filter_test.go index fad5a44c..acb7edd3 100644 --- a/internal/route/node_filter_test.go +++ b/internal/route/node_filter_test.go @@ -25,7 +25,7 @@ func TestAndFilter_Apply(t *testing.T) { filters []NodeFilter } - type args struct { + type args struct { //nolint:govet // field alignment doesn't matter in tests requestMetadata metadata.RequestMetadata upstreamConfig *config.UpstreamConfig } @@ -59,11 +59,12 @@ func TestSimpleIsStatePresentFilter_Apply(t *testing.T) { stateMethodMetadata := metadata.RequestMetadata{IsStateRequired: true} nonStateMethodMetadata := metadata.RequestMetadata{IsStateRequired: false} - type args struct { + type args struct { //nolint:govet // field alignment doesn't matter in tests requestMetadata metadata.RequestMetadata upstreamConfig *config.UpstreamConfig } - tests := []struct { + + tests := []struct { //nolint:govet // field alignment doesn't matter in tests name string args args want bool @@ -73,6 +74,7 @@ func TestSimpleIsStatePresentFilter_Apply(t *testing.T) { {"nonStateMethodFullNode", args{nonStateMethodMetadata, &fullNodeConfig}, true}, {"nonStateMethodArchiveNode", args{nonStateMethodMetadata, &archiveNodeConfig}, true}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := &SimpleIsStatePresentFilter{} diff --git a/internal/route/router.go b/internal/route/router.go index c44fc8b6..3a2954f0 100644 --- a/internal/route/router.go +++ b/internal/route/router.go @@ -99,6 +99,7 @@ func (r *SimpleRouter) Route( ) (*jsonrpc.ResponseBody, *http.Response, error) { requestMetadata := r.metadataParser.Parse(requestBody) id, err := r.routingStrategy.RouteNextRequest(r.priorityToUpstreams, requestMetadata) + if err != nil { switch { case errors.Is(err, ErrNoHealthyUpstreams): From 0dde85db5d461f225c8a66fa2bc7277deed54a05 Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 19 Sep 2022 18:36:47 +0300 Subject: [PATCH 12/16] feat: make NodeType required and update tests --- internal/config/config.go | 6 ++++++ internal/config/config_test.go | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 56b20866..15f27bec 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -33,6 +33,12 @@ func (c *UpstreamConfig) isValid(groups []GroupConfig) bool { zap.L().Error("httpUrl cannot be empty", zap.Any("config", c), zap.String("upstreamId", c.ID)) } + if c.NodeType == "" { + isValid = false + + zap.L().Error("nodeType cannot be empty", zap.Any("config", c), zap.String("upstreamId", c.ID)) + } + if c.HealthCheckConfig.UseWSForBlockHeight != nil && *c.HealthCheckConfig.UseWSForBlockHeight && c.WSURL == "" { isValid = false diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f22782f1..b89e1830 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -120,10 +120,12 @@ func TestParseConfig_ValidConfig(t *testing.T) { healthCheck: useWsForBlockHeight: true group: primary + nodeType: full - id: ankr-polygon httpURL: "https://rpc.ankr.com/polygon" wsURL: "wss://rpc.ankr.com/polygon/ws/${ANKR_API_KEY}" group: fallback + nodeType: archive ` configBytes := []byte(config) @@ -142,7 +144,8 @@ func TestParseConfig_ValidConfig(t *testing.T) { HealthCheckConfig: HealthCheckConfig{ UseWSForBlockHeight: newBool(true), }, - GroupID: "primary", + GroupID: "primary", + NodeType: Full, }, { ID: "ankr-polygon", @@ -151,7 +154,8 @@ func TestParseConfig_ValidConfig(t *testing.T) { HealthCheckConfig: HealthCheckConfig{ UseWSForBlockHeight: nil, }, - GroupID: "fallback", + GroupID: "fallback", + NodeType: Archive, }, }, Global: GlobalConfig{ From c989bcf912458bb14c70ee23f42f54db4d84d2bd Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Fri, 23 Sep 2022 15:14:21 +0300 Subject: [PATCH 13/16] chore: add nodeType to sample config --- configs/config.sample.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/configs/config.sample.yml b/configs/config.sample.yml index 01a1c5c9..f202959b 100644 --- a/configs/config.sample.yml +++ b/configs/config.sample.yml @@ -33,10 +33,12 @@ upstreams: # it quickly updates the gateway with the latest block height. If this # setting is undefined, the gateway will attempt to subscribe to new # heads if the upstream supports it. + # nodeType - full or archive - id: my-node httpURL: "http://12.57.207.168:8545" wsURL: "wss://12.57.207.168:8546" group: primary + nodeType: full - id: infura-eth httpURL: "https://mainnet.infura.io/v3/${INFURA_API_KEY}" wsURL: "wss://mainnet.infura.io/ws/v3/${INFURA_API_KEY}" @@ -44,9 +46,11 @@ upstreams: username: ~ password: ${INFURA_API_KEY_SECRET} group: fallback + nodeType: archive - id: alchemy-eth httpURL: "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" wsURL: "wss://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" healthCheck: useWsForBlockHeight: false group: fallback + nodeType: full From 46245fde54d833e6c772dec816233031f23b1bab Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 26 Sep 2022 15:54:36 +0300 Subject: [PATCH 14/16] chore: add link to JSON RPC State methods --- internal/metadata/request_metadata_parser.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/metadata/request_metadata_parser.go b/internal/metadata/request_metadata_parser.go index a4cbdf1a..c0902eb2 100644 --- a/internal/metadata/request_metadata_parser.go +++ b/internal/metadata/request_metadata_parser.go @@ -11,6 +11,7 @@ func (p *RequestMetadataParser) Parse(requestBody jsonrpc.RequestBody) RequestMe switch requestBody.Method { case "eth_getBalance", "eth_getStorageAt", "eth_getTransactionCount", "eth_getCode", "eth_call", "eth_estimateGas": + // List of state methods: https://ethereum.org/en/developers/docs/apis/json-rpc/#state_methods result.IsStateRequired = true default: result.IsStateRequired = false From 352da166afb53730f3a0b505cc1d24466ebada74 Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 26 Sep 2022 16:05:45 +0300 Subject: [PATCH 15/16] refactor: simplify test --- .../metadata/request_metadata_parser_test.go | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/internal/metadata/request_metadata_parser_test.go b/internal/metadata/request_metadata_parser_test.go index adc816b7..7fd85bf4 100644 --- a/internal/metadata/request_metadata_parser_test.go +++ b/internal/metadata/request_metadata_parser_test.go @@ -12,15 +12,25 @@ func TestRequestMetadataParser_Parse(t *testing.T) { requestBody jsonrpc.RequestBody } - tests := []struct { + type testArgs struct { name string args args want RequestMetadata - }{ - {"eth_call", args{jsonrpc.RequestBody{Method: "eth_call"}}, RequestMetadata{IsStateRequired: true}}, - {"eth_getBalance", args{jsonrpc.RequestBody{Method: "eth_getBalance"}}, RequestMetadata{IsStateRequired: true}}, - {"eth_getBlockByNumber", args{jsonrpc.RequestBody{Method: "eth_getBlockByNumber"}}, RequestMetadata{IsStateRequired: false}}, - {"eth_getTransactionReceipt", args{jsonrpc.RequestBody{Method: "eth_getTransactionReceipt"}}, RequestMetadata{IsStateRequired: false}}, + } + + testForMethod := func(methodName string, isStateRequired bool) testArgs { + return testArgs{ + methodName, + args{jsonrpc.RequestBody{Method: methodName}}, + RequestMetadata{IsStateRequired: isStateRequired}, + } + } + + tests := []testArgs{ + testForMethod("eth_call", true), + testForMethod("eth_getBalance", true), + testForMethod("eth_getBlockByNumber", false), + testForMethod("eth_getTransactionReceipt", false), } for _, tt := range tests { From 0eb30cfb7f5f6df9f64c5fcced3bd35e26fb1fdd Mon Sep 17 00:00:00 2001 From: Ivan Vergiliev Date: Mon, 26 Sep 2022 16:09:02 +0300 Subject: [PATCH 16/16] style: drop Filter suffix from state filter --- internal/route/node_filter.go | 16 ++++++++-------- internal/route/node_filter_test.go | 2 +- internal/server/web_server.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/route/node_filter.go b/internal/route/node_filter.go index 441dbbc9..d4dc3e13 100644 --- a/internal/route/node_filter.go +++ b/internal/route/node_filter.go @@ -63,9 +63,9 @@ func (f *IsAtMaxHeightForGroup) Apply(_ metadata.RequestMetadata, upstreamConfig return upstreamStatus.BlockHeightCheck.IsPassing(maxHeightForGroup) } -type SimpleIsStatePresentFilter struct{} +type SimpleIsStatePresent struct{} -func (f *SimpleIsStatePresentFilter) Apply( +func (f *SimpleIsStatePresent) Apply( requestMetadata metadata.RequestMetadata, upstreamConfig *config.UpstreamConfig, ) bool { @@ -107,8 +107,8 @@ func CreateSingleNodeFilter( healthCheckManager: manager, chainMetadataStore: store, } - case SimpleIsStatePresent: - return &SimpleIsStatePresentFilter{} + case SimpleStatePresent: + return &SimpleIsStatePresent{} default: panic("Unknown filter type " + filterName + "!") } @@ -117,8 +117,8 @@ func CreateSingleNodeFilter( type NodeFilterType string const ( - Healthy NodeFilterType = "healthy" - GlobalMaxHeight NodeFilterType = "globalMaxHeight" - MaxHeightForGroup NodeFilterType = "maxHeightForGroup" - SimpleIsStatePresent NodeFilterType = "simpleIsStatePresent" + Healthy NodeFilterType = "healthy" + GlobalMaxHeight NodeFilterType = "globalMaxHeight" + MaxHeightForGroup NodeFilterType = "maxHeightForGroup" + SimpleStatePresent NodeFilterType = "simpleStatePresent" ) diff --git a/internal/route/node_filter_test.go b/internal/route/node_filter_test.go index acb7edd3..dc1fa3d5 100644 --- a/internal/route/node_filter_test.go +++ b/internal/route/node_filter_test.go @@ -77,7 +77,7 @@ func TestSimpleIsStatePresentFilter_Apply(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := &SimpleIsStatePresentFilter{} + f := &SimpleIsStatePresent{} assert.Equalf(t, tt.want, f.Apply(tt.args.requestMetadata, tt.args.upstreamConfig), "Apply(%v, %v)", tt.args.requestMetadata, tt.args.upstreamConfig) }) } diff --git a/internal/server/web_server.go b/internal/server/web_server.go index 12d991de..dda057f5 100644 --- a/internal/server/web_server.go +++ b/internal/server/web_server.go @@ -68,7 +68,7 @@ func wireRouter(config conf.Config) route.Router { ticker := time.NewTicker(checks.PeriodicHealthCheckInterval) healthCheckManager := checks.NewHealthCheckManager(client.NewEthClient, config.Upstreams, chainMetadataStore, ticker) - enabledNodeFilters := []route.NodeFilterType{route.Healthy, route.MaxHeightForGroup, route.SimpleIsStatePresent} + enabledNodeFilters := []route.NodeFilterType{route.Healthy, route.MaxHeightForGroup, route.SimpleStatePresent} nodeFilter := route.CreateNodeFilter(enabledNodeFilters, healthCheckManager, chainMetadataStore) routingStrategy := route.FilteringRoutingStrategy{ NodeFilter: nodeFilter,