diff --git a/localnet/kubernetes/values-appgateserver.yaml b/localnet/kubernetes/values-appgateserver.yaml index 80357e815..d7fba6016 100644 --- a/localnet/kubernetes/values-appgateserver.yaml +++ b/localnet/kubernetes/values-appgateserver.yaml @@ -1,2 +1,3 @@ config: - query_node_url: tcp://sequencer-poktroll-sequencer:36657 + query_node_rpc_url: tcp://sequencer-poktroll-sequencer:36657 + query_node_grpc_url: tcp://sequencer-poktroll-sequencer:36658 \ No newline at end of file diff --git a/localnet/kubernetes/values-relayminer.yaml b/localnet/kubernetes/values-relayminer.yaml index 0fcd2b27c..3e5300a78 100644 --- a/localnet/kubernetes/values-relayminer.yaml +++ b/localnet/kubernetes/values-relayminer.yaml @@ -1,3 +1,20 @@ config: - query_node_url: tcp://sequencer-poktroll-sequencer:36657 - network_node_url: tcp://sequencer-poktroll-sequencer:36657 + signing_key_name: supplier1 + smt_store_path: smt_stores + pocket_node: + query_node_rpc_url: tcp://sequencer-poktroll-sequencer:36657 + query_node_grpc_url: tcp://sequencer-poktroll-sequencer:36658 + tx_node_rpc_url: tcp://sequencer-poktroll-sequencer:36657 + proxies: + - proxy_name: http-proxy + type: http + host: 0.0.0.0:8545 + suppliers: + - service_id: anvil + type: http + service_config: + url: http://anvil:8547/ + proxy_names: + - http-proxy + hosts: + - tcp://relayminers:8545 diff --git a/localnet/poktrolld/config/appgate_server_config.yaml b/localnet/poktrolld/config/appgate_server_config.yaml index 646753cae..475678930 100644 --- a/localnet/poktrolld/config/appgate_server_config.yaml +++ b/localnet/poktrolld/config/appgate_server_config.yaml @@ -1,8 +1,5 @@ -# Whether the server should sign all incoming requests with its own ring (for applications) -self_signing: true -# The name of the key (in the keyring) that will be used to sign relays +query_node_rpc_url: tcp://sequencer-poktroll-sequencer:36657 +query_node_grpc_url: tcp://sequencer-poktroll-sequencer:36658 signing_key: app1 -# The host and port that the appgate server will listen on -listening_endpoint: http://localhost:42069 -# tcp://: to a full pocket node for reading data and listening for on-chain events -query_node_url: tcp://127.0.0.1:36657 +self_signing: true +listening_endpoint: http://localhost:42069 \ No newline at end of file diff --git a/localnet/poktrolld/config/appgate_server_config_example.yaml b/localnet/poktrolld/config/appgate_server_config_example.yaml new file mode 100644 index 000000000..d34252066 --- /dev/null +++ b/localnet/poktrolld/config/appgate_server_config_example.yaml @@ -0,0 +1,11 @@ +# Pocket node URL that exposes CometBFT JSON-RPC API. +# This can be used by the Cosmos client SDK, event subscriptions, etc... +query_node_rpc_url: tcp://sequencer-poktroll-sequencer:36657 +# Pocket node URL that exposes the Cosmos gRPC service, dedicated to querying purposes. +query_node_grpc_url: tcp://sequencer-poktroll-sequencer:36658 +# The name of the key (in the keyring) that will be used to sign relays +signing_key: app1 +# Whether the server should sign all incoming requests with its own ring (for applications) +self_signing: true +# The host and port that the appgate server will listen on +listening_endpoint: http://localhost:42069 \ No newline at end of file diff --git a/localnet/poktrolld/config/relayminer_config.yaml b/localnet/poktrolld/config/relayminer_config.yaml index 710f8722b..0f01695b3 100644 --- a/localnet/poktrolld/config/relayminer_config.yaml +++ b/localnet/poktrolld/config/relayminer_config.yaml @@ -1,15 +1,19 @@ -# tcp://: to a full pocket node for reading data and listening for on-chain events -query_node_url: tcp://localhost:36657 -# tcp://: to a pocket node that gossips transactions throughout the network (may or may not be the sequencer) -network_node_url: tcp://127.0.0.1:36657 -# Name of the key (in the keyring) to sign transactions signing_key_name: supplier1 -# TODO_TECHDEBT(#137, #130): Once the `relayer.json` config file is implemented AND a local LLM RPC service -# is supported on LocalNet, this needs to be expanded to include more than one service. The ability to support -# multiple services is already in place but currently (as seen below) is hardcoded. -# TODO_UPNEXT(@okdas): this hostname should be updated to match that of the in-tilt anvil service. -proxied_service_endpoints: - anvil: http://anvil:8547 - svc1: http://localhost:8082 -# Path to where the data backing SMT KV store exists on disk smt_store_path: smt_stores +pocket_node: + query_node_rpc_url: tcp://sequencer-poktroll-sequencer:36657 + query_node_grpc_url: tcp://sequencer-poktroll-sequencer:36658 + tx_node_rpc_url: tcp://sequencer-poktroll-sequencer:36657 +proxies: + - proxy_name: http-proxy + type: http + host: 0.0.0.0:8545 +suppliers: + - service_id: anvil + type: http + service_config: + url: http://anvil:8547/ + proxy_names: + - http-proxy + hosts: + - tcp://relayminers:8545 diff --git a/localnet/poktrolld/config/relayminer_config_full_example.yaml b/localnet/poktrolld/config/relayminer_config_full_example.yaml new file mode 100644 index 000000000..99f370900 --- /dev/null +++ b/localnet/poktrolld/config/relayminer_config_full_example.yaml @@ -0,0 +1,126 @@ +# TODO_CONSIDERATION: We don't need this now, but it would be beneficial if the +# logic handling this config file could be designed in such a way that it allows for +# "hot" config changes in the future, meaning changes without restarting a process. +# This would be useful for adding a proxy or a supplier without interrupting the service. + +# Name of the key (in the keyring) to sign transactions. +signing_key_name: supplier1 +# Relative path (on the relayminer's machine) to where the data backing +# SMT KV store exists on disk. +smt_store_path: smt_stores + +pocket_node: + # Pocket node URL that exposes CometBFT JSON-RPC API. + # This can be used by the Cosmos client SDK, event subscriptions, etc... + # If unspecified, defaults to `tx_node_rpc_url`. + query_node_rpc_url: tcp://sequencer-poktroll-sequencer:36657 + # Pocket node URL that exposes the Cosmos gRPC service, dedicated to querying purposes. + query_node_grpc_url: tcp://sequencer-poktroll-sequencer:36658 + # Pocket node URL that exposes the TendermintRPC service. + tx_node_rpc_url: tcp://sequencer-poktroll-sequencer:36658 + +# Proxies are endpoints that expose different suppliers to the internet. +proxies: + # Name of the proxy. It will be used to reference in a supplier. + # Must be unique. + # Required. + # TODO_CONSIDERATION: if we enforce DNS compliant names, it can potentially + # become handy in the future. + # More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names + - proxy_name: http-example + # Type of proxy: currently only http is supported but will support more + # (https, tcp, quic ...) in the future. + # MUST match the type of the supplier. + # Required. + type: http + # Hostname to open port on. + # Use 0.0.0.0 in containerized environments. + # 127.0.0.1 with a reverse proxy when there's another process on localhost + # that can be used as a reverse proxy (nginx, apache, traefik, etc.). + # Required. + host: 127.0.0.1:8080 + + # TODO_IMPROVE: https is not currently supported, but this is how it could potentially look. + # - name: example-how-we-can-support-https + # type: https + # host: 0.0.0.0:8443 + # tls: + # enabled: true + # certificate: /path/to/crt + # key: /path/to/key + +# Suppliers are different services that can be offered through RelayMiner. +# When a supplier is configured to use a proxy and staked appropriately, +# the relays will start flowing through RelayMiner. +suppliers: + # The serviceId the supplier offered to the network . + # It must match the Service.Id of the service that has been staked for. + # Must be unique. + # Required. + - service_id: ethereum + # Type of how the supplier offers service through the network. + # Must match the type of the proxy the supplier is connected to. + # Required. + type: http + # Configuration of the service offered through RelayMiner. + service_config: + # URL RelayMiner proxies the requests to. + # Also known as the data node, or service node in some cases. + # Required. + url: http://anvil.servicer:8545 + # Authentication for the service. + # HTTP Basic Auth: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication + # Optional. + authentication: + username: user + password: pwd + + # TODO_IMPROVE: This is not supported in code yet, + # but some services authenticate via a header. + # Example, if the service requires a header like `Authorization: Bearer ` + # Authorization: Bearer + # Optional. + headers: {} + + # A list of hosts the proxy is accepting requests from. + # When linked to the proxy, each host is going to be used to lookup the + # the Supplier.Service in Pocket Network. + # Each host in the list must match a Supplier.Service.Endpoint that the Supplier + # has advertised on-chain when staking for that Service. + # There are various reasons to having multiple hosts for the same supplier services, + # - The on-chain Supplier may provide the same Service on multiple domains + # (e.g. for different regions). + # - The operator may want to route requests of different RPC types to + # the same proxy. + # - Migrating from one domain to another. Where the operator could still + # accept requests on the old domain while the new domain is being propagated. + # - The operator may want to have a different domain for internal requests. + # - The on-chain Service configuration accepts multiple endpoints. + # Must be unique within the proxy it is referenced in. + # Required. + hosts: + - ethereum.devnet1.poktroll.com + # The `service_id` of the supplier is automatically added to the hosts section + # for potential troubleshooting/debugging purposes such as: + # Having internal requests coming from non-FQDNs because of complex routing. + # Sending requests from k8s pods. + # Specify the `host` in curl requests when testing `curl -H "Host: ethereum" ...` + # and make the proxy server process the request without the need for an + # on-chain Endpoint entry. + # - ethereum # <- this part is added automatically. + + # Names of proxies that this supplier is connected to. + # This MUST correspond to the `proxy_name` entry in the `proxies` section + # in order for the supplier to be available to the external network. + # Required. + proxy_names: + - http-example # when the RelayMiner server builder runs. + - service_id: 7b-llm-model + type: http + service_config: + url: http://llama-endpoint + hosts: + - 7b-llm-model.devnet1.poktroll.com + # - 7b-llm-model # <- this part is added automatically. + proxy_names: + - http-example \ No newline at end of file diff --git a/pkg/appgateserver/cmd/cmd.go b/pkg/appgateserver/cmd/cmd.go index 657419e46..fa747abf0 100644 --- a/pkg/appgateserver/cmd/cmd.go +++ b/pkg/appgateserver/cmd/cmd.go @@ -25,8 +25,13 @@ import ( const omittedDefaultFlagValue = "explicitly omitting default" var ( + // flagAppGateConfig is the variable containing the AppGate config filepath + // sourced from the `--config` flag. flagAppGateConfig string - flagCosmosNodeURL string + // flagNodeRPCURL is the variable containing the Cosmos node RPC URL flag value. + flagNodeRPCURL string + // flagNodeGRPCURL is the variable containing the Cosmos node GRPC URL flag value. + flagNodeGRPCURL string ) // AppGateServerCmd returns the Cobra command for running the AppGate server. @@ -65,9 +70,11 @@ provided that: cmd.Flags().StringVar(&flagAppGateConfig, "config", "", "The path to the appgate config file") // Cosmos flags + // TODO_TECHDEBT(#256): Remove unneeded cosmos flags. cmd.Flags().String(cosmosflags.FlagKeyringBackend, "", "Select keyring's backend (os|file|kwallet|pass|test)") - cmd.Flags(). - StringVar(&flagCosmosNodeURL, cosmosflags.FlagNode, omittedDefaultFlagValue, "Register the default Cosmos node flag, which is needed to initialize the Cosmos query context correctly. It can be used to override the `QueryNodeUrl` field in the config file if specified.") + cmd.Flags().StringVar(&flagNodeRPCURL, cosmosflags.FlagNode, omittedDefaultFlagValue, "Register the default Cosmos node flag, which is needed to initialize the Cosmos query context correctly. It can be used to override the `QueryNodeUrl` field in the config file if specified.") + cmd.Flags().StringVar(&flagNodeGRPCURL, cosmosflags.FlagGRPC, omittedDefaultFlagValue, "Register the default Cosmos node grpc flag, which is needed to initialize the Cosmos query context with grpc correctly. It can be used to override the `QueryNodeGRPCUrl` field in the config file if specified.") + cmd.Flags().Bool(cosmosflags.FlagGRPCInsecure, true, "Used to initialize the Cosmos query context with grpc security options. It can be used to override the `QueryNodeGRPCInsecure` field in the config file if specified.") return cmd } @@ -145,24 +152,37 @@ func setupAppGateServerDependencies( cmd *cobra.Command, appGateConfig *appgateconfig.AppGateServerConfig, ) (_ depinject.Config, err error) { - queryNodeURL := appGateConfig.QueryNodeUrl - // Override the config file's `QueryNodeUrl` fields + queryNodeRPCURL := appGateConfig.QueryNodeRPCUrl + queryNodeGRPCURL := appGateConfig.QueryNodeGRPCUrl + + // Override the config file's `QueryNodeGRPCUrl` field + // with the `--grpc-addr` flag if it was specified. + // TODO_TECHDEBT(#223) Remove this check once viper is used as SoT for overridable config values. + if flagNodeGRPCURL != omittedDefaultFlagValue { + queryNodeGRPCURL, err = url.Parse(flagNodeGRPCURL) + if err != nil { + return nil, fmt.Errorf("failed to parse grpc query URL: %w", err) + } + } + + // Override the config file's `QueryNodeRPCURL` field // with the `--node` flag if it was specified. - if flagCosmosNodeURL != omittedDefaultFlagValue { - queryNodeURL, err = url.Parse(flagCosmosNodeURL) + // TODO_TECHDEBT(#223) Remove this check once viper is used as SoT for overridable config values. + if flagNodeRPCURL != omittedDefaultFlagValue { + queryNodeRPCURL, err = url.Parse(flagNodeRPCURL) if err != nil { - return nil, fmt.Errorf("failed to parse Cosmos node URL: %w", err) + return nil, fmt.Errorf("failed to parse rpc query URL: %w", err) } } supplierFuncs := []config.SupplierFn{ config.NewSupplyLoggerFromCtx(ctx), - config.NewSupplyEventsQueryClientFn(queryNodeURL.Host), // leaf - config.NewSupplyBlockClientFn(), // leaf - config.NewSupplyQueryClientContextFn(queryNodeURL.String()), // leaf - config.NewSupplyAccountQuerierFn(), // leaf - config.NewSupplyApplicationQuerierFn(), // leaf - config.NewSupplySessionQuerierFn(), // leaf + config.NewSupplyEventsQueryClientFn(queryNodeRPCURL), // leaf + config.NewSupplyBlockClientFn(), // leaf + config.NewSupplyQueryClientContextFn(queryNodeGRPCURL), // leaf + config.NewSupplyAccountQuerierFn(), // leaf + config.NewSupplyApplicationQuerierFn(), // leaf + config.NewSupplySessionQuerierFn(), // leaf config.NewSupplyRingCacheFn(), config.NewSupplyPOKTRollSDKFn(appGateConfig.SigningKey), } diff --git a/pkg/appgateserver/config/appgate_configs_reader.go b/pkg/appgateserver/config/appgate_configs_reader.go index da801e6fc..706c0b926 100644 --- a/pkg/appgateserver/config/appgate_configs_reader.go +++ b/pkg/appgateserver/config/appgate_configs_reader.go @@ -9,18 +9,20 @@ import ( // YAMLAppGateServerConfig is the structure used to unmarshal the AppGateServer config file // TODO_DOCUMENT(@red-0ne): Add proper README documentation for yaml config files. type YAMLAppGateServerConfig struct { - SelfSigning bool `yaml:"self_signing"` + QueryNodeRPCUrl string `yaml:"query_node_rpc_url"` + QueryNodeGRPCUrl string `yaml:"query_node_grpc_url"` SigningKey string `yaml:"signing_key"` + SelfSigning bool `yaml:"self_signing"` ListeningEndpoint string `yaml:"listening_endpoint"` - QueryNodeUrl string `yaml:"query_node_url"` } // AppGateServerConfig is the structure describing the AppGateServer config type AppGateServerConfig struct { - SelfSigning bool + QueryNodeRPCUrl *url.URL + QueryNodeGRPCUrl *url.URL SigningKey string + SelfSigning bool ListeningEndpoint *url.URL - QueryNodeUrl *url.URL } // ParseAppGateServerConfigs parses the stake config file into a AppGateConfig @@ -28,39 +30,53 @@ type AppGateServerConfig struct { func ParseAppGateServerConfigs(configContent []byte) (*AppGateServerConfig, error) { var yamlAppGateServerConfig YAMLAppGateServerConfig + if len(configContent) == 0 { + return nil, ErrAppGateConfigEmpty + } + // Unmarshal the stake config file into a yamlAppGateConfig if err := yaml.Unmarshal(configContent, &yamlAppGateServerConfig); err != nil { - return nil, ErrAppGateConfigUnmarshalYAML.Wrapf("%s", err) + return nil, ErrAppGateConfigUnmarshalYAML.Wrap(err.Error()) } - if yamlAppGateServerConfig.SigningKey == "" { + if len(yamlAppGateServerConfig.SigningKey) == 0 { return nil, ErrAppGateConfigEmptySigningKey } - if yamlAppGateServerConfig.ListeningEndpoint == "" { + if len(yamlAppGateServerConfig.ListeningEndpoint) == 0 { return nil, ErrAppGateConfigInvalidListeningEndpoint } listeningEndpoint, err := url.Parse(yamlAppGateServerConfig.ListeningEndpoint) if err != nil { - return nil, ErrAppGateConfigInvalidListeningEndpoint.Wrapf("%s", err) + return nil, ErrAppGateConfigInvalidListeningEndpoint.Wrap(err.Error()) } - if yamlAppGateServerConfig.QueryNodeUrl == "" { - return nil, ErrAppGateConfigInvalidQueryNodeUrl + if len(yamlAppGateServerConfig.QueryNodeGRPCUrl) == 0 { + return nil, ErrAppGateConfigInvalidQueryNodeGRPCUrl } - queryNodeUrl, err := url.Parse(yamlAppGateServerConfig.QueryNodeUrl) + queryNodeGRPCUrl, err := url.Parse(yamlAppGateServerConfig.QueryNodeGRPCUrl) if err != nil { - return nil, ErrAppGateConfigInvalidQueryNodeUrl.Wrapf("%s", err) + return nil, ErrAppGateConfigInvalidQueryNodeGRPCUrl.Wrap(err.Error()) + } + + if len(yamlAppGateServerConfig.QueryNodeRPCUrl) == 0 { + return nil, ErrAppGateConfigInvalidQueryNodeRPCUrl + } + + queryNodeRPCUrl, err := url.Parse(yamlAppGateServerConfig.QueryNodeRPCUrl) + if err != nil { + return nil, ErrAppGateConfigInvalidQueryNodeRPCUrl.Wrap(err.Error()) } // Populate the appGateServerConfig with the values from the yamlAppGateServerConfig appGateServerConfig := &AppGateServerConfig{ - SelfSigning: yamlAppGateServerConfig.SelfSigning, + QueryNodeRPCUrl: queryNodeRPCUrl, + QueryNodeGRPCUrl: queryNodeGRPCUrl, SigningKey: yamlAppGateServerConfig.SigningKey, + SelfSigning: yamlAppGateServerConfig.SelfSigning, ListeningEndpoint: listeningEndpoint, - QueryNodeUrl: queryNodeUrl, } return appGateServerConfig, nil diff --git a/pkg/appgateserver/config/appgate_configs_reader_test.go b/pkg/appgateserver/config/appgate_configs_reader_test.go index aae5ffdc5..82ee6e53f 100644 --- a/pkg/appgateserver/config/appgate_configs_reader_test.go +++ b/pkg/appgateserver/config/appgate_configs_reader_test.go @@ -16,7 +16,7 @@ func Test_ParseAppGateConfigs(t *testing.T) { tests := []struct { desc string - inputConfig string + inputConfigYAML string expectedError *sdkerrors.Error expectedConfig *config.AppGateServerConfig @@ -25,54 +25,59 @@ func Test_ParseAppGateConfigs(t *testing.T) { { desc: "valid: AppGateServer config", - inputConfig: ` - self_signing: true + inputConfigYAML: ` + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 signing_key: app1 + self_signing: true listening_endpoint: http://localhost:42069 - query_node_url: tcp://127.0.0.1:36657 `, expectedError: nil, expectedConfig: &config.AppGateServerConfig{ - SelfSigning: true, + QueryNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36657"}, + QueryNodeGRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36658"}, SigningKey: "app1", + SelfSigning: true, ListeningEndpoint: &url.URL{Scheme: "http", Host: "localhost:42069"}, - QueryNodeUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36657"}, }, }, { desc: "valid: AppGateServer config with undefined self signing", - inputConfig: ` + inputConfigYAML: ` + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 signing_key: app1 listening_endpoint: http://localhost:42069 - query_node_url: tcp://127.0.0.1:36657 `, expectedError: nil, expectedConfig: &config.AppGateServerConfig{ - SelfSigning: false, + QueryNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36657"}, + QueryNodeGRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36658"}, SigningKey: "app1", + SelfSigning: false, ListeningEndpoint: &url.URL{Scheme: "http", Host: "localhost:42069"}, - QueryNodeUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36657"}, }, }, // Invalid Configs { desc: "invalid: empty AppGateServer config", - inputConfig: ``, + inputConfigYAML: ``, - expectedError: config.ErrAppGateConfigUnmarshalYAML, + expectedError: config.ErrAppGateConfigEmpty, }, { desc: "invalid: no signing key", - inputConfig: ` + inputConfigYAML: ` + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + # NB: explicitly missing signing key self_signing: true - signing_key: listening_endpoint: http://localhost:42069 - query_node_url: tcp://127.0.0.1:36657 `, expectedError: config.ErrAppGateConfigEmptySigningKey, @@ -80,36 +85,77 @@ func Test_ParseAppGateConfigs(t *testing.T) { { desc: "invalid: invalid listening endpoint", - inputConfig: ` - self_signing: true + inputConfigYAML: ` + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 signing_key: app1 - listening_endpoint: &localhost:42069 - query_node_url: tcp://127.0.0.1:36657 + self_signing: true + listening_endpoint: l&ocalhost:42069 `, expectedError: config.ErrAppGateConfigInvalidListeningEndpoint, }, { - desc: "invalid: invalid query node url", + desc: "invalid: invalid query node grpc url", + + inputConfigYAML: ` + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: 1&27.0.0.1:36658 + signing_key: app1 + self_signing: true + listening_endpoint: http://localhost:42069 + `, + + expectedError: config.ErrAppGateConfigInvalidQueryNodeGRPCUrl, + }, + { + desc: "invalid: missing query node grpc url", - inputConfig: ` + inputConfigYAML: ` + query_node_rpc_url: tcp://127.0.0.1:36657 + # NB: explicitly missing query_node_grpc_url + signing_key: app1 + self_signing: true + listening_endpoint: http://localhost:42069 + `, + + expectedError: config.ErrAppGateConfigInvalidQueryNodeGRPCUrl, + }, + { + desc: "invalid: invalid query node rpc url", + + inputConfigYAML: ` + query_node_rpc_url: 1&27.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + signing_key: app1 self_signing: true + listening_endpoint: http://localhost:42069 + `, + + expectedError: config.ErrAppGateConfigInvalidQueryNodeRPCUrl, + }, + { + desc: "invalid: missing query node rpc url", + + inputConfigYAML: ` + # NB: explicitly missing query_node_rpc_url + query_node_grpc_url: tcp://127.0.0.1:36658 signing_key: app1 + self_signing: true listening_endpoint: http://localhost:42069 - query_node_url: &127.0.0.1:36657 `, - expectedError: config.ErrAppGateConfigInvalidQueryNodeUrl, + expectedError: config.ErrAppGateConfigInvalidQueryNodeRPCUrl, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - normalizedConfig := yaml.NormalizeYAMLIndentation(tt.inputConfig) + normalizedConfig := yaml.NormalizeYAMLIndentation(tt.inputConfigYAML) config, err := config.ParseAppGateServerConfigs([]byte(normalizedConfig)) if tt.expectedError != nil { - require.Error(t, err) + require.ErrorIs(t, err, tt.expectedError) require.Nil(t, config) stat, ok := status.FromError(tt.expectedError) require.True(t, ok) @@ -123,7 +169,8 @@ func Test_ParseAppGateConfigs(t *testing.T) { require.Equal(t, tt.expectedConfig.SelfSigning, config.SelfSigning) require.Equal(t, tt.expectedConfig.SigningKey, config.SigningKey) require.Equal(t, tt.expectedConfig.ListeningEndpoint.String(), config.ListeningEndpoint.String()) - require.Equal(t, tt.expectedConfig.QueryNodeUrl.String(), config.QueryNodeUrl.String()) + require.Equal(t, tt.expectedConfig.QueryNodeGRPCUrl.String(), config.QueryNodeGRPCUrl.String()) + require.Equal(t, tt.expectedConfig.QueryNodeGRPCUrl.String(), config.QueryNodeGRPCUrl.String()) }) } } diff --git a/pkg/appgateserver/config/errors.go b/pkg/appgateserver/config/errors.go index 3d4d6831f..450c75378 100644 --- a/pkg/appgateserver/config/errors.go +++ b/pkg/appgateserver/config/errors.go @@ -7,5 +7,7 @@ var ( ErrAppGateConfigUnmarshalYAML = sdkerrors.Register(codespace, 1, "config reader cannot unmarshal yaml content") ErrAppGateConfigEmptySigningKey = sdkerrors.Register(codespace, 2, "empty signing key in AppGateServer config") ErrAppGateConfigInvalidListeningEndpoint = sdkerrors.Register(codespace, 3, "invalid listening endpoint in AppGateServer config") - ErrAppGateConfigInvalidQueryNodeUrl = sdkerrors.Register(codespace, 4, "invalid pocket query node url in AppGateServer config") + ErrAppGateConfigInvalidQueryNodeGRPCUrl = sdkerrors.Register(codespace, 4, "invalid query node grpc url in AppGateServer config") + ErrAppGateConfigInvalidQueryNodeRPCUrl = sdkerrors.Register(codespace, 5, "invalid query node rpc url in AppGateServer config") + ErrAppGateConfigEmpty = sdkerrors.Register(codespace, 6, "empty AppGateServer config") ) diff --git a/pkg/client/query/supplierquerier.go b/pkg/client/query/supplierquerier.go index 4c4a91b2f..c622ad9e5 100644 --- a/pkg/client/query/supplierquerier.go +++ b/pkg/client/query/supplierquerier.go @@ -23,7 +23,7 @@ type supplierQuerier struct { // injecting the dependecies provided by the depinject.Config. // // Required dependencies: -// - clientCtx +// - grpc.ClientConn func NewSupplierQuerier(deps depinject.Config) (client.SupplierQueryClient, error) { supq := &supplierQuerier{} diff --git a/pkg/deps/config/suppliers.go b/pkg/deps/config/suppliers.go index ca439cbe3..4a9d536e9 100644 --- a/pkg/deps/config/suppliers.go +++ b/pkg/deps/config/suppliers.go @@ -2,6 +2,7 @@ package config import ( "context" + "net/url" "cosmossdk.io/depinject" cosmosclient "github.com/cosmos/cosmos-sdk/client" @@ -58,27 +59,23 @@ func NewSupplyLoggerFromCtx(ctx context.Context) SupplierFn { } } -// NewSupplyEventsQueryClientFn returns a new function which constructs an -// EventsQueryClient instance, with the given hostname converted into a websocket -// URL to subscribe to, and returns a new depinject.Config which is supplied -// with the given deps and the new EventsQueryClient. -func NewSupplyEventsQueryClientFn(queryHost string) SupplierFn { +// NewSupplyEventsQueryClientFn supplies a depinject config with an eventsQueryClient +// from the given queryNodeRPCURL. +func NewSupplyEventsQueryClientFn(queryNodeRPCURL *url.URL) SupplierFn { return func( _ context.Context, deps depinject.Config, _ *cobra.Command, ) (depinject.Config, error) { // Convert the host to a websocket URL - pocketNodeWebsocketURL := sdk.HostToWebsocketURL(queryHost) - eventsQueryClient := events.NewEventsQueryClient(pocketNodeWebsocketURL) + queryNodeWebsocketURL := sdk.HostToWebsocketURL(queryNodeRPCURL.Host) + eventsQueryClient := events.NewEventsQueryClient(queryNodeWebsocketURL) return depinject.Configs(deps, depinject.Supply(eventsQueryClient)), nil } } -// NewSupplyBlockClientFn returns a function which constructs a BlockClient -// instance and returns a new depinject.Config which is supplied with the -// given deps and the new BlockClient. +// NewSupplyBlockClientFn supplies a depinject config with a blockClient. func NewSupplyBlockClientFn() SupplierFn { return func( ctx context.Context, @@ -95,20 +92,27 @@ func NewSupplyBlockClientFn() SupplierFn { } } -// NewSupplyQueryClientContextFn returns a function with constructs a ClientContext -// instance with the given cmd and returns a new depinject.Config which is -// supplied with the given deps and the new ClientContext. -func NewSupplyQueryClientContextFn(pocketQueryNodeURL string) SupplierFn { +// NewSupplyQueryClientContextFn supplies a depinject config with a query +// +// ClientContext, a GRPC client connection, and a keyring from the given queryNodeGRPCURL. +func NewSupplyQueryClientContextFn(queryNodeGRPCURL *url.URL) SupplierFn { return func(_ context.Context, deps depinject.Config, cmd *cobra.Command, ) (depinject.Config, error) { - // Temporarily store the flag's current value - tmp := cosmosflags.FlagNode + // Temporarily store the flag's current value to be restored later, after + // the client context has been created with queryNodeGRPCURL. + // TODO_TECHDEBT(#223) Retrieve value from viper instead, once integrated. + tmpGRPC, err := cmd.Flags().GetString(cosmosflags.FlagGRPC) + if err != nil { + return nil, err + } - // Set --node flag to the pocketQueryNodeURL for the client context + // Set --grpc-addr flag to the pocketQueryNodeURL for the client context // This flag is read by cosmosclient.GetClientQueryContext. - if err := cmd.Flags().Set(cosmosflags.FlagNode, pocketQueryNodeURL); err != nil { + // Cosmos-SDK is expecting a GRPC address formatted as [:], + // so we only need to set the Host parameter of the URL to cosmosflags.FlagGRPC value. + if err := cmd.Flags().Set(cosmosflags.FlagGRPC, queryNodeGRPCURL.Host); err != nil { return nil, err } @@ -130,7 +134,7 @@ func NewSupplyQueryClientContextFn(pocketQueryNodeURL string) SupplierFn { // Restore the flag's original value in order for other components // to use the flag as expected. - if err := cmd.Flags().Set(cosmosflags.FlagNode, tmp); err != nil { + if err := cmd.Flags().Set(cosmosflags.FlagGRPC, tmpGRPC); err != nil { return nil, err } @@ -138,20 +142,46 @@ func NewSupplyQueryClientContextFn(pocketQueryNodeURL string) SupplierFn { } } -// NewSupplyTxClientContextFn returns a function with constructs a ClientContext -// instance with the given cmd and returns a new depinject.Config which is -// supplied with the given deps and the new ClientContext. -func NewSupplyTxClientContextFn(pocketTxNodeURL string) SupplierFn { +// NewSupplyTxClientContextFn supplies a depinject config with a TxClientContext +// from the given txNodeGRPCURL. +// TODO_TECHDEBT(#256): Remove this function once the as we may no longer +// need to supply a TxClientContext to the RelayMiner. +func NewSupplyTxClientContextFn( + queryNodeGRPCURL *url.URL, + txNodeRPCURL *url.URL, +) SupplierFn { return func(_ context.Context, deps depinject.Config, cmd *cobra.Command, ) (depinject.Config, error) { - // Temporarily store the flag's current value - tmp := cosmosflags.FlagNode + // Temporarily store the flag's current value to be restored later, after + // the client context has been created with txNodeRPCURL. + // TODO_TECHDEBT(#223) Retrieve value from viper instead, once integrated. + tmpNode, err := cmd.Flags().GetString(cosmosflags.FlagNode) + if err != nil { + return nil, err + } + + // Temporarily store the flag's current value to be restored later, after + // the client context has been created with queryNodeGRPCURL. + // TODO_TECHDEBT(#223) Retrieve value from viper instead, once integrated. + tmpGRPC, err := cmd.Flags().GetString(cosmosflags.FlagGRPC) + if err != nil { + return nil, err + } - // Set --node flag to the pocketTxNodeURL for the client context + // Set --node flag to the txNodeRPCURL for the client context // This flag is read by cosmosclient.GetClientTxContext. - if err := cmd.Flags().Set(cosmosflags.FlagNode, pocketTxNodeURL); err != nil { + if err := cmd.Flags().Set(cosmosflags.FlagNode, txNodeRPCURL.String()); err != nil { + return nil, err + } + + // Set --grpc-addr flag to the queryNodeGRPCURL for the client context + // This flag is read by cosmosclient.GetClientTxContext to query accounts + // for transaction signing. + // Cosmos-SDK is expecting a GRPC address formatted as [:], + // so we only need to set the Host parameter of the URL to cosmosflags.FlagGRPC value. + if err := cmd.Flags().Set(cosmosflags.FlagGRPC, queryNodeGRPCURL.Host); err != nil { return nil, err } @@ -171,7 +201,13 @@ func NewSupplyTxClientContextFn(pocketTxNodeURL string) SupplierFn { // Restore the flag's original value in order for other components // to use the flag as expected. - if err := cmd.Flags().Set(cosmosflags.FlagNode, tmp); err != nil { + if err := cmd.Flags().Set(cosmosflags.FlagGRPC, tmpGRPC); err != nil { + return nil, err + } + + // Restore the flag's original value in order for other components + // to use the flag as expected. + if err := cmd.Flags().Set(cosmosflags.FlagNode, tmpNode); err != nil { return nil, err } @@ -179,9 +215,7 @@ func NewSupplyTxClientContextFn(pocketTxNodeURL string) SupplierFn { } } -// NewSupplyAccountQuerierFn returns a function with constructs an AccountQuerier -// instance with the required dependencies and returns a new depinject.Config which -// is supplied with the given deps and the new AccountQuerier. +// NewSupplyAccountQuerierFn supplies a depinject config with an AccountQuerier. func NewSupplyAccountQuerierFn() SupplierFn { return func( _ context.Context, @@ -199,10 +233,7 @@ func NewSupplyAccountQuerierFn() SupplierFn { } } -// NewSupplyApplicationQuerierFn returns a function with constructs an -// ApplicationQuerier instance with the required dependencies and returns a new -// instance with the required dependencies and returns a new depinject.Config -// which is supplied with the given deps and the new ApplicationQuerier. +// NewSupplyApplicationQuerierFn supplies a depinject config with an ApplicationQuerier. func NewSupplyApplicationQuerierFn() SupplierFn { return func( _ context.Context, @@ -220,9 +251,7 @@ func NewSupplyApplicationQuerierFn() SupplierFn { } } -// NewSupplySessionQuerierFn returns a function which constructs a -// SessionQuerier instance with the required dependencies and returns a new -// depinject.Config which is supplied with the given deps and the new SessionQuerier. +// NewSupplySessionQuerierFn supplies a depinject config with a SessionQuerier. func NewSupplySessionQuerierFn() SupplierFn { return func( _ context.Context, @@ -240,10 +269,7 @@ func NewSupplySessionQuerierFn() SupplierFn { } } -// NewSupplySupplierQuerierFn returns a function which constructs a -// SupplierQuerier instance with the required dependencies and returns a new -// instance with the required dependencies and returns a new depinject.Config -// which is supplied with the given deps and the new SupplierQuerier. +// NewSupplySupplierQuerierFn supplies a depinject config with a SupplierQuerier. func NewSupplySupplierQuerierFn() SupplierFn { return func( _ context.Context, @@ -261,9 +287,7 @@ func NewSupplySupplierQuerierFn() SupplierFn { } } -// NewSupplyRingCacheFn returns a function with constructs a RingCache instance -// with the required dependencies and returns a new depinject.Config which is -// supplied with the given deps and the new RingCache. +// NewSupplyRingCacheFn supplies a depinject config with a RingCache. func NewSupplyRingCacheFn() SupplierFn { return func( _ context.Context, @@ -281,12 +305,9 @@ func NewSupplyRingCacheFn() SupplierFn { } } -// NewSupplyPOKTRollSDKFn returns a function which constructs a -// POKTRollSDK instance with the required dependencies and returns a new -// depinject.Config which is supplied with the given deps and the new POKTRollSDK. -func NewSupplyPOKTRollSDKFn( - signingKeyName string, -) SupplierFn { +// NewSupplyPOKTRollSDKFn supplies a depinject config with a POKTRollSDK given +// the signing key name. +func NewSupplyPOKTRollSDKFn(signingKeyName string) SupplierFn { return func( ctx context.Context, deps depinject.Config, @@ -310,11 +331,7 @@ func NewSupplyPOKTRollSDKFn( return nil, err } - config := &sdk.POKTRollSDKConfig{ - PrivateKey: privateKey, - Deps: deps, - } - + config := &sdk.POKTRollSDKConfig{PrivateKey: privateKey, Deps: deps} poktrollSDK, err := sdk.NewPOKTRollSDK(ctx, config) if err != nil { return nil, err diff --git a/pkg/relayer/cmd/cmd.go b/pkg/relayer/cmd/cmd.go index 3d37693d5..1dbc61b76 100644 --- a/pkg/relayer/cmd/cmd.go +++ b/pkg/relayer/cmd/cmd.go @@ -34,8 +34,13 @@ const omittedDefaultFlagValue = "explicitly omitting default" // TODO_CONSIDERATION: Consider moving all flags defined in `/pkg` to a `flags.go` file. var ( + // flagRelayMinerConfig is the variable containing the relay miner config filepath + // sourced from the `--config` flag. flagRelayMinerConfig string - flagCosmosNodeURL string + // flagNodeRPCURL is the variable containing the Cosmos node RPC URL flag value. + flagNodeRPCURL string + // flagNodeGRPCURL is the variable containing the Cosmos node GRPC URL flag value. + flagNodeGRPCURL string ) // RelayerCmd returns the Cobra command for running the relay miner. @@ -63,9 +68,11 @@ for such operations.`, cmd.Flags().StringVar(&flagRelayMinerConfig, "config", "", "The path to the relayminer config file") // Cosmos flags + // TODO_TECHDEBT(#256): Remove unneeded cosmos flags. cmd.Flags().String(cosmosflags.FlagKeyringBackend, "", "Select keyring's backend (os|file|kwallet|pass|test)") - cmd.Flags(). - StringVar(&flagCosmosNodeURL, cosmosflags.FlagNode, omittedDefaultFlagValue, "Register the default Cosmos node flag, which is needed to initialize the Cosmos query and tx contexts correctly. It can be used to override the `QueryNodeUrl` and `NetworkNodeUrl` fields in the config file if specified.") + cmd.Flags().StringVar(&flagNodeRPCURL, cosmosflags.FlagNode, omittedDefaultFlagValue, "Register the default Cosmos node flag, which is needed to initialize the Cosmos query and tx contexts correctly. It can be used to override the `QueryNodeRPCURL` and `TxNodeRPCURL` fields in the config file if specified.") + cmd.Flags().StringVar(&flagNodeGRPCURL, cosmosflags.FlagGRPC, omittedDefaultFlagValue, "Register the default Cosmos node grpc flag, which is needed to initialize the Cosmos query context with grpc correctly. It can be used to override the `QueryNodeGRPCURL` field in the config file if specified.") + cmd.Flags().Bool(cosmosflags.FlagGRPCInsecure, true, "Used to initialize the Cosmos query context with grpc security options. It can be used to override the `QueryNodeGRPCInsecure` field in the config file if specified.") return cmd } @@ -133,29 +140,44 @@ func setupRelayerDependencies( cmd *cobra.Command, relayMinerConfig *relayerconfig.RelayMinerConfig, ) (deps depinject.Config, err error) { - queryNodeURL := relayMinerConfig.QueryNodeUrl - networkNodeURL := relayMinerConfig.NetworkNodeUrl - // Override the config file's `QueryNodeUrl` and `NetworkNodeUrl` fields + queryNodeRPCUrl := relayMinerConfig.PocketNode.QueryNodeRPCUrl + queryNodeGRPCUrl := relayMinerConfig.PocketNode.QueryNodeGRPCUrl + txNodeRPCUrl := relayMinerConfig.PocketNode.TxNodeRPCUrl + + // Override the config file's `QueryNodeGRPCUrl` fields + // with the `--grpc-addr` flag if it was specified. + // TODO(#223) Remove this check once viper is used as SoT for overridable config values. + if flagNodeGRPCURL != omittedDefaultFlagValue { + parsedFlagNodeGRPCUrl, err := url.Parse(flagNodeGRPCURL) + if err != nil { + return nil, fmt.Errorf("failed to parse grpc query URL: %w", err) + } + queryNodeGRPCUrl = parsedFlagNodeGRPCUrl + } + + // Override the config file's `QueryNodeUrl` and `txNodeRPCUrl` fields // with the `--node` flag if it was specified. - if flagCosmosNodeURL != omittedDefaultFlagValue { - cosmosParsedURL, err := url.Parse(flagCosmosNodeURL) + // TODO(#223) Remove this check once viper is used as SoT for overridable config values. + if flagNodeRPCURL != omittedDefaultFlagValue { + parsedFlagNodeRPCUrl, err := url.Parse(flagNodeRPCURL) if err != nil { - return nil, fmt.Errorf("failed to parse Cosmos node URL: %w", err) + return nil, fmt.Errorf("failed to parse rpc query URL: %w", err) } - queryNodeURL = cosmosParsedURL - networkNodeURL = cosmosParsedURL + queryNodeRPCUrl = parsedFlagNodeRPCUrl + txNodeRPCUrl = parsedFlagNodeRPCUrl } + signingKeyName := relayMinerConfig.SigningKeyName - proxiedServiceEndpoints := relayMinerConfig.ProxiedServiceEndpoints + proxiedServiceEndpoints := relayMinerConfig.Proxies smtStorePath := relayMinerConfig.SmtStorePath supplierFuncs := []config.SupplierFn{ config.NewSupplyLoggerFromCtx(ctx), - config.NewSupplyEventsQueryClientFn(queryNodeURL.Host), // leaf - config.NewSupplyBlockClientFn(), // leaf - config.NewSupplyQueryClientContextFn(queryNodeURL.String()), // leaf + config.NewSupplyEventsQueryClientFn(queryNodeRPCUrl), // leaf + config.NewSupplyBlockClientFn(), // leaf + config.NewSupplyQueryClientContextFn(queryNodeGRPCUrl), // leaf supplyMiner, // leaf - config.NewSupplyTxClientContextFn(networkNodeURL.String()), // leaf + config.NewSupplyTxClientContextFn(queryNodeGRPCUrl, txNodeRPCUrl), // leaf config.NewSupplyAccountQuerierFn(), config.NewSupplyApplicationQuerierFn(), config.NewSupplySupplierQuerierFn(), @@ -272,7 +294,7 @@ func newSupplySupplierClientFn(signingKeyName string) config.SupplierFn { // is supplied with the given deps and the new RelayerProxy. func newSupplyRelayerProxyFn( signingKeyName string, - proxiedServiceEndpoints map[string]*url.URL, + proxiedServiceEndpoints map[string]*relayerconfig.RelayMinerProxyConfig, ) config.SupplierFn { return func( _ context.Context, diff --git a/pkg/relayer/config/errors.go b/pkg/relayer/config/errors.go index cabd22e76..eace596e3 100644 --- a/pkg/relayer/config/errors.go +++ b/pkg/relayer/config/errors.go @@ -3,11 +3,12 @@ package config import sdkerrors "cosmossdk.io/errors" var ( - codespace = "relayminer_config" - ErrRelayMinerConfigUnmarshalYAML = sdkerrors.Register(codespace, 1, "config reader cannot unmarshal yaml content") - ErrRelayMinerConfigInvalidQueryNodeUrl = sdkerrors.Register(codespace, 2, "invalid query node url in RelayMiner config") - ErrRelayMinerConfigInvalidNetworkNodeUrl = sdkerrors.Register(codespace, 3, "invalid network node url in RelayMiner config") - ErrRelayMinerConfigInvalidServiceEndpoint = sdkerrors.Register(codespace, 4, "invalid service endpoint in RelayMiner config") - ErrRelayMinerConfigInvalidSigningKeyName = sdkerrors.Register(codespace, 5, "invalid signing key name in RelayMiner config") - ErrRelayMinerConfigInvalidSmtStorePath = sdkerrors.Register(codespace, 6, "invalid smt store path in RelayMiner config") + codespace = "relayminer_config" + ErrRelayMinerConfigUnmarshalYAML = sdkerrors.Register(codespace, 1, "config reader cannot unmarshal yaml content") + ErrRelayMinerConfigInvalidNodeUrl = sdkerrors.Register(codespace, 2, "invalid node url in RelayMiner config") + ErrRelayMinerConfigInvalidSigningKeyName = sdkerrors.Register(codespace, 3, "invalid signing key name in RelayMiner config") + ErrRelayMinerConfigInvalidSmtStorePath = sdkerrors.Register(codespace, 4, "invalid smt store path in RelayMiner config") + ErrRelayMinerConfigEmpty = sdkerrors.Register(codespace, 5, "empty RelayMiner config") + ErrRelayMinerConfigInvalidSupplier = sdkerrors.Register(codespace, 6, "invalid supplier in RelayMiner config") + ErrRelayMinerConfigInvalidProxy = sdkerrors.Register(codespace, 7, "invalid proxy in RelayMiner config") ) diff --git a/pkg/relayer/config/pocket_node_config_hydrator.go b/pkg/relayer/config/pocket_node_config_hydrator.go new file mode 100644 index 000000000..88a57fa85 --- /dev/null +++ b/pkg/relayer/config/pocket_node_config_hydrator.go @@ -0,0 +1,56 @@ +package config + +import "net/url" + +// HydratePocketNodeUrls populates the pocket node fields of the RelayMinerConfig +// that are relevant to the "pocket_node" section in the config file. +func (relayMinerConfig *RelayMinerConfig) HydratePocketNodeUrls( + yamlPocketNodeConfig *YAMLRelayMinerPocketNodeConfig, +) error { + relayMinerConfig.PocketNode = &RelayMinerPocketNodeConfig{} + + if len(yamlPocketNodeConfig.TxNodeRPCUrl) == 0 { + return ErrRelayMinerConfigInvalidNodeUrl.Wrap("tx node rpc url is required") + } + + // Check if the pocket node rpc url is a valid URL + txNodeRPCUrl, err := url.Parse(yamlPocketNodeConfig.TxNodeRPCUrl) + if err != nil { + return ErrRelayMinerConfigInvalidNodeUrl.Wrapf( + "invalid tx node rpc url %s", + err.Error(), + ) + } + relayMinerConfig.PocketNode.TxNodeRPCUrl = txNodeRPCUrl + + // If the query node rpc url is empty, use the tx node rpc url + if len(yamlPocketNodeConfig.QueryNodeRPCUrl) == 0 { + relayMinerConfig.PocketNode.QueryNodeRPCUrl = relayMinerConfig.PocketNode.TxNodeRPCUrl + } else { + // If the query node rpc url is not empty, make sure it is a valid URL + queryNodeRPCUrl, err := url.Parse(yamlPocketNodeConfig.QueryNodeRPCUrl) + if err != nil { + return ErrRelayMinerConfigInvalidNodeUrl.Wrapf( + "invalid query node rpc url %s", + err.Error(), + ) + } + relayMinerConfig.PocketNode.QueryNodeRPCUrl = queryNodeRPCUrl + } + + if len(yamlPocketNodeConfig.QueryNodeGRPCUrl) == 0 { + return ErrRelayMinerConfigInvalidNodeUrl.Wrap("query node grpc url is required") + } + + // Check if the query node grpc url is a valid URL + queryNodeGRPCUrl, err := url.Parse(yamlPocketNodeConfig.QueryNodeGRPCUrl) + if err != nil { + return ErrRelayMinerConfigInvalidNodeUrl.Wrapf( + "invalid query node grpc url %s", + err.Error(), + ) + } + relayMinerConfig.PocketNode.QueryNodeGRPCUrl = queryNodeGRPCUrl + + return nil +} diff --git a/pkg/relayer/config/proxies_config_hydrator.go b/pkg/relayer/config/proxies_config_hydrator.go new file mode 100644 index 000000000..f3b7628ed --- /dev/null +++ b/pkg/relayer/config/proxies_config_hydrator.go @@ -0,0 +1,63 @@ +package config + +// HydrateProxies populates the proxies fields of the RelayMinerConfig that +// are relevant to the "proxies" section in the config file. +func (relayMinerConfig *RelayMinerConfig) HydrateProxies( + yamlProxyConfigs []YAMLRelayMinerProxyConfig, +) error { + // At least one proxy is required + if len(yamlProxyConfigs) == 0 { + return ErrRelayMinerConfigInvalidProxy.Wrap("no proxies provided") + } + + relayMinerConfig.Proxies = make(map[string]*RelayMinerProxyConfig) + + for _, yamlProxyConfig := range yamlProxyConfigs { + // Proxy name is required + if len(yamlProxyConfig.ProxyName) == 0 { + return ErrRelayMinerConfigInvalidProxy.Wrap("proxy name is required") + } + + // Proxy name should not be unique + if _, ok := relayMinerConfig.Proxies[yamlProxyConfig.ProxyName]; ok { + return ErrRelayMinerConfigInvalidProxy.Wrapf( + "duplicate porxy name %s", + yamlProxyConfig.ProxyName, + ) + } + + proxyConfig := &RelayMinerProxyConfig{ + ProxyName: yamlProxyConfig.ProxyName, + XForwardedHostLookup: yamlProxyConfig.XForwardedHostLookup, + Suppliers: make(map[string]*RelayMinerSupplierConfig), + } + + // Populate the proxy fields that are relevant to each supported proxy type + switch yamlProxyConfig.Type { + case "http": + if err := proxyConfig.parseHTTPProxyConfig(yamlProxyConfig); err != nil { + return err + } + default: + // Fail if the proxy type is not supported + return ErrRelayMinerConfigInvalidProxy.Wrapf( + "invalid proxy type %s", + yamlProxyConfig.Type, + ) + } + + switch yamlProxyConfig.Type { + case "http": + proxyConfig.Type = ProxyTypeHTTP + default: + ErrRelayMinerConfigInvalidProxy.Wrapf( + "invalid proxy type %s", + yamlProxyConfig.Type, + ) + } + + relayMinerConfig.Proxies[proxyConfig.ProxyName] = proxyConfig + } + + return nil +} diff --git a/pkg/relayer/config/proxy_http_config_parser.go b/pkg/relayer/config/proxy_http_config_parser.go new file mode 100644 index 000000000..a8f655f14 --- /dev/null +++ b/pkg/relayer/config/proxy_http_config_parser.go @@ -0,0 +1,73 @@ +package config + +import ( + "fmt" + "net/url" +) + +// parseHTTPProxyConfig populates the proxy fields of the target structure that +// are relevant to the "http" type in the proxy section of the config file. +// This function alters the target RelayMinerProxyConfig structure as a side effect. +func (proxyConfig *RelayMinerProxyConfig) parseHTTPProxyConfig( + yamlProxyConfig YAMLRelayMinerProxyConfig, +) error { + // Check if the proxy host is a valid URL. + // Since `yamlProxyConfig.Host` is a string representing the host, we need to + // prepend it with the "http://" scheme to make it a valid URL; we end up + // using the `Host` field of the resulting `url.URL` struct, so the prepended + // scheme is irrelevant. + proxyUrl, err := url.Parse(fmt.Sprintf("http://%s", yamlProxyConfig.Host)) + if err != nil { + return ErrRelayMinerConfigInvalidProxy.Wrapf( + "invalid proxy host %s", + err.Error(), + ) + } + + if proxyUrl.Host == "" { + return ErrRelayMinerConfigInvalidProxy.Wrap("empty proxy host") + } + + proxyConfig.Host = proxyUrl.Host + return nil +} + +// parseHTTPSupplierConfig populates the supplier fields of the target structure +// that are relevant to the "http" type in the supplier section of the config file. +// This function alters the target RelayMinerSupplierServiceConfig structure +// as a side effect. +func (supplierServiceConfig *RelayMinerSupplierServiceConfig) parseHTTPSupplierConfig( + yamlSupplierServiceConfig YAMLRelayMinerSupplierServiceConfig, +) error { + // Check if the supplier url is not empty + if len(yamlSupplierServiceConfig.Url) == 0 { + return ErrRelayMinerConfigInvalidSupplier.Wrap("empty supplier url") + } + + // Check if the supplier url is a valid URL + supplierServiceUrl, err := url.Parse(yamlSupplierServiceConfig.Url) + if err != nil { + return ErrRelayMinerConfigInvalidSupplier.Wrapf( + "invalid supplier url %s", + err.Error(), + ) + } + + supplierServiceConfig.Url = supplierServiceUrl + + // If the Authentication section is not empty, populate the supplier service + // authentication fields + if yamlSupplierServiceConfig.Authentication != (YAMLRelayMinerSupplierServiceAuthentication{}) { + supplierServiceConfig.Authentication = &RelayMinerSupplierServiceAuthentication{ + Username: yamlSupplierServiceConfig.Authentication.Username, + Password: yamlSupplierServiceConfig.Authentication.Password, + } + } + + // If the Headers section is not empty, populate the supplier service headers fields + if yamlSupplierServiceConfig.Headers != nil { + supplierServiceConfig.Headers = yamlSupplierServiceConfig.Headers + } + + return nil +} diff --git a/pkg/relayer/config/relayminer_configs_reader.go b/pkg/relayer/config/relayminer_configs_reader.go index bd1012f69..8f7c3b5ca 100644 --- a/pkg/relayer/config/relayminer_configs_reader.go +++ b/pkg/relayer/config/relayminer_configs_reader.go @@ -1,102 +1,77 @@ package config -import ( - "net/url" - - yaml "gopkg.in/yaml.v2" - - "github.com/pokt-network/poktroll/pkg/sdk" -) - -// YAMLRelayMinerConfig is the structure used to unmarshal the RelayMiner config file -// TODO_DOCUMENT(@red-0ne): Add proper README documentation for yaml config files. -type YAMLRelayMinerConfig struct { - QueryNodeUrl string `yaml:"query_node_url"` - NetworkNodeUrl string `yaml:"network_node_url"` - PocketNodeWebsocketUrl string `yaml:"pocket_node_websocket_url"` - SigningKeyName string `yaml:"signing_key_name"` - ProxiedServiceEndpoints map[string]string `yaml:"proxied_service_endpoints"` - SmtStorePath string `yaml:"smt_store_path"` -} - -// RelayMinerConfig is the structure describing the RelayMiner config -type RelayMinerConfig struct { - QueryNodeUrl *url.URL - NetworkNodeUrl *url.URL - PocketNodeWebsocketUrl string - SigningKeyName string - ProxiedServiceEndpoints map[string]*url.URL - SmtStorePath string -} +import yaml "gopkg.in/yaml.v2" // ParseRelayMinerConfigs parses the relay miner config file into a RelayMinerConfig func ParseRelayMinerConfigs(configContent []byte) (*RelayMinerConfig, error) { - var yamlRelayMinerConfig YAMLRelayMinerConfig + var ( + yamlRelayMinerConfig YAMLRelayMinerConfig + relayMinerConfig = &RelayMinerConfig{} + ) + + // The config file should not be empty + if len(configContent) == 0 { + return nil, ErrRelayMinerConfigEmpty + } // Unmarshal the stake config file into a yamlAppGateConfig if err := yaml.Unmarshal(configContent, &yamlRelayMinerConfig); err != nil { - return nil, ErrRelayMinerConfigUnmarshalYAML.Wrapf("%s", err) - } - - // Check that the query node URL is provided - if yamlRelayMinerConfig.QueryNodeUrl == "" { - return nil, ErrRelayMinerConfigInvalidQueryNodeUrl.Wrapf("query node url is required") + return nil, ErrRelayMinerConfigUnmarshalYAML.Wrap(err.Error()) } - // Parse the query node URL - queryNodeUrl, err := url.Parse(yamlRelayMinerConfig.QueryNodeUrl) - if err != nil { - return nil, ErrRelayMinerConfigInvalidQueryNodeUrl.Wrapf("%s", err) + // Top level section + // SigningKeyName is required + if len(yamlRelayMinerConfig.SigningKeyName) == 0 { + return nil, ErrRelayMinerConfigInvalidSigningKeyName } + relayMinerConfig.SigningKeyName = yamlRelayMinerConfig.SigningKeyName - // Check that the network node URL is provided - if yamlRelayMinerConfig.NetworkNodeUrl == "" { - return nil, ErrRelayMinerConfigInvalidNetworkNodeUrl.Wrapf("network node url is required") + // SmtStorePath is required + if len(yamlRelayMinerConfig.SmtStorePath) == 0 { + return nil, ErrRelayMinerConfigInvalidSmtStorePath } + relayMinerConfig.SmtStorePath = yamlRelayMinerConfig.SmtStorePath - // Parse the network node URL - networkNodeUrl, err := url.Parse(yamlRelayMinerConfig.NetworkNodeUrl) - if err != nil { - return nil, ErrRelayMinerConfigInvalidNetworkNodeUrl.Wrapf("%s", err) + // Hydrate the pocket node urls + if err := relayMinerConfig.HydratePocketNodeUrls(&yamlRelayMinerConfig.PocketNode); err != nil { + return nil, err } - // Parse the websocket URL of the Pocket Node to connect to for subscribing to on-chain events. - pocketNodeWebsocketUrl := sdk.HostToWebsocketURL(queryNodeUrl.Host) - - if yamlRelayMinerConfig.SigningKeyName == "" { - return nil, ErrRelayMinerConfigInvalidSigningKeyName + // Hydrate the proxies + if err := relayMinerConfig.HydrateProxies(yamlRelayMinerConfig.Proxies); err != nil { + return nil, err } - if yamlRelayMinerConfig.SmtStorePath == "" { - return nil, ErrRelayMinerConfigInvalidSmtStorePath + // Hydrate the suppliers + if err := relayMinerConfig.HydrateSuppliers(yamlRelayMinerConfig.Suppliers); err != nil { + return nil, err } - if yamlRelayMinerConfig.ProxiedServiceEndpoints == nil { - return nil, ErrRelayMinerConfigInvalidServiceEndpoint.Wrapf("proxied service endpoints are required") + // Check if proxies are referencing hosts more than once + if err := relayMinerConfig.EnsureUniqueHosts(); err != nil { + return nil, err } - if len(yamlRelayMinerConfig.ProxiedServiceEndpoints) == 0 { - return nil, ErrRelayMinerConfigInvalidServiceEndpoint.Wrapf("no proxied service endpoints provided") - } + return relayMinerConfig, nil +} - // Parse the proxied service endpoints - proxiedServiceEndpoints := make(map[string]*url.URL, len(yamlRelayMinerConfig.ProxiedServiceEndpoints)) - for serviceId, endpointUrl := range yamlRelayMinerConfig.ProxiedServiceEndpoints { - endpoint, err := url.Parse(endpointUrl) - if err != nil { - return nil, ErrRelayMinerConfigInvalidServiceEndpoint.Wrapf("%s", err) +// EnsureUniqueHosts checks if each proxy is referencing a host more than once +func (relayMinerConfig *RelayMinerConfig) EnsureUniqueHosts() error { + for _, proxyConfig := range relayMinerConfig.Proxies { + existingHosts := make(map[string]bool) + for _, supplierConfig := range proxyConfig.Suppliers { + for _, host := range supplierConfig.Hosts { + if _, ok := existingHosts[host]; ok { + return ErrRelayMinerConfigInvalidProxy.Wrapf( + "duplicate host %s in proxy %s", + host, + proxyConfig.ProxyName, + ) + } + existingHosts[host] = true + } } - proxiedServiceEndpoints[serviceId] = endpoint - } - - relayMinerCMDConfig := &RelayMinerConfig{ - QueryNodeUrl: queryNodeUrl, - NetworkNodeUrl: networkNodeUrl, - PocketNodeWebsocketUrl: pocketNodeWebsocketUrl, - SigningKeyName: yamlRelayMinerConfig.SigningKeyName, - ProxiedServiceEndpoints: proxiedServiceEndpoints, - SmtStorePath: yamlRelayMinerConfig.SmtStorePath, } - return relayMinerCMDConfig, nil + return nil } diff --git a/pkg/relayer/config/relayminer_configs_reader_test.go b/pkg/relayer/config/relayminer_configs_reader_test.go index 20bc32de7..6b8bc3c60 100644 --- a/pkg/relayer/config/relayminer_configs_reader_test.go +++ b/pkg/relayer/config/relayminer_configs_reader_test.go @@ -16,7 +16,7 @@ func Test_ParseRelayMinerConfigs(t *testing.T) { tests := []struct { desc string - inputConfig string + inputConfigYAML string expectedError *sdkerrors.Error expectedConfig *config.RelayMinerConfig @@ -25,172 +25,1127 @@ func Test_ParseRelayMinerConfigs(t *testing.T) { { desc: "valid: relay miner config", - inputConfig: ` - query_node_url: tcp://localhost:26657 - network_node_url: tcp://127.0.0.1:36657 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 signing_key_name: servicer1 - proxied_service_endpoints: - anvil: http://anvil:8080 - svc1: http://svc1:8080 smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + authentication: + username: user + password: pwd + headers: {} + hosts: + - tcp://ethereum.devnet1.poktroll.com + - tcp://ethereum + proxy_names: + - http-example `, expectedError: nil, expectedConfig: &config.RelayMinerConfig{ - QueryNodeUrl: &url.URL{Scheme: "tcp", Host: "localhost:26657"}, - NetworkNodeUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36657"}, + PocketNode: &config.RelayMinerPocketNodeConfig{ + QueryNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36657"}, + QueryNodeGRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36658"}, + TxNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36659"}, + }, + SigningKeyName: "servicer1", + SmtStorePath: "smt_stores", + Proxies: map[string]*config.RelayMinerProxyConfig{ + "http-example": { + ProxyName: "http-example", + Host: "127.0.0.1:8080", + Type: config.ProxyTypeHTTP, + XForwardedHostLookup: false, + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "ethereum": { + ServiceId: "ethereum", + Type: config.ProxyTypeHTTP, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "anvil.servicer:8545"}, + Authentication: &config.RelayMinerSupplierServiceAuthentication{ + Username: "user", + Password: "pwd", + }, + Headers: map[string]string{}, + }, + Hosts: []string{ + "ethereum.devnet1.poktroll.com", + "ethereum", + }, + }, + }, + }, + }, + }, + }, + { + desc: "valid: multiple suppliers, single proxy", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + authentication: + username: user + password: pwd + headers: {} + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + - service_id: 7b-llm-model + type: http + service_config: + url: http://llama-endpoint + hosts: + - tcp://7b-llm-model.devnet1.poktroll.com + - tcp://7b-llm-model + proxy_names: + - http-example + `, + + expectedError: nil, + expectedConfig: &config.RelayMinerConfig{ + PocketNode: &config.RelayMinerPocketNodeConfig{ + QueryNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36657"}, + QueryNodeGRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36658"}, + TxNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36659"}, + }, + SigningKeyName: "servicer1", + SmtStorePath: "smt_stores", + Proxies: map[string]*config.RelayMinerProxyConfig{ + "http-example": { + ProxyName: "http-example", + Host: "127.0.0.1:8080", + Type: config.ProxyTypeHTTP, + XForwardedHostLookup: false, + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "ethereum": { + ServiceId: "ethereum", + Type: config.ProxyTypeHTTP, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "anvil.servicer:8545"}, + Authentication: &config.RelayMinerSupplierServiceAuthentication{ + Username: "user", + Password: "pwd", + }, + Headers: map[string]string{}, + }, + Hosts: []string{ + "ethereum.devnet1.poktroll.com", + "ethereum", + }, + }, + "7b-llm-model": { + ServiceId: "7b-llm-model", + Type: config.ProxyTypeHTTP, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "llama-endpoint"}, + }, + Hosts: []string{ + "7b-llm-model.devnet1.poktroll.com", + "7b-llm-model", + }, + }, + }, + }, + }, + }, + }, + { + desc: "valid: multiple proxies for a single supplier, no auth", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: first-proxy + host: 127.0.0.1:8080 + type: http + - proxy_name: second-proxy + host: 127.0.0.1:8081 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - first-proxy + - second-proxy + `, + + expectedError: nil, + expectedConfig: &config.RelayMinerConfig{ + PocketNode: &config.RelayMinerPocketNodeConfig{ + QueryNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36657"}, + QueryNodeGRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36658"}, + TxNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36659"}, + }, + SigningKeyName: "servicer1", + SmtStorePath: "smt_stores", + Proxies: map[string]*config.RelayMinerProxyConfig{ + "first-proxy": { + ProxyName: "first-proxy", + Host: "127.0.0.1:8080", + Type: config.ProxyTypeHTTP, + XForwardedHostLookup: false, + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "ethereum": { + ServiceId: "ethereum", + Type: config.ProxyTypeHTTP, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "anvil.servicer:8545"}, + }, + Hosts: []string{ + "ethereum.devnet1.poktroll.com", + }, + }, + }, + }, + "second-proxy": { + ProxyName: "second-proxy", + Host: "127.0.0.1:8081", + Type: config.ProxyTypeHTTP, + XForwardedHostLookup: false, + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "ethereum": { + ServiceId: "ethereum", + Type: config.ProxyTypeHTTP, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "anvil.servicer:8545"}, + }, + Hosts: []string{ + "ethereum.devnet1.poktroll.com", + }, + }, + }, + }, + }, + }, + }, + { + desc: "valid: relay miner config with query node rpc url defaulting to tx node rpc url", + + inputConfigYAML: ` + pocket_node: + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + - tcp://ethereum + proxy_names: + - http-example + `, + + expectedError: nil, + expectedConfig: &config.RelayMinerConfig{ + PocketNode: &config.RelayMinerPocketNodeConfig{ + QueryNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36659"}, + QueryNodeGRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36658"}, + TxNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36659"}, + }, + SigningKeyName: "servicer1", + SmtStorePath: "smt_stores", + Proxies: map[string]*config.RelayMinerProxyConfig{ + "http-example": { + ProxyName: "http-example", + Host: "127.0.0.1:8080", + Type: config.ProxyTypeHTTP, + XForwardedHostLookup: false, + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "ethereum": { + ServiceId: "ethereum", + Type: config.ProxyTypeHTTP, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "anvil.servicer:8545"}, + }, + Hosts: []string{ + "ethereum.devnet1.poktroll.com", + "ethereum", + }, + }, + }, + }, + }, + }, + }, + { + desc: "valid: relay miner config with x_forwarded_host_lookup set to true", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + x_forwarded_host_lookup: true + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + - tcp://ethereum + proxy_names: + - http-example + `, + + expectedError: nil, + expectedConfig: &config.RelayMinerConfig{ + PocketNode: &config.RelayMinerPocketNodeConfig{ + QueryNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36657"}, + QueryNodeGRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36658"}, + TxNodeRPCUrl: &url.URL{Scheme: "tcp", Host: "127.0.0.1:36659"}, + }, SigningKeyName: "servicer1", - ProxiedServiceEndpoints: map[string]*url.URL{ - "anvil": {Scheme: "http", Host: "anvil:8080"}, - "svc1": {Scheme: "http", Host: "svc1:8080"}, + SmtStorePath: "smt_stores", + Proxies: map[string]*config.RelayMinerProxyConfig{ + "http-example": { + ProxyName: "http-example", + Host: "127.0.0.1:8080", + Type: config.ProxyTypeHTTP, + XForwardedHostLookup: true, + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "ethereum": { + ServiceId: "ethereum", + Type: config.ProxyTypeHTTP, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "anvil.servicer:8545"}, + }, + Hosts: []string{ + "ethereum.devnet1.poktroll.com", + "ethereum", + }, + }, + }, + }, }, - SmtStorePath: "smt_stores", }, }, // Invalid Configs { - desc: "invalid: invalid network node url", + desc: "invalid: invalid tx node grpc url", - inputConfig: ` - query_node_url: tcp://localhost:26657 - network_node_url: &tcp://127.0.0.1:36657 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: &tcp://127.0.0.1:36659 signing_key_name: servicer1 - proxied_service_endpoints: - anvil: http://anvil:8080 - svc1: http://svc1:8080 smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example `, - expectedError: config.ErrRelayMinerConfigInvalidNetworkNodeUrl, + expectedError: config.ErrRelayMinerConfigInvalidNodeUrl, }, { - desc: "invalid: missing network node url", + desc: "invalid: missing tx node grpc url", - inputConfig: ` - query_node_url: tcp://localhost:26657 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + # explicitly omitted tx node grpc url + query_node_grpc_url: tcp://127.0.0.1:36658 signing_key_name: servicer1 - proxied_service_endpoints: - anvil: http://anvil:8080 - svc1: http://svc1:8080 smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example `, - expectedError: config.ErrRelayMinerConfigInvalidNetworkNodeUrl, + expectedError: config.ErrRelayMinerConfigInvalidNodeUrl, }, { - desc: "invalid: invalid query node url", + desc: "invalid: invalid query node grpc url", - inputConfig: ` - query_node_url: &tcp://localhost:26657 - network_node_url: tcp://127.0.0.1:36657 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: &tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 signing_key_name: servicer1 - proxied_service_endpoints: - anvil: http://anvil:8080 - svc1: http://svc1:8080 smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example `, - expectedError: config.ErrRelayMinerConfigInvalidNetworkNodeUrl, + expectedError: config.ErrRelayMinerConfigInvalidNodeUrl, }, { - desc: "invalid: missing query node url", + desc: "invalid: invalid query node rpc url", - inputConfig: ` - network_node_url: tcp://127.0.0.1:36657 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: &tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 signing_key_name: servicer1 - proxied_service_endpoints: - anvil: http://anvil:8080 - svc1: http://svc1:8080 smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example `, - expectedError: config.ErrRelayMinerConfigInvalidNetworkNodeUrl, + expectedError: config.ErrRelayMinerConfigInvalidNodeUrl, + }, + { + desc: "invalid: missing query node grpc url", + + inputConfigYAML: ` + pocket_node: + # explicitly omitted query node rpc url + query_node_rpc_url: tcp://127.0.0.1:36657 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidNodeUrl, }, { desc: "invalid: missing signing key name", - inputConfig: ` - query_node_url: tcp://localhost:26657 - network_node_url: &tcp://127.0.0.1:36657 - signing_key_name: - proxied_service_endpoints: - anvil: http://anvil:8080 - svc1: http://svc1:8080 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + # explicitly omitted signing key name smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example `, - expectedError: config.ErrRelayMinerConfigInvalidNetworkNodeUrl, + expectedError: config.ErrRelayMinerConfigInvalidSigningKeyName, }, { desc: "invalid: missing smt store path", - inputConfig: ` - query_node_url: tcp://localhost:26657 - network_node_url: &tcp://127.0.0.1:36657 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + # explicitly omitted smt store path + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidSmtStorePath, + }, + { + desc: "invalid: missing proxies section", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + # explicitly omitted proxies section + suppliers: + - proxy_name: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, + }, + { + desc: "invalid: empty proxies section", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: # explicitly empty proxies section + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, + }, + { + desc: "invalid: omitted proxy name", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + # explicitly omitted proxy name + - host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, + }, + { + desc: "invalid: empty proxy name", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: # explicitly empty proxy name + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, + }, + { + desc: "invalid: missing http proxy host", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + # explicitly missing proxy host + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, + }, + { + desc: "invalid: empty http proxy host", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: # explicitly empty proxy host + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, + }, + { + desc: "invalid: missing proxy type", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + # explicitly missing proxy type + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, + }, + { + desc: "invalid: empty proxy type", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: # explicitly empty proxy type + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, + }, + { + desc: "invalid: unsupported proxy type", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: unsupported + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, + }, + { + desc: "invalid: missing supplier name", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 signing_key_name: servicer1 - proxied_service_endpoints: - anvil: http://anvil:8080 - svc1: http://svc1:8080 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + # explicitly missing supplier name + - type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example `, - expectedError: config.ErrRelayMinerConfigInvalidNetworkNodeUrl, + expectedError: config.ErrRelayMinerConfigInvalidSupplier, }, { - desc: "invalid: empty proxied service endpoints", + desc: "invalid: empty supplier name", - inputConfig: ` - query_node_url: tcp://localhost:26657 - network_node_url: &tcp://127.0.0.1:36657 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 signing_key_name: servicer1 - proxied_service_endpoints: smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: # explicitly empty supplier name + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example `, - expectedError: config.ErrRelayMinerConfigInvalidNetworkNodeUrl, + expectedError: config.ErrRelayMinerConfigInvalidSupplier, }, { - desc: "invalid: invalid proxied service endpoint", + desc: "invalid: unsupported supplier type", - inputConfig: ` - query_node_url: tcp://localhost:26657 - network_node_url: &tcp://127.0.0.1:36657 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 signing_key_name: servicer1 - proxied_service_endpoints: - anvil: &http://anvil:8080 - svc1: http://svc1:8080 smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: unsupported + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example `, - expectedError: config.ErrRelayMinerConfigInvalidNetworkNodeUrl, + expectedError: config.ErrRelayMinerConfigInvalidSupplier, }, { - desc: "invalid: invalid network node url", + desc: "invalid: missing supplier type", - inputConfig: ` - query_node_url: tcp://localhost:26657 - network_node_url: &tcp://127.0.0.1:36657 + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 signing_key_name: servicer1 smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + # explicitly missing supplier type + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example `, - expectedError: config.ErrRelayMinerConfigUnmarshalYAML, + expectedError: config.ErrRelayMinerConfigInvalidSupplier, + }, + { + desc: "invalid: empty supplier type", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: # explicitly empty supplier type + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidSupplier, + }, + { + desc: "invalid: bad supplier service config url", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: &http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidSupplier, + }, + { + desc: "invalid: empty supplier service config url", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: # explicitly empty supplier service config url + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidSupplier, + }, + { + desc: "invalid: missing supplier service config url", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + # explicitly missing supplier service config url + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidSupplier, + }, + { + desc: "invalid: bad supplier host", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - &tcp://ethereum.devnet1.poktroll.com + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidSupplier, + }, + { + desc: "invalid: blank supplier host", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - # explicitly blank supplier host + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidSupplier, + }, + { + desc: "invalid: empty supplier proxy references", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://ethereum.devnet1.poktroll.com + proxy_names: + - bad-proxy-name + `, + + expectedError: config.ErrRelayMinerConfigInvalidSupplier, + }, + { + desc: "invalid: empty supplier proxy references", + + inputConfigYAML: ` + pocket_node: + query_node_rpc_url: tcp://127.0.0.1:36657 + query_node_grpc_url: tcp://127.0.0.1:36658 + tx_node_rpc_url: tcp://127.0.0.1:36659 + signing_key_name: servicer1 + smt_store_path: smt_stores + proxies: + - proxy_name: http-example + host: 127.0.0.1:8080 + type: http + suppliers: + - service_id: ethereum + type: http + service_config: + url: http://anvil.servicer:8545 + hosts: + - tcp://devnet1.poktroll.com # hosts for both suppliers are the same + proxy_names: + - http-example + - service_id: avax + type: http + service_config: + url: http://avax.servicer:8545 + hosts: + - tcp://devnet1.poktroll.com # hosts for both suppliers are the same + proxy_names: + - http-example + `, + + expectedError: config.ErrRelayMinerConfigInvalidProxy, }, { desc: "invalid: empty RelayMiner config file", - inputConfig: ``, + inputConfigYAML: ``, - expectedError: config.ErrRelayMinerConfigUnmarshalYAML, + expectedError: config.ErrRelayMinerConfigEmpty, }, + // TODO_NB: Test for supplier and proxy types mismatch once we have more + // than one proxy type. } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - normalizedConfig := yaml.NormalizeYAMLIndentation(tt.inputConfig) + normalizedConfig := yaml.NormalizeYAMLIndentation(tt.inputConfigYAML) config, err := config.ParseRelayMinerConfigs([]byte(normalizedConfig)) if tt.expectedError != nil { - require.Error(t, err) + require.ErrorIs(t, err, tt.expectedError) require.Nil(t, config) stat, ok := status.FromError(tt.expectedError) require.True(t, ok) @@ -201,13 +1156,109 @@ func Test_ParseRelayMinerConfigs(t *testing.T) { require.NoError(t, err) - require.Equal(t, tt.expectedConfig.QueryNodeUrl.String(), config.QueryNodeUrl.String()) - require.Equal(t, tt.expectedConfig.NetworkNodeUrl.String(), config.NetworkNodeUrl.String()) - require.Equal(t, tt.expectedConfig.SigningKeyName, config.SigningKeyName) - require.Equal(t, tt.expectedConfig.SmtStorePath, config.SmtStorePath) - require.Equal(t, len(tt.expectedConfig.ProxiedServiceEndpoints), len(config.ProxiedServiceEndpoints)) - for serviceId, endpoint := range tt.expectedConfig.ProxiedServiceEndpoints { - require.Equal(t, endpoint.String(), config.ProxiedServiceEndpoints[serviceId].String()) + require.Equal( + t, + tt.expectedConfig.SigningKeyName, + config.SigningKeyName, + ) + + require.Equal( + t, + tt.expectedConfig.SmtStorePath, + config.SmtStorePath, + ) + + require.Equal( + t, + tt.expectedConfig.PocketNode.QueryNodeGRPCUrl.String(), + config.PocketNode.QueryNodeGRPCUrl.String(), + ) + + require.Equal( + t, + tt.expectedConfig.PocketNode.QueryNodeRPCUrl.String(), + config.PocketNode.QueryNodeRPCUrl.String(), + ) + + require.Equal( + t, + tt.expectedConfig.PocketNode.TxNodeRPCUrl.String(), + config.PocketNode.TxNodeRPCUrl.String(), + ) + + for proxyName, proxy := range tt.expectedConfig.Proxies { + require.Equal( + t, + proxy.ProxyName, + config.Proxies[proxyName].ProxyName, + ) + + require.Equal( + t, + proxy.Host, + config.Proxies[proxyName].Host, + ) + + require.Equal( + t, + proxy.Type, + config.Proxies[proxyName].Type, + ) + + for supplierName, supplier := range proxy.Suppliers { + require.Equal( + t, + supplier.ServiceId, + config.Proxies[proxyName].Suppliers[supplierName].ServiceId, + ) + + require.Equal( + t, + supplier.Type, + config.Proxies[proxyName].Suppliers[supplierName].Type, + ) + + require.Equal( + t, + supplier.ServiceConfig.Url.String(), + config.Proxies[proxyName].Suppliers[supplierName].ServiceConfig.Url.String(), + ) + + if supplier.ServiceConfig.Authentication != nil { + require.NotNil( + t, + config.Proxies[proxyName].Suppliers[supplierName].ServiceConfig.Authentication, + ) + + require.Equal( + t, + supplier.ServiceConfig.Authentication.Username, + config.Proxies[proxyName].Suppliers[supplierName].ServiceConfig.Authentication.Username, + ) + + require.Equal( + t, + supplier.ServiceConfig.Authentication.Password, + config.Proxies[proxyName].Suppliers[supplierName].ServiceConfig.Authentication.Password, + ) + } + + for headerKey, headerValue := range supplier.ServiceConfig.Headers { + require.Equal( + t, + headerValue, + config.Proxies[proxyName].Suppliers[supplierName].ServiceConfig.Headers[headerKey], + ) + } + + for i, host := range supplier.Hosts { + require.Contains( + t, + host, + config.Proxies[proxyName].Suppliers[supplierName].Hosts[i], + ) + } + } } }) } diff --git a/pkg/relayer/config/supplier_hydrator.go b/pkg/relayer/config/supplier_hydrator.go new file mode 100644 index 000000000..0b54a36f0 --- /dev/null +++ b/pkg/relayer/config/supplier_hydrator.go @@ -0,0 +1,82 @@ +package config + +import "net/url" + +// HydrateSupplier populates a single supplier's fields of the RelayMinerConfig +// that are relevant to each supplier in the "suppliers" section of the config file. +func (supplierConfig *RelayMinerSupplierConfig) HydrateSupplier( + yamlSupplierConfig YAMLRelayMinerSupplierConfig, +) error { + // Supplier name is required + if len(yamlSupplierConfig.ServiceId) == 0 { + return ErrRelayMinerConfigInvalidSupplier.Wrap("supplier name is required") + } + supplierConfig.ServiceId = yamlSupplierConfig.ServiceId + + // Supplier hosts + supplierConfig.Hosts = []string{} + existingHosts := make(map[string]bool) + for _, host := range yamlSupplierConfig.Hosts { + // Check if the supplier host is empty + if len(host) == 0 { + return ErrRelayMinerConfigInvalidSupplier.Wrap("empty supplier host") + } + + // Check if the supplier host is a valid URL + supplierHost, err := url.Parse(host) + if err != nil { + return ErrRelayMinerConfigInvalidSupplier.Wrapf( + "invalid supplier host %s", + host, + ) + } + + // Check if the supplier host is unique + if _, ok := existingHosts[supplierHost.Host]; ok { + return ErrRelayMinerConfigInvalidSupplier.Wrapf( + "duplicate supplier host %s", + host, + ) + } + existingHosts[supplierHost.Host] = true + + // Add the supplier host to the suppliers list + supplierConfig.Hosts = append(supplierConfig.Hosts, supplierHost.Host) + } + + // Add a default host which corresponds to the supplier name if it is not + // already in the list + if _, ok := existingHosts[supplierConfig.ServiceId]; !ok { + supplierConfig.Hosts = append(supplierConfig.Hosts, supplierConfig.ServiceId) + } + + // Populate the supplier service fields that are relevant to each supported + // supplier type. + // If other supplier types are added in the future, they should be handled + // by their own functions. + supplierConfig.ServiceConfig = &RelayMinerSupplierServiceConfig{} + switch yamlSupplierConfig.Type { + case "http": + supplierConfig.Type = ProxyTypeHTTP + if err := supplierConfig.ServiceConfig. + parseHTTPSupplierConfig(yamlSupplierConfig.ServiceConfig); err != nil { + return err + } + default: + // Fail if the supplier type is not supported + return ErrRelayMinerConfigInvalidSupplier.Wrapf( + "invalid supplier type %s", + yamlSupplierConfig.Type, + ) + } + + // Check if the supplier has proxies + if len(yamlSupplierConfig.ProxyNames) == 0 { + return ErrRelayMinerConfigInvalidSupplier.Wrapf( + "supplier %s has no proxies", + supplierConfig.ServiceId, + ) + } + + return nil +} diff --git a/pkg/relayer/config/suppliers_config_hydrator.go b/pkg/relayer/config/suppliers_config_hydrator.go new file mode 100644 index 000000000..b8f0f4fa7 --- /dev/null +++ b/pkg/relayer/config/suppliers_config_hydrator.go @@ -0,0 +1,51 @@ +package config + +// HydrateSuppliers populates the suppliers fields of the RelayMinerConfig that +// are relevant to the "suppliers" section in the config file. +func (relayMinerConfig *RelayMinerConfig) HydrateSuppliers( + yamlSupplierConfigs []YAMLRelayMinerSupplierConfig, +) error { + existingSuppliers := make(map[string]bool) + for _, yamlSupplierConfig := range yamlSupplierConfigs { + // Hydrate and validate each supplier in the suppliers list of the config file. + supplierConfig := &RelayMinerSupplierConfig{} + if err := supplierConfig.HydrateSupplier(yamlSupplierConfig); err != nil { + return err + } + + // Supplier name should not be unique + if _, ok := existingSuppliers[yamlSupplierConfig.ServiceId]; ok { + return ErrRelayMinerConfigInvalidSupplier.Wrapf( + "duplicate supplier name %s", + yamlSupplierConfig.ServiceId, + ) + } + // Mark the supplier as existing + existingSuppliers[yamlSupplierConfig.ServiceId] = true + + // Add the supplier config to the referenced proxies + for _, proxyName := range yamlSupplierConfig.ProxyNames { + // If the proxy name is referencing a non-existent proxy, fail + if _, ok := relayMinerConfig.Proxies[proxyName]; !ok { + return ErrRelayMinerConfigInvalidSupplier.Wrapf( + "no matching proxy %s for supplier %s", + supplierConfig.ServiceId, + proxyName, + ) + } + + // If the proxy name is referencing a proxy of a different type, fail + if supplierConfig.Type != relayMinerConfig.Proxies[proxyName].Type { + return ErrRelayMinerConfigInvalidSupplier.Wrapf( + "supplier %s and proxy %s have different types", + supplierConfig.ServiceId, + proxyName, + ) + } + + relayMinerConfig.Proxies[proxyName].Suppliers[supplierConfig.ServiceId] = supplierConfig + } + } + + return nil +} diff --git a/pkg/relayer/config/types.go b/pkg/relayer/config/types.go new file mode 100644 index 000000000..fb14b8b20 --- /dev/null +++ b/pkg/relayer/config/types.go @@ -0,0 +1,147 @@ +package config + +import "net/url" + +type ProxyType int + +const ( + ProxyTypeHTTP ProxyType = iota + // TODO: Support other proxy types: HTTPS, TCP, UNIX socket, UDP, QUIC, WebRTC ... +) + +// YAMLRelayMinerConfig is the structure used to unmarshal the RelayMiner config file +// TODO_DOCUMENT(@red-0ne): Add proper README documentation for yaml config files +// and update inline comments accordingly. +type YAMLRelayMinerConfig struct { + PocketNode YAMLRelayMinerPocketNodeConfig `yaml:"pocket_node"` + SigningKeyName string `yaml:"signing_key_name"` + SmtStorePath string `yaml:"smt_store_path"` + Proxies []YAMLRelayMinerProxyConfig `yaml:"proxies"` + Suppliers []YAMLRelayMinerSupplierConfig `yaml:"suppliers"` +} + +// YAMLRelayMinerPocketNodeConfig is the structure used to unmarshal the pocket +// node URLs section of the RelayMiner config file +type YAMLRelayMinerPocketNodeConfig struct { + QueryNodeRPCUrl string `yaml:"query_node_rpc_url"` + QueryNodeGRPCUrl string `yaml:"query_node_grpc_url"` + TxNodeRPCUrl string `yaml:"tx_node_rpc_url"` +} + +// YAMLRelayMinerProxyConfig is the structure used to unmarshal the proxy +// section of the RelayMiner config file +type YAMLRelayMinerProxyConfig struct { + ProxyName string `yaml:"proxy_name"` + Type string `yaml:"type"` + Host string `yaml:"host"` + XForwardedHostLookup bool `yaml:"x_forwarded_host_lookup"` +} + +// YAMLRelayMinerSupplierConfig is the structure used to unmarshal the supplier +// section of the RelayMiner config file +type YAMLRelayMinerSupplierConfig struct { + ServiceId string `yaml:"service_id"` + Type string `yaml:"type"` + Hosts []string `yaml:"hosts"` + ServiceConfig YAMLRelayMinerSupplierServiceConfig `yaml:"service_config"` + ProxyNames []string `yaml:"proxy_names"` +} + +// YAMLRelayMinerSupplierServiceConfig is the structure used to unmarshal the supplier +// service sub-section of the RelayMiner config file +type YAMLRelayMinerSupplierServiceConfig struct { + Url string `yaml:"url"` + Authentication YAMLRelayMinerSupplierServiceAuthentication `yaml:"authentication,omitempty"` + Headers map[string]string `yaml:"headers,omitempty"` +} + +// YAMLRelayMinerSupplierServiceAuthentication is the structure used to unmarshal +// the supplier service basic auth of the RelayMiner config file when the +// supplier is of type "http" +type YAMLRelayMinerSupplierServiceAuthentication struct { + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` +} + +// RelayMinerConfig is the structure describing the RelayMiner config +type RelayMinerConfig struct { + PocketNode *RelayMinerPocketNodeConfig + SigningKeyName string + SmtStorePath string + Proxies map[string]*RelayMinerProxyConfig +} + +// RelayMinerPocketNodeConfig is the structure resulting from parsing the pocket +// node URLs section of the RelayMiner config file +type RelayMinerPocketNodeConfig struct { + QueryNodeRPCUrl *url.URL + QueryNodeGRPCUrl *url.URL + TxNodeRPCUrl *url.URL +} + +// RelayMinerProxyConfig is the structure resulting from parsing the proxy +// section of the RelayMiner config file. +// Each proxy embeds a map of supplier configs that are associated with it. +// Other proxy types may embed other fields in the future. eg. "https" may +// embed a TLS config. +type RelayMinerProxyConfig struct { + // ProxyName is the name of the proxy server, used to identify it in the config + ProxyName string + // Type is the transport protocol used by the proxy server like (http, https, etc.) + Type ProxyType + // Host is the host on which the proxy server will listen for incoming + // relay requests + Host string + // XForwardedHostLookup is a flag that indicates whether the proxy server + // should lookup the host from the X-Forwarded-Host header before falling + // back to the Host header. + XForwardedHostLookup bool + // Suppliers is a map of serviceIds -> RelayMinerSupplierConfig + Suppliers map[string]*RelayMinerSupplierConfig +} + +// RelayMinerSupplierConfig is the structure resulting from parsing the supplier +// section of the RelayMiner config file. +type RelayMinerSupplierConfig struct { + // ServiceId is the serviceId corresponding to the current configuration. + ServiceId string + // Type is the transport protocol used by the supplier, it must match the + // type of the proxy it is associated with. + Type ProxyType + // Hosts is a list of hosts advertised on-chain by the supplier, the corresponding + // proxy server will accept relay requests for these hosts. + Hosts []string + // ServiceConfig is the config of the service that relays will be proxied to. + // Other supplier types may embed other fields in the future. eg. "https" may + // embed a TLS config. + ServiceConfig *RelayMinerSupplierServiceConfig +} + +// RelayMinerSupplierServiceConfig is the structure resulting from parsing the supplier +// service sub-section of the RelayMiner config file. +type RelayMinerSupplierServiceConfig struct { + // Url is the URL of the service that relays will be proxied to. + Url *url.URL + // Authentication is the basic auth structure used to authenticate to the + // request being proxied from the current proxy server. + // If the service the relay requests are forwarded to requires basic auth + // then this field must be populated. + // TODO_TECHDEBT(@red-0ne): Pass the authentication to the service instance + // when the relay request is forwarded to it. + Authentication *RelayMinerSupplierServiceAuthentication + // Headers is a map of headers to be used for other authentication means. + // If the service the relay requests are forwarded to requires header based + // authentication then this field must be populated accordingly. + // For example: { "Authorization": "Bearer " } + // TODO_TECHDEBT(@red-0ne): Add these headers to the forwarded request + // before sending it to the service instance. + Headers map[string]string +} + +// RelayMinerSupplierServiceAuthentication is the structure resulting from parsing +// the supplier service basic auth of the RelayMiner config file when the +// supplier is of type "http" +type RelayMinerSupplierServiceAuthentication struct { + Username string + Password string +} diff --git a/pkg/relayer/interface.go b/pkg/relayer/interface.go index e9cc0ae72..74ab7975a 100644 --- a/pkg/relayer/interface.go +++ b/pkg/relayer/interface.go @@ -83,9 +83,6 @@ type RelayServer interface { // Stop terminates the service server and returns an error if it fails. Stop(ctx context.Context) error - - // Service returns the service to which the RelayServer relays. - Service() *sharedtypes.Service } // RelayerSessionsManager is responsible for managing the relayer's session lifecycles. diff --git a/pkg/relayer/proxy/errors.go b/pkg/relayer/proxy/errors.go index 7b14328c0..84604e836 100644 --- a/pkg/relayer/proxy/errors.go +++ b/pkg/relayer/proxy/errors.go @@ -15,4 +15,6 @@ var ( ErrRelayerProxyInvalidRelayRequest = sdkerrors.Register(codespace, 7, "invalid relay request") ErrRelayerProxyInvalidRelayResponse = sdkerrors.Register(codespace, 8, "invalid relay response") ErrRelayerProxyEmptyRelayRequestSignature = sdkerrors.Register(codespace, 9, "empty relay response signature") + ErrRelayerProxyServiceEndpointNotHandled = sdkerrors.Register(codespace, 10, "service endpoint not handled by relayer proxy") + ErrRelayerProxyUnsupportedTransportType = sdkerrors.Register(codespace, 11, "unsupported proxy transport type") ) diff --git a/pkg/relayer/proxy/options.go b/pkg/relayer/proxy/options.go index 4304d6067..ab9344c95 100644 --- a/pkg/relayer/proxy/options.go +++ b/pkg/relayer/proxy/options.go @@ -2,6 +2,7 @@ package proxy import ( "github.com/pokt-network/poktroll/pkg/relayer" + "github.com/pokt-network/poktroll/pkg/relayer/config" ) // WithSigningKeyName sets the signing key name used by the relayer proxy to sign relay responses. @@ -13,8 +14,8 @@ func WithSigningKeyName(keyName string) relayer.RelayerProxyOption { } // WithProxiedServicesEndpoints sets the endpoints of the proxied services. -func WithProxiedServicesEndpoints(proxiedServicesEndpoints servicesEndpointsMap) relayer.RelayerProxyOption { +func WithProxiedServicesEndpoints(proxyConfig map[string]*config.RelayMinerProxyConfig) relayer.RelayerProxyOption { return func(relProxy relayer.RelayerProxy) { - relProxy.(*relayerProxy).proxiedServicesEndpoints = proxiedServicesEndpoints + relProxy.(*relayerProxy).proxyConfigs = proxyConfig } } diff --git a/pkg/relayer/proxy/proxy.go b/pkg/relayer/proxy/proxy.go index 728b6a2c4..3b5c69c12 100644 --- a/pkg/relayer/proxy/proxy.go +++ b/pkg/relayer/proxy/proxy.go @@ -2,7 +2,6 @@ package proxy import ( "context" - "net/url" "cosmossdk.io/depinject" "github.com/cosmos/cosmos-sdk/crypto/keyring" @@ -13,17 +12,12 @@ import ( "github.com/pokt-network/poktroll/pkg/observable/channel" "github.com/pokt-network/poktroll/pkg/polylog" "github.com/pokt-network/poktroll/pkg/relayer" + "github.com/pokt-network/poktroll/pkg/relayer/config" "github.com/pokt-network/poktroll/x/service/types" ) var _ relayer.RelayerProxy = (*relayerProxy)(nil) -type ( - serviceId = string - relayServersMap = map[serviceId][]relayer.RelayServer - servicesEndpointsMap = map[serviceId]*url.URL -) - // relayerProxy is the main relayer proxy that takes relay requests of supported services from the client // and proxies them to the supported proxied services. // It is responsible for notifying the miner about the relays that have been served so they can be counted @@ -49,13 +43,15 @@ type relayerProxy struct { // which is needed to check if the relay proxy should be serving an incoming relay request. sessionQuerier client.SessionQueryClient - // advertisedRelayServers is a map of the services provided by the relayer proxy. Each provided service - // has the necessary information to start the server that listens for incoming relay requests and - // the client that relays the request to the supported proxied service. - advertisedRelayServers relayServersMap + // proxyServers is a map of proxyName -> RelayServer provided by the relayer proxy, + // where proxyName is the name of the proxy defined in the config file and + // RelayServer is the server that listens for incoming relay requests. + proxyServers map[string]relayer.RelayServer - // proxiedServicesEndpoints is a map of the proxied services endpoints that the relayer proxy supports. - proxiedServicesEndpoints servicesEndpointsMap + // proxyConfigs is a map of proxyName -> RelayMinerProxyConfig where proxyName + // is the name of the proxy defined in the config file and RelayMinerProxyConfig + // is the configuration of the proxy. + proxyConfigs map[string]*config.RelayMinerProxyConfig // servedRelays is an observable that notifies the miner about the relays that have been served. servedRelays relayer.RelaysObservable @@ -129,11 +125,9 @@ func (rp *relayerProxy) Start(ctx context.Context) error { startGroup, ctx := errgroup.WithContext(ctx) - for _, relayServer := range rp.advertisedRelayServers { - for _, svr := range relayServer { - server := svr // create a new variable scoped to the anonymous function - startGroup.Go(func() error { return server.Start(ctx) }) - } + for _, relayServer := range rp.proxyServers { + server := relayServer // create a new variable scoped to the anonymous function + startGroup.Go(func() error { return server.Start(ctx) }) } return startGroup.Wait() @@ -144,11 +138,10 @@ func (rp *relayerProxy) Start(ctx context.Context) error { func (rp *relayerProxy) Stop(ctx context.Context) error { stopGroup, ctx := errgroup.WithContext(ctx) - for _, providedService := range rp.advertisedRelayServers { - for _, svr := range providedService { - server := svr // create a new variable scoped to the anonymous function - stopGroup.Go(func() error { return server.Stop(ctx) }) - } + for _, relayServer := range rp.proxyServers { + // Create a new object (i.e. deep copy) variable scoped to the anonymous function below + server := relayServer + stopGroup.Go(func() error { return server.Stop(ctx) }) } return stopGroup.Wait() @@ -168,7 +161,7 @@ func (rp *relayerProxy) validateConfig() error { return ErrRelayerProxyUndefinedSigningKeyName } - if rp.proxiedServicesEndpoints == nil || len(rp.proxiedServicesEndpoints) == 0 { + if rp.proxyConfigs == nil || len(rp.proxyConfigs) == 0 { return ErrRelayerProxyUndefinedProxiedServicesEndpoints } diff --git a/pkg/relayer/proxy/proxy_test.go b/pkg/relayer/proxy/proxy_test.go index 1887f6170..bb699be71 100644 --- a/pkg/relayer/proxy/proxy_test.go +++ b/pkg/relayer/proxy/proxy_test.go @@ -3,6 +3,7 @@ package proxy_test import ( "bytes" "context" + "fmt" "io" "net/http" "net/url" @@ -13,6 +14,7 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" "github.com/stretchr/testify/require" + "github.com/pokt-network/poktroll/pkg/relayer/config" "github.com/pokt-network/poktroll/pkg/relayer/proxy" "github.com/pokt-network/poktroll/testutil/testproxy" servicetypes "github.com/pokt-network/poktroll/x/service/types" @@ -22,37 +24,94 @@ import ( const blockHeight = 1 var ( - // TODO_TECHDEBT(@okdas, @red-0ne): Source relayerProxyUrl from its config file once - // RelayerProxy is building its servers from the provided config file - relayerProxyUrl string - // helpers used for tests that are initialized in init() - supplierKeyName string - supplierEndpoints []*sharedtypes.SupplierEndpoint - appPrivateKey *secp256k1.PrivKey - proxiedServices map[string]*url.URL + supplierKeyName string + + // supplierEndpoints is the map of serviceName -> []SupplierEndpoint + // where serviceName is the name of the service the supplier staked for + // and SupplierEndpoint is the endpoint of the service advertised on-chain + // by the supplier + supplierEndpoints map[string][]*sharedtypes.SupplierEndpoint + + // appPrivateKey is the private key of the application that is used to sign + // relay responses. + // It is also used in these tests to derive the public key and address of the + // application. + appPrivateKey *secp256k1.PrivKey + // proxiedServices is the parsed configuration of the RelayMinerProxyConfig + proxiedServices map[string]*config.RelayMinerProxyConfig + + // defaultRelayerProxyBehavior is the list of functions that are used to + // define the behavior of the RelayerProxy in the tests. defaultRelayerProxyBehavior []func(*testproxy.TestBehavior) ) func init() { supplierKeyName = "supplierKeyName" - supplierEndpoints = []*sharedtypes.SupplierEndpoint{ - { - // TODO_TECHDEBT(@red-0ne): This URL is not used by the tests until we add - // support for the new `RelayMiner` config - // see https://github.com/pokt-network/poktroll/pull/246 - Url: "http://supplier:8545", - // TODO_EXTEND: Consider adding support for non JSON RPC services in the future - RpcType: sharedtypes.RPCType_JSON_RPC, + appPrivateKey = secp256k1.GenPrivKey() + + supplierEndpoints = map[string][]*sharedtypes.SupplierEndpoint{ + "service1": { + { + Url: "http://supplier:8545/", + // TODO_EXTEND: Consider adding support for non JSON RPC services in the future + RpcType: sharedtypes.RPCType_JSON_RPC, + }, + }, + "service2": { + { + Url: "http://supplier:8546/", + RpcType: sharedtypes.RPCType_GRPC, + }, + }, + "service3": { + { + Url: "http://supplier:8547/", + RpcType: sharedtypes.RPCType_GRPC, + }, }, } - appPrivateKey = secp256k1.GenPrivKey() - relayerProxyUrl = "http://127.0.0.1:8545/" - proxiedServices = map[string]*url.URL{ - "service1": {Scheme: "http", Host: "localhost:8180", Path: "/"}, - "service2": {Scheme: "http", Host: "localhost:8181", Path: "/"}, + proxiedServices = map[string]*config.RelayMinerProxyConfig{ + "server1": { + ProxyName: "server1", + Type: config.ProxyTypeHTTP, + Host: "127.0.0.1:8080", + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "service1": { + ServiceId: "service1", + Type: config.ProxyTypeHTTP, + Hosts: []string{"supplier:8545"}, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "127.0.0.1:8545", Path: "/"}, + }, + }, + "service2": { + ServiceId: "service2", + Type: config.ProxyTypeHTTP, + Hosts: []string{"supplier:8546"}, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "127.0.0.1:8546", Path: "/"}, + }, + }, + }, + }, + "server2": { + ProxyName: "server2", + Type: config.ProxyTypeHTTP, + Host: "127.0.0.1:8081", + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "service3": { + ServiceId: "service3", + Type: config.ProxyTypeHTTP, + Hosts: []string{"supplier:8547"}, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "127.0.0.1:8547", Path: "/"}, + }, + }, + }, + }, } defaultRelayerProxyBehavior = []func(*testproxy.TestBehavior){ @@ -84,7 +143,12 @@ func TestRelayerProxy_StartAndStop(t *testing.T) { time.Sleep(100 * time.Millisecond) // Test that RelayerProxy is handling requests (ignoring the actual response content) - res, err := http.DefaultClient.Get(relayerProxyUrl) + res, err := http.DefaultClient.Get(fmt.Sprintf("http://%s/", proxiedServices["server1"].Host)) + require.NoError(t, err) + require.NotNil(t, res) + + // Test that RelayerProxy is handling requests from the other server + res, err = http.DefaultClient.Get(fmt.Sprintf("http://%s/", proxiedServices["server2"].Host)) require.NoError(t, err) require.NotNil(t, res) @@ -131,7 +195,7 @@ func TestRelayerProxy_NoProxiedServices(t *testing.T) { _, err := proxy.NewRelayerProxy( test.Deps, proxy.WithSigningKeyName(supplierKeyName), - proxy.WithProxiedServicesEndpoints(make(map[string]*url.URL)), + proxy.WithProxiedServicesEndpoints(make(map[string]*config.RelayMinerProxyConfig)), ) require.Error(t, err) } @@ -141,16 +205,13 @@ func TestRelayerProxy_NoProxiedServices(t *testing.T) { func TestRelayerProxy_UnsupportedRpcType(t *testing.T) { ctx := context.TODO() - unsupportedSupplierEndpoint := []*sharedtypes.SupplierEndpoint{ - { - Url: "http://supplier:8545/jsonrpc", - // TODO_EXTEND: Consider adding support for non JSON RPC services in the future - RpcType: sharedtypes.RPCType_JSON_RPC, - }, - { - Url: "http://supplier:8545/grpc", - // TODO_EXTEND: Consider adding support for non JSON RPC services in the future - RpcType: sharedtypes.RPCType_GRPC, + unsupportedSupplierEndpoint := map[string][]*sharedtypes.SupplierEndpoint{ + "service1": { + { + Url: "http://unsupported:8545/jsonrpc", + // TODO_EXTEND: Consider adding support for non JSON RPC services in the future + RpcType: sharedtypes.RPCType_JSON_RPC, + }, }, } @@ -177,6 +238,105 @@ func TestRelayerProxy_UnsupportedRpcType(t *testing.T) { require.Error(t, err) } +func TestRelayerProxy_UnsupportedTransportType(t *testing.T) { + ctx := context.TODO() + + badTransportSupplierEndpoints := map[string][]*sharedtypes.SupplierEndpoint{ + "service1": { + { + Url: "xttp://supplier:8545/", + RpcType: sharedtypes.RPCType_JSON_RPC, + }, + }, + } + + unsupportedTransportProxy := map[string]*config.RelayMinerProxyConfig{ + "server1": { + ProxyName: "server1", + // The proxy is configured with an unsupported transport type + Type: config.ProxyType(100), + Host: "127.0.0.1:8080", + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "service1": { + ServiceId: "service1", + // The proxy is configured with an unsupported transport type + Type: config.ProxyType(100), + Hosts: []string{"supplier:8545"}, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "127.0.0.1:8545", Path: "/"}, + }, + }, + }, + }, + } + + unsupportedTransportTypeBehavior := []func(*testproxy.TestBehavior){ + testproxy.WithRelayerProxyDependencies(supplierKeyName), + + // The proxy is configured with an unsupported transport type for the proxy + testproxy.WithRelayerProxiedServices(unsupportedTransportProxy), + testproxy.WithDefaultSupplier(supplierKeyName, badTransportSupplierEndpoints), + testproxy.WithDefaultApplication(appPrivateKey), + testproxy.WithDefaultSessionSupplier(supplierKeyName, "service1", appPrivateKey), + } + + test := testproxy.NewRelayerProxyTestBehavior(ctx, t, unsupportedTransportTypeBehavior...) + + rp, err := proxy.NewRelayerProxy( + test.Deps, + proxy.WithSigningKeyName(supplierKeyName), + proxy.WithProxiedServicesEndpoints(unsupportedTransportProxy), + ) + require.NoError(t, err) + + err = rp.Start(ctx) + require.ErrorIs(t, err, proxy.ErrRelayerProxyUnsupportedTransportType) +} + +func TestRelayerProxy_NonConfiguredSupplierServices(t *testing.T) { + ctx := context.TODO() + + missingServicesProxy := map[string]*config.RelayMinerProxyConfig{ + "server1": { + ProxyName: "server1", + Type: config.ProxyTypeHTTP, + Host: "127.0.0.1:8080", + Suppliers: map[string]*config.RelayMinerSupplierConfig{ + "service1": { + ServiceId: "service1", + Type: config.ProxyTypeHTTP, + Hosts: []string{"supplier:8545"}, + ServiceConfig: &config.RelayMinerSupplierServiceConfig{ + Url: &url.URL{Scheme: "http", Host: "127.0.0.1:8545", Path: "/"}, + }, + }, + }, + }, + } + + unsupportedTransportTypeBehavior := []func(*testproxy.TestBehavior){ + testproxy.WithRelayerProxyDependencies(supplierKeyName), + + // The proxy is configured with an unsupported transport type for the proxy + testproxy.WithRelayerProxiedServices(missingServicesProxy), + testproxy.WithDefaultSupplier(supplierKeyName, supplierEndpoints), + testproxy.WithDefaultApplication(appPrivateKey), + testproxy.WithDefaultSessionSupplier(supplierKeyName, "service1", appPrivateKey), + } + + test := testproxy.NewRelayerProxyTestBehavior(ctx, t, unsupportedTransportTypeBehavior...) + + rp, err := proxy.NewRelayerProxy( + test.Deps, + proxy.WithSigningKeyName(supplierKeyName), + proxy.WithProxiedServicesEndpoints(missingServicesProxy), + ) + require.NoError(t, err) + + err = rp.Start(ctx) + require.ErrorIs(t, err, proxy.ErrRelayerProxyServiceEndpointNotHandled) +} + // Test different RelayRequest scenarios func TestRelayerProxy_Relays(t *testing.T) { tests := []struct { @@ -337,7 +497,11 @@ func sendRequestWithUnparsableBody( // Send non JSONRpc payload when the post request specifies json reader := io.NopCloser(bytes.NewReader([]byte("invalid request"))) - res, err := http.DefaultClient.Post(relayerProxyUrl, "application/json", reader) + res, err := http.DefaultClient.Post( + fmt.Sprintf("http://%s", proxiedServices["server1"].Host), + "application/json", + reader, + ) require.NoError(t, err) require.NotNil(t, res) @@ -354,7 +518,7 @@ func sendRequestWithMissingMeta( Payload: testproxy.PrepareJsonRPCRequestPayload(), } - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } func sendRequestWithMissingSignature( @@ -369,7 +533,7 @@ func sendRequestWithMissingSignature( testproxy.PrepareJsonRPCRequestPayload(), ) req.Meta.Signature = nil - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } func sendRequestWithInvalidSignature( @@ -385,7 +549,7 @@ func sendRequestWithInvalidSignature( ) req.Meta.Signature = []byte("invalid signature") - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } func sendRequestWithMissingSessionHeaderApplicationAddress( @@ -408,7 +572,7 @@ func sendRequestWithMissingSessionHeaderApplicationAddress( // before looking at the application address req.Meta.Signature = testproxy.GetApplicationRingSignature(t, req, randomPrivKey) - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } func sendRequestWithNonStakedApplicationAddress( @@ -427,7 +591,7 @@ func sendRequestWithNonStakedApplicationAddress( // Have a valid signature from the non staked key req.Meta.Signature = testproxy.GetApplicationRingSignature(t, req, randomPrivKey) - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } func sendRequestWithRingSignatureMismatch( @@ -446,7 +610,7 @@ func sendRequestWithRingSignatureMismatch( randomPrivKey := secp256k1.GenPrivKey() req.Meta.Signature = testproxy.GetApplicationRingSignature(t, req, randomPrivKey) - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } func sendRequestWithDifferentSession( @@ -463,7 +627,7 @@ func sendRequestWithDifferentSession( ) req.Meta.Signature = testproxy.GetApplicationRingSignature(t, req, appPrivateKey) - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } func sendRequestWithInvalidRelaySupplier( @@ -479,7 +643,7 @@ func sendRequestWithInvalidRelaySupplier( ) req.Meta.Signature = testproxy.GetApplicationRingSignature(t, req, appPrivateKey) - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } func sendRequestWithSignatureForDifferentPayload( @@ -497,7 +661,7 @@ func sendRequestWithSignatureForDifferentPayload( // Alter the request payload so the hash doesn't match the one used by the signature req.Payload = []byte(`{"method":"someMethod","id":1,"jsonrpc":"2.0","params":["alteredParam"]}`) - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } func sendRequestWithSuccessfulReply( @@ -513,5 +677,5 @@ func sendRequestWithSuccessfulReply( ) req.Meta.Signature = testproxy.GetApplicationRingSignature(t, req, appPrivateKey) - return testproxy.MarshalAndSend(test, relayerProxyUrl, req) + return testproxy.MarshalAndSend(test, proxiedServices, "server1", "service1", req) } diff --git a/pkg/relayer/proxy/server_builder.go b/pkg/relayer/proxy/server_builder.go index f6ed8d23f..a42e876be 100644 --- a/pkg/relayer/proxy/server_builder.go +++ b/pkg/relayer/proxy/server_builder.go @@ -2,8 +2,12 @@ package proxy import ( "context" + "net/url" + + "golang.org/x/exp/slices" "github.com/pokt-network/poktroll/pkg/relayer" + "github.com/pokt-network/poktroll/pkg/relayer/config" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) @@ -28,59 +32,78 @@ func (rp *relayerProxy) BuildProvidedServices(ctx context.Context) error { return err } - services := supplier.Services - - // Build the advertised relay servers map. For each service's endpoint, create the appropriate RelayServer. - providedServices := make(relayServersMap) - for _, serviceConfig := range services { - service := serviceConfig.Service - proxiedServicesEndpoints := rp.proxiedServicesEndpoints[service.Id] - var serviceEndpoints []relayer.RelayServer - - for _, endpoint := range serviceConfig.Endpoints { - // url, err := url.Parse(endpoint.Url) - // if err != nil { - // return err - // } - // supplierEndpointHost := url.Host - - // This will throw an error if we have more than one endpoint - supplierEndpointHost := "0.0.0.0:8545" - - rp.logger.Info(). - Fields(map[string]any{ - "service_id": service.Id, - "endpoint_url": endpoint.Url, - }). - Msg("starting relay server") - - // Switch to the RPC type - // TODO(@h5law): Implement a switch that handles all synchronous - // RPC types in one server type and asynchronous RPC types in another - // to create the appropriate RelayServer - var server relayer.RelayServer - switch endpoint.RpcType { - case sharedtypes.RPCType_JSON_RPC: - server = NewSynchronousServer( - rp.logger, - service, - supplierEndpointHost, - proxiedServicesEndpoints, - rp.servedRelaysPublishCh, - rp, - ) - default: - return ErrRelayerProxyUnsupportedRPCType + // Check that the supplier's advertised services' endpoints are present in + // the proxy config and handled by a proxy host + // Iterate over the supplier's advertised services then iterate over each + // service's endpoint + for _, service := range supplier.Services { + for _, endpoint := range service.Endpoints { + endpointUrl, err := url.Parse(endpoint.Url) + if err != nil { + return err + } + found := false + // Iterate over the proxy configs and check if `endpointUrl` is present + // in any of the proxy config's suppliers' service's hosts + for _, proxyConfig := range rp.proxyConfigs { + supplierService, ok := proxyConfig.Suppliers[service.Service.Id] + if ok && slices.Contains(supplierService.Hosts, endpointUrl.Host) { + found = true + break + } } - serviceEndpoints = append(serviceEndpoints, server) + if !found { + return ErrRelayerProxyServiceEndpointNotHandled.Wrapf( + "service endpoint %s not handled by proxy", + endpoint.Url, + ) + } } + } - providedServices[service.Id] = serviceEndpoints + if rp.proxyServers, err = rp.initializeProxyServers(supplier.Services); err != nil { + return err } - rp.advertisedRelayServers = providedServices rp.supplierAddress = supplier.Address return nil } + +// initializeProxyServers initializes the proxy servers for each proxy config. +func (rp *relayerProxy) initializeProxyServers( + supplierServices []*sharedtypes.SupplierServiceConfig, +) (proxyServerMap map[string]relayer.RelayServer, err error) { + // Build a map of serviceId -> service for the supplier's advertised services + supplierServiceMap := make(map[string]*sharedtypes.Service) + for _, service := range supplierServices { + supplierServiceMap[service.Service.Id] = service.Service + } + + // Build a map of proxyName -> RelayServer for each proxy defined in the config file + proxyServers := make(map[string]relayer.RelayServer) + + for _, proxyConfig := range rp.proxyConfigs { + rp.logger.Info().Str("proxy host", proxyConfig.Host).Msg("starting relay proxy server") + + // TODO(@h5law): Implement a switch that handles all synchronous + // RPC types in one server type and asynchronous RPC types in another + // to create the appropriate RelayServer. + // Initialize the proxy server according to the proxy type defined in the config file + switch proxyConfig.Type { + case config.ProxyTypeHTTP: + proxyServers[proxyConfig.ProxyName] = NewSynchronousServer( + rp.logger, + proxyConfig, + supplierServiceMap, + rp.servedRelaysPublishCh, + rp, + ) + default: + return nil, ErrRelayerProxyUnsupportedTransportType + } + } + + return proxyServers, nil +} diff --git a/pkg/relayer/proxy/synchronous.go b/pkg/relayer/proxy/synchronous.go index c748be383..85b181f43 100644 --- a/pkg/relayer/proxy/synchronous.go +++ b/pkg/relayer/proxy/synchronous.go @@ -11,6 +11,7 @@ import ( "github.com/pokt-network/poktroll/pkg/polylog" "github.com/pokt-network/poktroll/pkg/relayer" + "github.com/pokt-network/poktroll/pkg/relayer/config" "github.com/pokt-network/poktroll/x/service/types" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) @@ -23,11 +24,14 @@ var _ relayer.RelayServer = (*synchronousRPCServer)(nil) type synchronousRPCServer struct { logger polylog.Logger - // service is the service that the server is responsible for. - service *sharedtypes.Service + // supplierServiceMap is a map of serviceId -> SupplierServiceConfig + // representing the supplier's advertised services. + supplierServiceMap map[string]*sharedtypes.Service - // proxiedServiceEndpoint is the address of the proxied service that the server relays requests to. - proxiedServiceEndpoint *url.URL + // proxyConfig is the configuration of the proxy server. It contains the + // host address of the server, the service endpoint, and the advertised service. + // endpoints it gets relay requests from. + proxyConfig *config.RelayMinerProxyConfig // server is the HTTP server that listens for incoming relay requests. server *http.Server @@ -46,19 +50,18 @@ type synchronousRPCServer struct { // and returns a RelayServer that listens to incoming RelayRequests. func NewSynchronousServer( logger polylog.Logger, - service *sharedtypes.Service, - supplierEndpointHost string, - proxiedServiceEndpoint *url.URL, + proxyConfig *config.RelayMinerProxyConfig, + supplierServiceMap map[string]*sharedtypes.Service, servedRelaysProducer chan<- *types.Relay, proxy relayer.RelayerProxy, ) relayer.RelayServer { return &synchronousRPCServer{ - logger: logger, - service: service, - server: &http.Server{Addr: supplierEndpointHost}, - relayerProxy: proxy, - proxiedServiceEndpoint: proxiedServiceEndpoint, - servedRelaysProducer: servedRelaysProducer, + logger: logger, + supplierServiceMap: supplierServiceMap, + server: &http.Server{Addr: proxyConfig.Host}, + relayerProxy: proxy, + servedRelaysProducer: servedRelaysProducer, + proxyConfig: proxyConfig, } } @@ -82,11 +85,6 @@ func (sync *synchronousRPCServer) Stop(ctx context.Context) error { return sync.server.Shutdown(ctx) } -// Service returns the underlying service object. -func (sync *synchronousRPCServer) Service() *sharedtypes.Service { - return sync.service -} - // ServeHTTP listens for incoming relay requests. It implements the respective // method of the http.Handler interface. It is called by http.ListenAndServe() // when synchronousRPCServer is used as an http.Handler with an http.Server. @@ -94,6 +92,53 @@ func (sync *synchronousRPCServer) Service() *sharedtypes.Service { func (sync *synchronousRPCServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) { ctx := request.Context() + var originHost string + // When the proxy is behind a reverse proxy, or is getting its requests from + // a CDN or a load balancer, the host header may not contain the on-chain + // advertized address needed to determine the service that the relay request is for. + // These CDNs and reverse proxies usually set the X-Forwarded-Host header + // to the original host. + // RelayMiner operators that have such a setup can set the XForwardedHostLookup + // option to true in the proxy config to enable the proxy to look up the + // original host from the X-Forwarded-Host header. + // Get the original host from X-Forwarded-Host header if specified in the proxy + // config and fall back to the Host header if it is not specified. + if sync.proxyConfig.XForwardedHostLookup { + originHost = request.Header.Get("X-Forwarded-Host") + } + + if originHost == "" { + originHost = request.Host + } + + var supplierService *sharedtypes.Service + var serviceUrl *url.URL + + // Get the Service and serviceUrl corresponding to the originHost. + // TODO_IMPROVE(red-0ne): Checking that the originHost is currently done by + // iterating over the proxy config's suppliers and checking if the originHost + // is present in any of the supplier's service's hosts. We could improve this + // by building a map at the server initialization level with originHost as the + // key so that we can get the service and serviceUrl in O(1) time. + for _, supplierServiceConfig := range sync.proxyConfig.Suppliers { + for _, host := range supplierServiceConfig.Hosts { + if host == originHost { + supplierService = sync.supplierServiceMap[supplierServiceConfig.ServiceId] + serviceUrl = supplierServiceConfig.ServiceConfig.Url + break + } + } + + if serviceUrl != nil { + break + } + } + + if supplierService == nil || serviceUrl == nil { + sync.replyWithError(ctx, []byte{}, writer, ErrRelayerProxyServiceEndpointNotHandled) + return + } + sync.logger.Debug().Msg("serving synchronous relay request") // Extract the relay request from the request body. @@ -116,7 +161,7 @@ func (sync *synchronousRPCServer) ServeHTTP(writer http.ResponseWriter, request } // Relay the request to the proxied service and build the response that will be sent back to the client. - relay, err := sync.serveHTTP(ctx, request, relayRequest) + relay, err := sync.serveHTTP(ctx, serviceUrl, supplierService, request, relayRequest) if err != nil { // Reply with an error if the relay could not be served. sync.replyWithError(ctx, relayRequest.Payload, writer, err) @@ -145,6 +190,8 @@ func (sync *synchronousRPCServer) ServeHTTP(writer http.ResponseWriter, request // serveHTTP holds the underlying logic of ServeHTTP. func (sync *synchronousRPCServer) serveHTTP( ctx context.Context, + serviceUrl *url.URL, + supplierService *sharedtypes.Service, request *http.Request, relayRequest *types.RelayRequest, ) (*types.Relay, error) { @@ -155,7 +202,7 @@ func (sync *synchronousRPCServer) serveHTTP( // request signature verification, session verification, and response signature. // This would help in separating concerns and improving code maintainability. // See https://github.com/pokt-network/poktroll/issues/160 - if err := sync.relayerProxy.VerifyRelayRequest(ctx, relayRequest, sync.service); err != nil { + if err := sync.relayerProxy.VerifyRelayRequest(ctx, relayRequest, supplierService); err != nil { return nil, err } @@ -170,14 +217,14 @@ func (sync *synchronousRPCServer) serveHTTP( // Build the request to be sent to the native service by substituting // the destination URL's host with the native service's listen address. sync.logger.Debug(). - Str("destination_url", sync.proxiedServiceEndpoint.String()). + Str("destination_url", serviceUrl.String()). Msg("building relay request payload to service") relayHTTPRequest := &http.Request{ Method: request.Method, Header: request.Header, - URL: sync.proxiedServiceEndpoint, - Host: sync.proxiedServiceEndpoint.Host, + URL: serviceUrl, + Host: serviceUrl.Host, Body: requestBodyReader, } diff --git a/testutil/testproxy/relayerproxy.go b/testutil/testproxy/relayerproxy.go index 9961584f0..96768aabb 100644 --- a/testutil/testproxy/relayerproxy.go +++ b/testutil/testproxy/relayerproxy.go @@ -21,6 +21,7 @@ import ( "github.com/pokt-network/poktroll/pkg/crypto/rings" "github.com/pokt-network/poktroll/pkg/polylog" + "github.com/pokt-network/poktroll/pkg/relayer/config" "github.com/pokt-network/poktroll/pkg/signer" "github.com/pokt-network/poktroll/testutil/testclient/testblock" testkeyring "github.com/pokt-network/poktroll/testutil/testclient/testkeyring" @@ -104,20 +105,26 @@ func WithRelayerProxyDependencies(keyName string) func(*TestBehavior) { // WithRelayerProxiedServices creates the services that the relayer proxy will // proxy requests to. -func WithRelayerProxiedServices(proxiedServices map[string]*url.URL) func(*TestBehavior) { +// It creates an HTTP server for each service and starts listening on the +// provided host. +func WithRelayerProxiedServices( + proxiedServices map[string]*config.RelayMinerProxyConfig, +) func(*TestBehavior) { return func(test *TestBehavior) { - for serviceId, endpoint := range proxiedServices { - server := &http.Server{Addr: endpoint.Host} - server.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(prepareJsonRPCResponsePayload()) - }) - go func() { server.ListenAndServe() }() - go func() { - <-test.ctx.Done() - server.Shutdown(test.ctx) - }() - - test.proxiedServices[serviceId] = server + for _, proxy := range proxiedServices { + for serviceId, service := range proxy.Suppliers { + server := &http.Server{Addr: service.ServiceConfig.Url.Host} + server.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(prepareJsonRPCResponsePayload()) + }) + go func() { server.ListenAndServe() }() + go func() { + <-test.ctx.Done() + server.Shutdown(test.ctx) + }() + + test.proxiedServices[serviceId] = server + } } } } @@ -125,7 +132,7 @@ func WithRelayerProxiedServices(proxiedServices map[string]*url.URL) func(*TestB // WithDefaultSupplier creates the default staked supplier for the test func WithDefaultSupplier( supplierKeyName string, - supplierEndpoints []*sharedtypes.SupplierEndpoint, + supplierEndpoints map[string][]*sharedtypes.SupplierEndpoint, ) func(*TestBehavior) { return func(test *TestBehavior) { var keyring keyringtypes.Keyring @@ -141,12 +148,14 @@ func WithDefaultSupplier( supplierAddress := supplierAccAddress.String() - testqueryclients.AddSuppliersWithServiceEndpoints( - test.t, - supplierAddress, - "service1", - supplierEndpoints, - ) + for serviceId, endpoints := range supplierEndpoints { + testqueryclients.AddSuppliersWithServiceEndpoints( + test.t, + supplierAddress, + serviceId, + endpoints, + ) + } } } @@ -206,17 +215,41 @@ func WithDefaultSessionSupplier( } } -// MarshalAndSend marshals the request and sends it to the provided service +// TODO_TECHDEBT(@red-0ne): This function only supports JSON-RPC requests and +// needs to have its http.Request "Content-Type" header passed-in as a parameter +// and take out the GetRelayResponseError function which parses JSON-RPC responses +// to make it RPC-type agnostic. + +// MarshalAndSend marshals the request and sends it to the provided service. func MarshalAndSend( test *TestBehavior, - url string, + proxiedServices map[string]*config.RelayMinerProxyConfig, + proxyServeName string, + serviceId string, request *servicetypes.RelayRequest, ) (errCode int32, errorMessage string) { reqBz, err := request.Marshal() require.NoError(test.t, err) + var scheme string + switch proxiedServices[proxyServeName].Type { + case config.ProxyTypeHTTP: + scheme = "http" + default: + require.FailNow(test.t, "unsupported proxy type") + } + reader := io.NopCloser(bytes.NewReader(reqBz)) - res, err := http.DefaultClient.Post(url, "application/json", reader) + req := &http.Request{ + Method: http.MethodPost, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + URL: &url.URL{Scheme: scheme, Host: proxiedServices[proxyServeName].Host}, + Host: proxiedServices[proxyServeName].Suppliers[serviceId].Hosts[0], + Body: reader, + } + res, err := http.DefaultClient.Do(req) require.NoError(test.t, err) require.NotNil(test.t, res)