From 2dbe8669fb9c8ef0de18e3469780826e170c7e39 Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Thu, 26 Oct 2023 14:08:24 -0700 Subject: [PATCH 01/28] Update pull_request_template.md Replace `make go_test` with `make go_develop_and_test` --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 88c355a48..1599a1f49 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -44,7 +44,7 @@ Select one or more: ## Testing -- [ ] **Run all unit tests**: `make go_test` +- [ ] **Run all unit tests**: `make go_develop_and_test` - [ ] **Verify Localnet manually**: See the instructions [here](TODO: add link to instructions) ## Sanity Checklist From e021b42090a7b15915add67d6c57b5e9b15535d3 Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Thu, 26 Oct 2023 15:34:31 -0700 Subject: [PATCH 02/28] [Session] Implement the 1st iteration of the SessionHydrator (#78) Add support for: - Storing `ServiceConfigs` in the `Application` store - Storing `ServiceConfigs` in the `Supplier` store - Business logic for `GetSession` and the underlying `SessionHydrator` Not adding support for: - Staking/unstaking `Service(s)` for `Application` / `Supplier` - Using the proper (prior) `BlockHash` for session generation - The `cliContext` required to retrieve a `Session` See https://github.com/pokt-network/poktroll/pull/78 for more details ---- Co-authored-by: Bryan White Co-authored-by: Redouane Lakrache Co-authored-by: red-0ne Co-authored-by: Daniel Olshansky Co-authored-by: harry <53987565+h5law@users.noreply.github.com> --- Makefile | 3 + app/app.go | 3 + docs/static/openapi.yml | 1567 +++++++++++++++++++- go.mod | 6 +- pkg/observable/channel/observer.go | 2 +- proto/pocket/application/application.proto | 7 +- proto/pocket/application/tx.proto | 8 +- proto/pocket/pocket/query.proto | 1 - proto/pocket/session/query.proto | 26 +- proto/pocket/session/session.proto | 12 +- proto/pocket/shared/service.proto | 20 +- proto/pocket/shared/supplier.proto | 6 +- proto/pocket/supplier/tx.proto | 4 +- testutil/keeper/session.go | 127 ++ testutil/session/mocks/mocks.go | 6 + x/session/client/cli/query_get_session.go | 9 +- x/session/keeper/keeper.go | 9 + x/session/keeper/msg_server_test.go | 1 + x/session/keeper/query_get_session.go | 13 +- x/session/keeper/query_get_session_test.go | 130 ++ x/session/keeper/query_params_test.go | 1 + x/session/keeper/session_hydrator.go | 223 +++ x/session/keeper/session_hydrator_test.go | 305 ++++ x/session/module.go | 8 +- x/session/types/errors.go | 4 +- x/session/types/expected_keepers.go | 19 +- x/supplier/keeper/supplier.go | 3 + 27 files changed, 2425 insertions(+), 98 deletions(-) create mode 100644 testutil/session/mocks/mocks.go create mode 100644 x/session/keeper/query_get_session_test.go create mode 100644 x/session/keeper/session_hydrator.go create mode 100644 x/session/keeper/session_hydrator_test.go diff --git a/Makefile b/Makefile index aac47efac..7663539a1 100644 --- a/Makefile +++ b/Makefile @@ -127,9 +127,11 @@ itest: go_version_check ## Run tests iteratively (see usage for more) .PHONY: go_mockgen go_mockgen: ## Use `mockgen` to generate mocks used for testing purposes of all the modules. + find . -name "*_mock.go" | xargs --no-run-if-empty rm go generate ./x/application/types/ go generate ./x/gateway/types/ go generate ./x/supplier/types/ + go generate ./x/session/types/ .PHONY: go_develop go_develop: proto_regen go_mockgen ## Generate protos and mocks @@ -156,6 +158,7 @@ go_develop_and_test: go_develop go_test ## Generate protos, mocks and run all te # TODO - General Purpose catch-all. # TODO_DECIDE - A TODO indicating we need to make a decision and document it using an ADR in the future; https://github.com/pokt-network/pocket-network-protocol/tree/main/ADRs # TODO_TECHDEBT - Not a great implementation, but we need to fix it later. +# TODO_BLOCKER - Similar to TECHDEBT, but of higher priority, urgency & risk prior to the next release # TODO_IMPROVE - A nice to have, but not a priority. It's okay if we never get to this. # TODO_OPTIMIZE - An opportunity for performance improvement if/when it's necessary # TODO_DISCUSS - Probably requires a lengthy offline discussion to understand next steps. diff --git a/app/app.go b/app/app.go index ecec4ae9c..f640a6ee7 100644 --- a/app/app.go +++ b/app/app.go @@ -579,6 +579,9 @@ func New( keys[sessionmoduletypes.StoreKey], keys[sessionmoduletypes.MemStoreKey], app.GetSubspace(sessionmoduletypes.ModuleName), + + app.ApplicationKeeper, + app.SupplierKeeper, ) sessionModule := sessionmodule.NewAppModule(appCodec, app.SessionKeeper, app.AccountKeeper, app.BankKeeper) diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index c45bf3ebb..26b83a9b8 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46455,9 +46455,7 @@ paths: type: string title: >- The Bech32 address of the application using cosmos' - ScalarDescriptor to ensure deterministic deterministic - encoding using cosmos' ScalarDescriptor to ensure - deterministic deterministic encoding + ScalarDescriptor to ensure deterministic encoding stake: title: The total amount of uPOKT the application has staked type: object @@ -46474,6 +46472,26 @@ paths: custom method signatures required by gogoproto. + service_ids: + type: array + items: + type: object + properties: + id: + type: string + title: Unique identifier for the service + name: + type: string + title: >- + (Optional) Semantic human readable name for the + service + title: >- + ServiceId message to encapsulate unique and semantic + identifiers for a service on the network + description: The ID of the service this session is servicing + title: >- + TODO(@olshansk): Change this to + `shared.ApplicationServiceConfig` in #95 title: >- Application defines the type used to store an on-chain definition and state for an application @@ -46599,9 +46617,7 @@ paths: type: string title: >- The Bech32 address of the application using cosmos' - ScalarDescriptor to ensure deterministic deterministic - encoding using cosmos' ScalarDescriptor to ensure - deterministic deterministic encoding + ScalarDescriptor to ensure deterministic encoding stake: title: The total amount of uPOKT the application has staked type: object @@ -46618,6 +46634,26 @@ paths: custom method signatures required by gogoproto. + service_ids: + type: array + items: + type: object + properties: + id: + type: string + title: Unique identifier for the service + name: + type: string + title: >- + (Optional) Semantic human readable name for the + service + title: >- + ServiceId message to encapsulate unique and semantic + identifiers for a service on the network + description: The ID of the service this session is servicing + title: >- + TODO(@olshansk): Change this to + `shared.ApplicationServiceConfig` in #95 title: >- Application defines the type used to store an on-chain definition and state for an application @@ -46994,6 +47030,259 @@ paths: description: A successful response. schema: type: object + properties: + session: + type: object + properties: + header: + title: The header of the session containing lightweight data + type: object + properties: + application_address: + type: string + title: >- + The Bech32 address of the application using cosmos' + ScalarDescriptor to ensure deterministic encoding + service_id: + title: The ID of the service this session is servicing + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session + for a certain service but with some additional + configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts + as a reminder than an optional onchain + representation of the service is necessary + session_start_block_height: + type: string + format: int64 + title: The height at which this session started + session_id: + type: string + description: A unique pseudoranom ID for this session + title: >- + NOTE: session_id can be derived from the above values + using on-chain but is included in the header for + convenience + description: >- + SessionHeader is a lightweight header for a session that + can be passed around. + + It is the minimal amount of data required to hydrate & + retrieve all data relevant to the session. + session_id: + type: string + title: A unique pseudoranom ID for this session + session_number: + type: string + format: int64 + title: The session number since genesis + num_blocks_per_session: + type: string + format: int64 + title: The number of blocks per session when this session started + application: + title: A fully hydrated application object this session is for + type: object + properties: + address: + type: string + title: >- + The Bech32 address of the application using cosmos' + ScalarDescriptor to ensure deterministic encoding + stake: + title: The total amount of uPOKT the application has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an + amount. + + + NOTE: The amount field is an Int which implements the + custom method + + signatures required by gogoproto. + service_configs: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but + was desigtned created to enable more complex + service identification + + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for + the service + title: >- + TODO_TECHDEBT: Name is currently unused but + acts as a reminder than an optional onchain + representation of the service is necessary + title: >- + ApplicationServiceConfig holds the service + configuration the application stakes for + title: The ID of the service this session is servicing + suppliers: + type: array + items: + type: object + properties: + address: + type: string + title: >- + The Bech32 address of the supplier using cosmos' + ScalarDescriptor to ensure deterministic encoding + stake: + title: The total amount of uPOKT the supplier has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an + amount. + + + NOTE: The amount field is an Int which implements + the custom method + + signatures required by gogoproto. + services: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant + but was desigtned created to enable more + complex service identification + + For example, what if we want to request a + session for a certain service but with + some additional configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name + for the service + title: >- + TODO_TECHDEBT: Name is currently unused + but acts as a reminder than an optional + onchain representation of the service is + necessary + endpoints: + type: array + items: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, + SLAs or something else? There will be + more discussion once we get closer to + implementing on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as + proto maps can't be keyed by enums + title: >- + Additional configuration options for the + endpoint + title: >- + SupplierEndpoint message to hold service + configuration details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service + configuration the supplier stakes for + title: The service configs this supplier can support + description: >- + Supplier is the type defining the actor in Pocket + Network that provides RPC services. + title: >- + A fully hydrated set of servicers that are serving the + application + description: >- + Session is a fully hydrated session object that contains all + the information for the Session + + and its parcipants. default: description: An unexpected error response. schema: @@ -47012,6 +47301,43 @@ paths: '@type': type: string additionalProperties: {} + parameters: + - name: application_address + description: >- + The Bech32 address of the application using cosmos' ScalarDescriptor + to ensure deterministic encoding + in: query + required: false + type: string + - name: service_id.id + description: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created to + enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? + + + Unique identifier for the service + in: query + required: false + type: string + - name: service_id.name + description: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder than + an optional onchain representation of the service is necessary + + + (Optional) Semantic human readable name for the service + in: query + required: false + type: string + - name: block_height + description: The block height to query the session for + in: query + required: false + type: string + format: int64 tags: - Query /pocket/session/params: @@ -47104,9 +47430,7 @@ paths: type: string title: >- The Bech32 address of the supplier using cosmos' - ScalarDescriptor to ensure deterministic deterministic - encoding using cosmos' ScalarDescriptor to ensure - deterministic deterministic encoding + ScalarDescriptor to ensure deterministic encoding stake: title: The total amount of uPOKT the supplier has staked type: object @@ -47123,6 +47447,82 @@ paths: custom method signatures required by gogoproto. + services: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + title: Unique identifier for the service + name: + type: string + title: Semantic name for the service + endpoints: + type: array + items: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, + SLAs or something else? There will be + more discussion once we get closer to + implementing on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as + proto maps can't be keyed by enums + title: >- + Additional configuration options for the + endpoint + title: >- + Endpoint message to hold service configuration + details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service configuration + the supplier stakes for + title: The service configs this supplier can support description: >- Supplier is the type defining the actor in Pocket Network that provides RPC services. @@ -47248,9 +47648,7 @@ paths: type: string title: >- The Bech32 address of the supplier using cosmos' - ScalarDescriptor to ensure deterministic deterministic - encoding using cosmos' ScalarDescriptor to ensure - deterministic deterministic encoding + ScalarDescriptor to ensure deterministic encoding stake: title: The total amount of uPOKT the supplier has staked type: object @@ -47267,6 +47665,82 @@ paths: custom method signatures required by gogoproto. + services: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + title: Unique identifier for the service + name: + type: string + title: Semantic name for the service + endpoints: + type: array + items: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, + SLAs or something else? There will be + more discussion once we get closer to + implementing on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as + proto maps can't be keyed by enums + title: >- + Additional configuration options for the + endpoint + title: >- + Endpoint message to hold service configuration + details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service configuration + the supplier stakes for + title: The service configs this supplier can support description: >- Supplier is the type defining the actor in Pocket Network that provides RPC services. @@ -76041,8 +76515,7 @@ definitions: type: string title: >- The Bech32 address of the application using cosmos' ScalarDescriptor - to ensure deterministic deterministic encoding using cosmos' - ScalarDescriptor to ensure deterministic deterministic encoding + to ensure deterministic encoding stake: title: The total amount of uPOKT the application has staked type: object @@ -76056,6 +76529,54 @@ definitions: NOTE: The amount field is an Int which implements the custom method signatures required by gogoproto. + service_ids: + type: array + items: + type: object + properties: + id: + type: string + title: Unique identifier for the service + name: + type: string + title: (Optional) Semantic human readable name for the service + title: >- + ServiceId message to encapsulate unique and semantic identifiers for + a service on the network + description: The ID of the service this session is servicing + title: >- + TODO(@olshansk): Change this to `shared.ApplicationServiceConfig` in + #95 + service_configs: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary + title: >- + ApplicationServiceConfig holds the service configuration the + application stakes for + title: The ID of the service this session is servicing title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76082,9 +76603,7 @@ definitions: type: string title: >- The Bech32 address of the application using cosmos' - ScalarDescriptor to ensure deterministic deterministic encoding - using cosmos' ScalarDescriptor to ensure deterministic - deterministic encoding + ScalarDescriptor to ensure deterministic encoding stake: title: The total amount of uPOKT the application has staked type: object @@ -76101,6 +76620,24 @@ definitions: method signatures required by gogoproto. + service_ids: + type: array + items: + type: object + properties: + id: + type: string + title: Unique identifier for the service + name: + type: string + title: (Optional) Semantic human readable name for the service + title: >- + ServiceId message to encapsulate unique and semantic + identifiers for a service on the network + description: The ID of the service this session is servicing + title: >- + TODO(@olshansk): Change this to + `shared.ApplicationServiceConfig` in #95 title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76140,9 +76677,7 @@ definitions: type: string title: >- The Bech32 address of the application using cosmos' - ScalarDescriptor to ensure deterministic deterministic encoding - using cosmos' ScalarDescriptor to ensure deterministic - deterministic encoding + ScalarDescriptor to ensure deterministic encoding stake: title: The total amount of uPOKT the application has staked type: object @@ -76159,6 +76694,24 @@ definitions: method signatures required by gogoproto. + service_ids: + type: array + items: + type: object + properties: + id: + type: string + title: Unique identifier for the service + name: + type: string + title: (Optional) Semantic human readable name for the service + title: >- + ServiceId message to encapsulate unique and semantic identifiers + for a service on the network + description: The ID of the service this session is servicing + title: >- + TODO(@olshansk): Change this to `shared.ApplicationServiceConfig` + in #95 title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76169,6 +76722,20 @@ definitions: description: params holds all the parameters of this module. type: object description: QueryParamsResponse is response type for the Query/Params RPC method. + pocket.shared.ServiceId: + type: object + properties: + id: + type: string + title: Unique identifier for the service + description: Unique identifier for the service + name: + type: string + title: (Optional) Semantic human readable name for the service + description: (Optional) Semantic human readable name for the service + title: >- + ServiceId message to encapsulate unique and semantic identifiers for a + service on the network pocket.gateway.Gateway: type: object properties: @@ -76305,13 +76872,496 @@ definitions: description: Params defines the parameters for the module. pocket.session.QueryGetSessionResponse: type: object - pocket.session.QueryParamsResponse: - type: object properties: - params: - description: params holds all the parameters of this module. + session: + type: object + properties: + header: + title: The header of the session containing lightweight data + type: object + properties: + application_address: + type: string + title: >- + The Bech32 address of the application using cosmos' + ScalarDescriptor to ensure deterministic encoding + service_id: + title: The ID of the service this session is servicing + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary + session_start_block_height: + type: string + format: int64 + title: The height at which this session started + session_id: + type: string + description: A unique pseudoranom ID for this session + title: >- + NOTE: session_id can be derived from the above values using + on-chain but is included in the header for convenience + description: >- + SessionHeader is a lightweight header for a session that can be + passed around. + + It is the minimal amount of data required to hydrate & retrieve + all data relevant to the session. + session_id: + type: string + title: A unique pseudoranom ID for this session + session_number: + type: string + format: int64 + title: The session number since genesis + num_blocks_per_session: + type: string + format: int64 + title: The number of blocks per session when this session started + application: + title: A fully hydrated application object this session is for + type: object + properties: + address: + type: string + title: >- + The Bech32 address of the application using cosmos' + ScalarDescriptor to ensure deterministic encoding + stake: + title: The total amount of uPOKT the application has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the custom + method + + signatures required by gogoproto. + service_configs: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session + for a certain service but with some additional + configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as + a reminder than an optional onchain representation + of the service is necessary + title: >- + ApplicationServiceConfig holds the service configuration the + application stakes for + title: The ID of the service this session is servicing + suppliers: + type: array + items: + type: object + properties: + address: + type: string + title: >- + The Bech32 address of the supplier using cosmos' + ScalarDescriptor to ensure deterministic encoding + stake: + title: The total amount of uPOKT the supplier has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the custom + method + + signatures required by gogoproto. + services: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session + for a certain service but with some additional + configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts + as a reminder than an optional onchain + representation of the service is necessary + endpoints: + type: array + items: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, + SLAs or something else? There will be more + discussion once we get closer to + implementing on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as proto + maps can't be keyed by enums + title: >- + Additional configuration options for the + endpoint + title: >- + SupplierEndpoint message to hold service + configuration details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service configuration the + supplier stakes for + title: The service configs this supplier can support + description: >- + Supplier is the type defining the actor in Pocket Network that + provides RPC services. + title: A fully hydrated set of servicers that are serving the application + description: >- + Session is a fully hydrated session object that contains all the + information for the Session + + and its parcipants. + pocket.session.QueryParamsResponse: + type: object + properties: + params: + description: params holds all the parameters of this module. type: object description: QueryParamsResponse is response type for the Query/Params RPC method. + pocket.session.Session: + type: object + properties: + header: + title: The header of the session containing lightweight data + type: object + properties: + application_address: + type: string + title: >- + The Bech32 address of the application using cosmos' + ScalarDescriptor to ensure deterministic encoding + service_id: + title: The ID of the service this session is servicing + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that identify + it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder + than an optional onchain representation of the service is + necessary + session_start_block_height: + type: string + format: int64 + title: The height at which this session started + session_id: + type: string + description: A unique pseudoranom ID for this session + title: >- + NOTE: session_id can be derived from the above values using + on-chain but is included in the header for convenience + description: >- + SessionHeader is a lightweight header for a session that can be passed + around. + + It is the minimal amount of data required to hydrate & retrieve all + data relevant to the session. + session_id: + type: string + title: A unique pseudoranom ID for this session + session_number: + type: string + format: int64 + title: The session number since genesis + num_blocks_per_session: + type: string + format: int64 + title: The number of blocks per session when this session started + application: + title: A fully hydrated application object this session is for + type: object + properties: + address: + type: string + title: >- + The Bech32 address of the application using cosmos' + ScalarDescriptor to ensure deterministic encoding + stake: + title: The total amount of uPOKT the application has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the custom + method + + signatures required by gogoproto. + service_configs: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary + title: >- + ApplicationServiceConfig holds the service configuration the + application stakes for + title: The ID of the service this session is servicing + suppliers: + type: array + items: + type: object + properties: + address: + type: string + title: >- + The Bech32 address of the supplier using cosmos' + ScalarDescriptor to ensure deterministic encoding + stake: + title: The total amount of uPOKT the supplier has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the custom + method + + signatures required by gogoproto. + services: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for + a certain service but with some additional configs + that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of + the service is necessary + endpoints: + type: array + items: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, SLAs + or something else? There will be more + discussion once we get closer to implementing + on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as proto + maps can't be keyed by enums + title: Additional configuration options for the endpoint + title: >- + SupplierEndpoint message to hold service configuration + details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service configuration the + supplier stakes for + title: The service configs this supplier can support + description: >- + Supplier is the type defining the actor in Pocket Network that + provides RPC services. + title: A fully hydrated set of servicers that are serving the application + description: >- + Session is a fully hydrated session object that contains all the + information for the Session + + and its parcipants. pocket.session.SessionHeader: type: object properties: @@ -76320,16 +77370,30 @@ definitions: title: >- The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding + service_id: + title: The ID of the service this session is servicing + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created + to enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder + than an optional onchain representation of the service is + necessary session_start_block_height: type: string format: int64 - description: The height at which this session started - title: >- - TODO(@Olshansk): Uncomment the line below once the `ServiceId` proto - is defined - - service.ServiceId service_id = 2; // The ID of the service this - session is servicing + title: The height at which this session started session_id: type: string description: A unique pseudoranom ID for this session @@ -76342,6 +77406,85 @@ definitions: It is the minimal amount of data required to hydrate & retrieve all data relevant to the session. + pocket.shared.ApplicationServiceConfig: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created + to enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder + than an optional onchain representation of the service is + necessary + title: >- + ApplicationServiceConfig holds the service configuration the application + stakes for + pocket.shared.ConfigOption: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, SLAs or something else? There + will be more discussion once we get closer to implementing on-chain + QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as proto maps can't be keyed by + enums + pocket.shared.ConfigOptions: + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, SLAs or something else? There will + be more discussion once we get closer to implementing on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + pocket.shared.RPCType: + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + title: Enum to define RPC types pocket.shared.Supplier: type: object properties: @@ -76349,8 +77492,7 @@ definitions: type: string title: >- The Bech32 address of the supplier using cosmos' ScalarDescriptor to - ensure deterministic deterministic encoding using cosmos' - ScalarDescriptor to ensure deterministic deterministic encoding + ensure deterministic encoding stake: title: The total amount of uPOKT the supplier has staked type: object @@ -76364,9 +77506,218 @@ definitions: NOTE: The amount field is an Int which implements the custom method signatures required by gogoproto. + services: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary + endpoints: + type: array + items: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, SLAs or + something else? There will be more discussion once + we get closer to implementing on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as proto maps + can't be keyed by enums + title: Additional configuration options for the endpoint + title: SupplierEndpoint message to hold service configuration details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service configuration the supplier + stakes for + title: The service configs this supplier can support description: >- Supplier is the type defining the actor in Pocket Network that provides RPC services. + pocket.shared.SupplierEndpoint: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, SLAs or something else? + There will be more discussion once we get closer to implementing + on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as proto maps can't be keyed + by enums + title: Additional configuration options for the endpoint + title: SupplierEndpoint message to hold service configuration details + pocket.shared.SupplierServiceConfig: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created + to enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder + than an optional onchain representation of the service is + necessary + endpoints: + type: array + items: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, SLAs or something + else? There will be more discussion once we get closer to + implementing on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as proto maps can't be + keyed by enums + title: Additional configuration options for the endpoint + title: SupplierEndpoint message to hold service configuration details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service configuration the supplier stakes + for pocket.supplier.MsgCreateClaimResponse: type: object pocket.supplier.MsgStakeSupplierResponse: @@ -76390,9 +77741,7 @@ definitions: type: string title: >- The Bech32 address of the supplier using cosmos' - ScalarDescriptor to ensure deterministic deterministic encoding - using cosmos' ScalarDescriptor to ensure deterministic - deterministic encoding + ScalarDescriptor to ensure deterministic encoding stake: title: The total amount of uPOKT the supplier has staked type: object @@ -76409,6 +77758,78 @@ definitions: method signatures required by gogoproto. + services: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + title: Unique identifier for the service + name: + type: string + title: Semantic name for the service + endpoints: + type: array + items: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, SLAs + or something else? There will be more + discussion once we get closer to implementing + on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as proto + maps can't be keyed by enums + title: Additional configuration options for the endpoint + title: Endpoint message to hold service configuration details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service configuration the + supplier stakes for + title: The service configs this supplier can support description: >- Supplier is the type defining the actor in Pocket Network that provides RPC services. @@ -76448,8 +77869,7 @@ definitions: type: string title: >- The Bech32 address of the supplier using cosmos' ScalarDescriptor - to ensure deterministic deterministic encoding using cosmos' - ScalarDescriptor to ensure deterministic deterministic encoding + to ensure deterministic encoding stake: title: The total amount of uPOKT the supplier has staked type: object @@ -76466,6 +77886,77 @@ definitions: method signatures required by gogoproto. + services: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + title: Unique identifier for the service + name: + type: string + title: Semantic name for the service + endpoints: + type: array + items: + type: object + properties: + url: + type: string + title: URL of the endpoint + rpc_type: + title: Type of RPC exposed on the url above + type: string + enum: + - UNKNOWN_RPC + - GRPC + - WEBSOCKET + - JSON_RPC + default: UNKNOWN_RPC + description: |- + - UNKNOWN_RPC: Undefined RPC type + - GRPC: gRPC + - WEBSOCKET: WebSocket + - JSON_RPC: JSON-RPC + configs: + type: array + items: + type: object + properties: + key: + title: Config option key + type: string + enum: + - UNKNOWN_CONFIG + - TIMEOUT + default: UNKNOWN_CONFIG + description: >- + Enum to define configuration options + + TODO_RESEARCH: Should these be configs, SLAs or + something else? There will be more discussion + once we get closer to implementing on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as proto maps + can't be keyed by enums + title: Additional configuration options for the endpoint + title: Endpoint message to hold service configuration details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service configuration the + supplier stakes for + title: The service configs this supplier can support description: >- Supplier is the type defining the actor in Pocket Network that provides RPC services. diff --git a/go.mod b/go.mod index 9c881afe5..8931ee507 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( cosmossdk.io/math v1.0.1 github.com/cometbft/cometbft v0.37.2 github.com/cometbft/cometbft-db v0.8.0 + github.com/cosmos/cosmos-proto v1.0.0-beta.2 github.com/cosmos/cosmos-sdk v0.47.3 github.com/cosmos/gogoproto v1.4.10 github.com/cosmos/ibc-go/v7 v7.1.0 @@ -21,7 +22,9 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 + golang.org/x/crypto v0.12.0 golang.org/x/sync v0.3.0 + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 google.golang.org/grpc v1.56.1 gopkg.in/yaml.v2 v2.4.0 ) @@ -66,7 +69,6 @@ require ( github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect - github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.0 // indirect @@ -252,7 +254,6 @@ require ( go.uber.org/fx v1.19.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.12.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.14.0 // indirect @@ -265,7 +266,6 @@ require ( gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/api v0.122.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/observable/channel/observer.go b/pkg/observable/channel/observer.go index 394a6bdbb..3a2455e64 100644 --- a/pkg/observable/channel/observer.go +++ b/pkg/observable/channel/observer.go @@ -92,7 +92,7 @@ func (obsvr *channelObserver[V]) unsubscribe() { // 1. this is library code; prefer fewer external dependencies, esp. I/O // 2. the stdlib log pkg is pretty good, idiomatic, and globally // configurable; perhaps it is sufficient - log.Printf("%s", observable.ErrObserverClosed.Wrap("redundant unsubscribe")) + log.Printf("%s", observable.ErrObserverClosed.Wrap("WARN: redundant unsubscribe")) return } diff --git a/proto/pocket/application/application.proto b/proto/pocket/application/application.proto index 0f9cfebfa..4ccb1940a 100644 --- a/proto/pocket/application/application.proto +++ b/proto/pocket/application/application.proto @@ -5,12 +5,13 @@ option go_package = "pocket/x/application/types"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; +import "pocket/shared/service.proto"; // Application defines the type used to store an on-chain definition and state for an application message Application { - string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked - // TODO(@Olshansk): Uncomment the line below once the `ServiceId` proto is defined - // repeated service.ServiceId service_ids = 3; // The ID of the service this session is servicing + // TODO(@olshansk): Change this to `shared.ApplicationServiceConfig` in #95 + repeated shared.ServiceId service_ids = 3; // The ID of the service this session is servicing } diff --git a/proto/pocket/application/tx.proto b/proto/pocket/application/tx.proto index f20ea3fb5..4bf9eb789 100644 --- a/proto/pocket/application/tx.proto +++ b/proto/pocket/application/tx.proto @@ -17,11 +17,11 @@ service Msg { } message MsgStakeApplication { option (cosmos.msg.v1.signer) = "address"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries - string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding - cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked. Must be ≥ to the current amount that the application has staked (if any) - // TODO(@Olshansk): Uncomment the line below once the `ServiceId` proto is defined - // repeated service.ServiceId service_ids = 3; // The ID of the service this session is servicing + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding + cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked. Must be ≥ to the current amount that the application has staked (if any) + // TODO(@Olshansk): Update the tx flow to add support for `services` + // repeated service.ApplicationServiceConfig services = 3; // The list of services this application is staked to request service for } message MsgStakeApplicationResponse {} diff --git a/proto/pocket/pocket/query.proto b/proto/pocket/pocket/query.proto index d0e72e3fc..3e983730d 100644 --- a/proto/pocket/pocket/query.proto +++ b/proto/pocket/pocket/query.proto @@ -3,7 +3,6 @@ package pocket.pocket; import "gogoproto/gogo.proto"; import "google/api/annotations.proto"; -import "cosmos/base/query/v1beta1/pagination.proto"; import "pocket/pocket/params.proto"; option go_package = "pocket/x/pocket/types"; diff --git a/proto/pocket/session/query.proto b/proto/pocket/session/query.proto index 55ea79e73..cd3ef8380 100644 --- a/proto/pocket/session/query.proto +++ b/proto/pocket/session/query.proto @@ -1,27 +1,29 @@ syntax = "proto3"; - package pocket.session; import "gogoproto/gogo.proto"; import "google/api/annotations.proto"; -import "cosmos/base/query/v1beta1/pagination.proto"; +import "cosmos_proto/cosmos.proto"; + import "pocket/session/params.proto"; +import "pocket/session/session.proto"; +import "pocket/shared/service.proto"; option go_package = "pocket/x/session/types"; // Query defines the gRPC querier service. service Query { - + // Parameters queries the parameters of the module. rpc Params (QueryParamsRequest) returns (QueryParamsResponse) { option (google.api.http).get = "/pocket/session/params"; - + } - + // Queries a list of GetSession items. rpc GetSession (QueryGetSessionRequest) returns (QueryGetSessionResponse) { option (google.api.http).get = "/pocket/session/get_session"; - + } } // QueryParamsRequest is request type for the Query/Params RPC method. @@ -29,12 +31,18 @@ message QueryParamsRequest {} // QueryParamsResponse is response type for the Query/Params RPC method. message QueryParamsResponse { - + // params holds all the parameters of this module. Params params = 1 [(gogoproto.nullable) = false]; } -message QueryGetSessionRequest {} +message QueryGetSessionRequest { + string application_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding + shared.ServiceId service_id = 2; // The service id to query the session for + int64 block_height = 3; // The block height to query the session for +} -message QueryGetSessionResponse {} +message QueryGetSessionResponse { + session.Session session = 1; +} diff --git a/proto/pocket/session/session.proto b/proto/pocket/session/session.proto index e0945c890..7d864e1b1 100644 --- a/proto/pocket/session/session.proto +++ b/proto/pocket/session/session.proto @@ -4,8 +4,7 @@ package pocket.session; option go_package = "pocket/x/session/types"; import "cosmos_proto/cosmos.proto"; -// TODO(@Olshansk): Uncomment the line below once the service.proto file is added -// import "pocket/service/service.proto"; +import "pocket/shared/service.proto"; import "pocket/application/application.proto"; import "pocket/shared/supplier.proto"; @@ -15,8 +14,7 @@ import "pocket/shared/supplier.proto"; // It is the minimal amount of data required to hydrate & retrieve all data relevant to the session. message SessionHeader { string application_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding - // TODO(@Olshansk): Uncomment the line below once the `ServiceId` proto is defined - // service.ServiceId service_id = 2; // The ID of the service this session is servicing + shared.ServiceId service_id = 2; // The ID of the service this session is servicing int64 session_start_block_height = 3; // The height at which this session started // NOTE: session_id can be derived from the above values using on-chain but is included in the header for convenience string session_id = 4; // A unique pseudoranom ID for this session @@ -29,8 +27,6 @@ message Session { string session_id = 2; // A unique pseudoranom ID for this session int64 session_number = 3; // The session number since genesis int64 num_blocks_per_session = 4; // The number of blocks per session when this session started - // TODO(@Olshansk): Uncomment the line below once the `Service` proto is defined - // service.Service service = 5; // A fully hydrated service object this session is for - application.Application application = 6; // A fully hydrated application object this session is for - repeated shared.Supplier suppliers = 7; // A fully hydrated set of servicers that are serving the application + application.Application application = 5; // A fully hydrated application object this session is for + repeated shared.Supplier suppliers = 6; // A fully hydrated set of servicers that are serving the application } \ No newline at end of file diff --git a/proto/pocket/shared/service.proto b/proto/pocket/shared/service.proto index 7c56b6166..cff126d35 100644 --- a/proto/pocket/shared/service.proto +++ b/proto/pocket/shared/service.proto @@ -1,31 +1,35 @@ syntax = "proto3"; -package pocket.shared; // NOTE that the `shared` package is not a Cosmos module, // but rather a manually created package to resolve circular dependencies. - -// TODO_CLEANUP(@Olshansk): Add native optional identifiers once its supported; https://github.com/ignite/cli/issues/3698 +package pocket.shared; option go_package = "pocket/x/shared/types"; +// TODO_CLEANUP(@Olshansk): Add native optional identifiers once its supported; https://github.com/ignite/cli/issues/3698 + // ServiceId message to encapsulate unique and semantic identifiers for a service on the network message ServiceId { string id = 1; // Unique identifier for the service - string name = 2; // Semantic name for the service + string name = 2; // (Optional) Semantic human readable name for the service + // NOTE: `ServiceId.Id` may seem redundant but was designed to enable more complex service identification. + // For example, what if we want to request a session for a certain service but with some additional configs that identify it? } // SupplierServiceConfig holds the service configuration the application stakes for message ApplicationServiceConfig { - ServiceId id = 1; // Unique and semantic identifier for the service - // TODO_RESEARCH: There is an opportunity for applications to advertise what kind of configurations (and price) - // they want, but it is out of scope for the MVP. + repeated ServiceId service_id = 1; // Unique and semantic identifier for the service + // TODO_RESEARCH: There is an opportunity for applications to advertise the max + // they're willing to pay for a certain configuration/price, but this is outside of scope. // repeated RPCConfig rpc_configs = 2; // List of endpoints for the service } // SupplierServiceConfig holds the service configuration the supplier stakes for message SupplierServiceConfig { - ServiceId id = 1; // Unique and semantic identifier for the service + ServiceId service_id = 1; // Unique and semantic identifier for the service repeated SupplierEndpoint endpoints = 2; // List of endpoints for the service + // TODO_RESEARCH: There is an opportunity for supplier to advertise the min + // they're willing to earn for a certain configuration/price, but this is outside of scope. } // Endpoint message to hold service configuration details diff --git a/proto/pocket/shared/supplier.proto b/proto/pocket/shared/supplier.proto index 9e1826522..1aa626b16 100644 --- a/proto/pocket/shared/supplier.proto +++ b/proto/pocket/shared/supplier.proto @@ -8,12 +8,12 @@ option go_package = "pocket/x/shared/types"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; +import "pocket/shared/service.proto"; // Supplier is the type defining the actor in Pocket Network that provides RPC services. message Supplier { - string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the supplier using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the supplier using cosmos' ScalarDescriptor to ensure deterministic encoding cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the supplier has staked - // TODO(@Olshansk): Uncomment the line below once the `ServiceId` proto is defined - // repeated service.SupplierServiceConfig services = 3; // The service configs this supplier can support + repeated SupplierServiceConfig services = 3; // The service configs this supplier can support } diff --git a/proto/pocket/supplier/tx.proto b/proto/pocket/supplier/tx.proto index 8fa81c76f..d994b03aa 100644 --- a/proto/pocket/supplier/tx.proto +++ b/proto/pocket/supplier/tx.proto @@ -22,8 +22,8 @@ message MsgStakeSupplier { string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the supplier using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the supplier has staked. Must be ≥ to the current amount that the supplier has staked (if any) - // TODO(@Olshansk): Uncomment the line below once the `ServiceId` proto is defined - // repeated service.SupplierServiceConfig services = 3; // The ID of the service this session is servicing + // TODO(@Olshansk): Update the tx flow to add support for `services` + // repeated service.SupplierServiceConfig services = 3; // The list of services this supplier is staked to provide service for } message MsgStakeSupplierResponse {} diff --git a/testutil/keeper/session.go b/testutil/keeper/session.go index ae57bb952..cd3ea868f 100644 --- a/testutil/keeper/session.go +++ b/testutil/keeper/session.go @@ -1,6 +1,7 @@ package keeper import ( + "context" "testing" tmdb "github.com/cometbft/cometbft-db" @@ -12,9 +13,77 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" typesparams "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + + "pocket/testutil/sample" + mocks "pocket/testutil/session/mocks" + apptypes "pocket/x/application/types" "pocket/x/session/keeper" "pocket/x/session/types" + sharedtypes "pocket/x/shared/types" +) + +type option[V any] func(k *keeper.Keeper) + +var ( + TestServiceId1 = "svc1" + TestServiceId2 = "svc2" + + TestApp1Address = "pokt106grzmkmep67pdfrm6ccl9snynryjqus6l3vct" // Generated via sample.AccAddress() + TestApp1 = apptypes.Application{ + Address: TestApp1Address, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + ServiceIds: []*sharedtypes.ServiceId{ + { + Id: TestServiceId1, + }, + { + Id: TestServiceId2, + }, + }, + } + + TestApp2Address = "pokt1dm7tr0a99ja232gzt5rjtrl7hj6z6h40669fwh" // Generated via sample.AccAddress() + TestApp2 = apptypes.Application{ + Address: TestApp1Address, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + ServiceIds: []*sharedtypes.ServiceId{ + { + Id: TestServiceId1, + }, + { + Id: TestServiceId2, + }, + }, + } + + TestSupplierUrl = "http://olshansky.info" + TestSupplierAddress = sample.AccAddress() + TestSupplier = sharedtypes.Supplier{ + Address: TestSupplierAddress, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId1}, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: TestSupplierUrl, + RpcType: sharedtypes.RPCType_JSON_RPC, + }, + }, + }, + { + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId2}, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: TestSupplierUrl, + RpcType: sharedtypes.RPCType_GRPC, + }, + }, + }, + }, + } ) func SessionKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { @@ -30,6 +99,9 @@ func SessionKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { registry := codectypes.NewInterfaceRegistry() cdc := codec.NewProtoCodec(registry) + mockAppKeeper := defaultAppKeeperMock(t) + mockSupplierKeeper := defaultSupplierKeeperMock(t) + paramsSubspace := typesparams.NewSubspace(cdc, types.Amino, storeKey, @@ -41,8 +113,17 @@ func SessionKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { storeKey, memStoreKey, paramsSubspace, + + mockAppKeeper, + mockSupplierKeeper, ) + // TODO_TECHDEBT: See the comment at the bottom of this file explaining + // why we don't support options yet. + // for _, opt := range opts { + // opt(k) + // } + ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) // Initialize params @@ -50,3 +131,49 @@ func SessionKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { return k, ctx } + +func defaultAppKeeperMock(t testing.TB) types.ApplicationKeeper { + t.Helper() + ctrl := gomock.NewController(t) + + getAppFn := func(_ context.Context, appAddr string) (apptypes.Application, bool) { + switch appAddr { + case TestApp1Address: + return TestApp1, true + case TestApp2Address: + return TestApp2, true + default: + return apptypes.Application{}, false + } + } + + mockAppKeeper := mocks.NewMockApplicationKeeper(ctrl) + mockAppKeeper.EXPECT().GetApplication(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(getAppFn) + mockAppKeeper.EXPECT().GetApplication(gomock.Any(), TestApp1Address).AnyTimes().Return(TestApp1, true) + + return mockAppKeeper +} + +func defaultSupplierKeeperMock(t testing.TB) types.SupplierKeeper { + t.Helper() + ctrl := gomock.NewController(t) + + allSuppliers := []sharedtypes.Supplier{TestSupplier} + + mockSupplierKeeper := mocks.NewMockSupplierKeeper(ctrl) + mockSupplierKeeper.EXPECT().GetAllSupplier(gomock.Any()).AnyTimes().Return(allSuppliers) + + return mockSupplierKeeper +} + +// TODO_TECHDEBT: Figure out how to vary the supplierKeep on a per test basis with exposing `SupplierKeeper publically` + +// type option[V any] func(k *keeper.Keeper) + +// WithPublisher returns an option function which sets the given publishCh of the +// resulting observable when passed to NewObservable(). +// func WithSupplierKeeperMock(supplierKeeper types.SupplierKeeper) option[any] { +// return func(k *keeper.Keeper) { +// k.supplierKeeper = supplierKeeper +// } +// } diff --git a/testutil/session/mocks/mocks.go b/testutil/session/mocks/mocks.go new file mode 100644 index 000000000..4ccc3e251 --- /dev/null +++ b/testutil/session/mocks/mocks.go @@ -0,0 +1,6 @@ +package mocks + +// This file is in place to declare the package for dynamically generated structs. +// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. +// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go +// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests diff --git a/x/session/client/cli/query_get_session.go b/x/session/client/cli/query_get_session.go index f99dc6283..deebf1a31 100644 --- a/x/session/client/cli/query_get_session.go +++ b/x/session/client/cli/query_get_session.go @@ -6,28 +6,27 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" + "pocket/x/session/types" ) var _ = strconv.Itoa(0) +// TODO(@Olshansk): Implement the CLI component of `GetSession`. func CmdGetSession() *cobra.Command { cmd := &cobra.Command{ Use: "get-session", Short: "Query get-session", Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) (err error) { - clientCtx, err := client.GetClientQueryContext(cmd) if err != nil { return err } - queryClient := types.NewQueryClient(clientCtx) + req := &types.QueryGetSessionRequest{} - params := &types.QueryGetSessionRequest{} - - res, err := queryClient.GetSession(cmd.Context(), params) + res, err := queryClient.GetSession(cmd.Context(), req) if err != nil { return err } diff --git a/x/session/keeper/keeper.go b/x/session/keeper/keeper.go index 1bf4d8958..4515d5a1e 100644 --- a/x/session/keeper/keeper.go +++ b/x/session/keeper/keeper.go @@ -18,6 +18,9 @@ type ( storeKey storetypes.StoreKey memKey storetypes.StoreKey paramstore paramtypes.Subspace + + appKeeper types.ApplicationKeeper + supplierKeeper types.SupplierKeeper } ) @@ -27,6 +30,9 @@ func NewKeeper( memKey storetypes.StoreKey, ps paramtypes.Subspace, + appKeeper types.ApplicationKeeper, + supplierKeeper types.SupplierKeeper, + ) *Keeper { // set KeyTable if it has not already been set if !ps.HasKeyTable() { @@ -38,6 +44,9 @@ func NewKeeper( storeKey: storeKey, memKey: memKey, paramstore: ps, + + appKeeper: appKeeper, + supplierKeeper: supplierKeeper, } } diff --git a/x/session/keeper/msg_server_test.go b/x/session/keeper/msg_server_test.go index d1ce55e4d..61c82603a 100644 --- a/x/session/keeper/msg_server_test.go +++ b/x/session/keeper/msg_server_test.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + keepertest "pocket/testutil/keeper" "pocket/x/session/keeper" "pocket/x/session/types" diff --git a/x/session/keeper/query_get_session.go b/x/session/keeper/query_get_session.go index 9834daed9..f937f5033 100644 --- a/x/session/keeper/query_get_session.go +++ b/x/session/keeper/query_get_session.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "pocket/x/session/types" ) @@ -16,8 +17,14 @@ func (k Keeper) GetSession(goCtx context.Context, req *types.QueryGetSessionRequ ctx := sdk.UnwrapSDKContext(goCtx) - // TODO: Process the query - _ = ctx + sessionHydrator := NewSessionHydrator(req.ApplicationAddress, req.ServiceId.Id, req.BlockHeight) + session, err := k.HydrateSession(ctx, sessionHydrator) + if err != nil { + return nil, err + } - return &types.QueryGetSessionResponse{}, nil + res := &types.QueryGetSessionResponse{ + Session: session, + } + return res, nil } diff --git a/x/session/keeper/query_get_session_test.go b/x/session/keeper/query_get_session_test.go new file mode 100644 index 000000000..b18bc830c --- /dev/null +++ b/x/session/keeper/query_get_session_test.go @@ -0,0 +1,130 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "pocket/cmd/pocketd/cmd" + keepertest "pocket/testutil/keeper" + "pocket/x/session/types" + sharedtypes "pocket/x/shared/types" +) + +func init() { + cmd.InitSDKConfig() +} + +// NOTE: See `session_hydrator_test.go` for more extensive test coverage of different +// GetSession scenarios. This is just used to verify a few basic scenarios that act as +// the Cosmos SDK context aware wrapper around it. + +func TestSession_GetSession_Success(t *testing.T) { + keeper, ctx := keepertest.SessionKeeper(t) + wctx := sdk.WrapSDKContext(ctx) + + type test struct { + name string + + appAddr string + serviceId string + blockHeight int64 + + expectedSessionId string + expectedSessionNumber int64 + expectedNumSuppliers int + } + + tests := []test{ + { + name: "valid - app1 svc1 at height=1", + + appAddr: keepertest.TestApp1Address, + serviceId: keepertest.TestServiceId1, + blockHeight: 1, + + // Intentionally only checking a subset of the session metadata returned + expectedSessionId: "e1e51d087e447525d7beb648711eb3deaf016a8089938a158e6a0f600979370c", + expectedSessionNumber: 0, + expectedNumSuppliers: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + req := &types.QueryGetSessionRequest{ + ApplicationAddress: tt.appAddr, + ServiceId: &sharedtypes.ServiceId{ + Id: tt.serviceId, + }, + BlockHeight: 1, + } + + response, err := keeper.GetSession(wctx, req) + require.NoError(t, err) + require.NotNil(t, response) + + require.Equal(t, tt.expectedSessionId, response.Session.SessionId) + require.Equal(t, tt.expectedSessionNumber, response.Session.SessionNumber) + require.Len(t, response.Session.Suppliers, tt.expectedNumSuppliers) + }) + } +} + +func TestSession_GetSession_Failure(t *testing.T) { + keeper, ctx := keepertest.SessionKeeper(t) + wctx := sdk.WrapSDKContext(ctx) + + type test struct { + name string + + appAddr string + serviceId string + blockHeight int64 + + expectedErrContains string + } + + tests := []test{ + { + name: "application address does not reflected a staked application", + + appAddr: "some string that is not a valid app address", + serviceId: keepertest.TestServiceId1, + blockHeight: 1, + + expectedErrContains: types.ErrAppNotFound.Error(), + }, + { + name: "service ID does not reflect one with staked suppliers", + + appAddr: keepertest.TestApp1Address, + serviceId: "some string that is not a valid service Id", + blockHeight: 1, + + expectedErrContains: types.ErrSuppliersNotFound.Error(), + }, + } + + expectedRes := (*types.QueryGetSessionResponse)(nil) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + req := &types.QueryGetSessionRequest{ + ApplicationAddress: tt.appAddr, + ServiceId: &sharedtypes.ServiceId{ + Id: tt.serviceId, + }, + BlockHeight: 1, + } + + res, err := keeper.GetSession(wctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedErrContains) + require.Equal(t, expectedRes, res) + }) + } +} diff --git a/x/session/keeper/query_params_test.go b/x/session/keeper/query_params_test.go index 80d5a77a6..c7ff9b68a 100644 --- a/x/session/keeper/query_params_test.go +++ b/x/session/keeper/query_params_test.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + testkeeper "pocket/testutil/keeper" "pocket/x/session/types" ) diff --git a/x/session/keeper/session_hydrator.go b/x/session/keeper/session_hydrator.go new file mode 100644 index 000000000..7f4e45afb --- /dev/null +++ b/x/session/keeper/session_hydrator.go @@ -0,0 +1,223 @@ +package keeper + +import ( + "crypto" + "encoding/binary" + "encoding/hex" + "fmt" + "math/rand" + + sdkerrors "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + _ "golang.org/x/crypto/sha3" + + "pocket/x/session/types" + sharedtypes "pocket/x/shared/types" +) + +var SHA3HashLen = crypto.SHA3_256.Size() + +// TODO(#21): Make these configurable governance param +const ( + NumBlocksPerSession = 4 + NumSupplierPerSession = 15 + SessionIDComponentDelimiter = "." +) + +type sessionHydrator struct { + // The session header that is used to hydrate the rest of the session data + sessionHeader *types.SessionHeader + + // The fully hydrated session object + session *types.Session + + // The height at which the session being request + blockHeight int64 + + // A redundant helper that maintains a hex decoded copy of `session.Id` used for session hydration + sessionIdBz []byte +} + +func NewSessionHydrator( + appAddress string, + serviceId string, + blockHeight int64, +) *sessionHydrator { + sessionHeader := &types.SessionHeader{ + ApplicationAddress: appAddress, + ServiceId: &sharedtypes.ServiceId{Id: serviceId}, + } + return &sessionHydrator{ + sessionHeader: sessionHeader, + session: &types.Session{}, + blockHeight: blockHeight, + sessionIdBz: make([]byte, 0), + } +} + +// GetSession implements of the exposed `UtilityModule.GetSession` function +// TECHDEBT(#519): Add custom error types depending on the type of issue that occurred and assert on them in the unit tests. +func (k Keeper) HydrateSession(ctx sdk.Context, sh *sessionHydrator) (*types.Session, error) { + logger := k.Logger(ctx).With("method", "hydrateSession") + + if err := k.hydrateSessionMetadata(ctx, sh); err != nil { + return nil, sdkerrors.Wrapf(types.ErrHydratingSession, "failed to hydrate the session metadata: %v", err) + } + logger.Debug("Finished hydrating session metadata") + + if err := k.hydrateSessionID(ctx, sh); err != nil { + return nil, sdkerrors.Wrapf(types.ErrHydratingSession, "failed to hydrate the session ID: %v", err) + } + logger.Info("Finished hydrating session ID: %s", sh.sessionHeader.SessionId) + + if err := k.hydrateSessionApplication(ctx, sh); err != nil { + return nil, sdkerrors.Wrapf(types.ErrHydratingSession, "failed to hydrate application for session: %v", err) + } + logger.Debug("Finished hydrating session application: %+v", sh.session.Application) + + if err := k.hydrateSessionSuppliers(ctx, sh); err != nil { + return nil, sdkerrors.Wrapf(types.ErrHydratingSession, "failed to hydrate suppliers for session: %v", err) + } + logger.Debug("Finished hydrating session suppliers: %+v") + + sh.session.Header = sh.sessionHeader + sh.session.SessionId = sh.sessionHeader.SessionId + + return sh.session, nil +} + +// hydrateSessionMetadata hydrates metadata related to the session such as the height at which the session started, its number, the number of blocks per session, etc.. +func (k Keeper) hydrateSessionMetadata(ctx sdk.Context, sh *sessionHydrator) error { + // TODO_TECHDEBT: Add a test if `blockHeight` is ahead of the current chain or what this node is aware of + + sh.session.NumBlocksPerSession = NumBlocksPerSession + sh.session.SessionNumber = int64(sh.blockHeight / NumBlocksPerSession) + sh.sessionHeader.SessionStartBlockHeight = sh.blockHeight - (sh.blockHeight % NumBlocksPerSession) + return nil +} + +// hydrateSessionID use both session and on-chain data to determine a unique session ID +func (k Keeper) hydrateSessionID(ctx sdk.Context, sh *sessionHydrator) error { + // TODO_BLOCKER: Need to retrieve the block hash at SessionStartBlockHeight, but this requires + // a bit of work and the `ctx` only gives access to the current block/header. See this thread + // for more details: https://github.com/pokt-network/poktroll/pull/78/files#r1369215667 + // prevHashBz := ctx.HeaderHash() + prevHashBz := []byte("TODO_BLOCKER: See the comment above") + appPubKeyBz := []byte(sh.sessionHeader.ApplicationAddress) + + // TODO_TECHDEBT: In the future, we will need to valid that the ServiceId is a valid service depending on whether + // or not its permissioned or permissionless + // TODO(@Olshansk): Add a check to make sure `IsValidServiceName(ServiceId.Id)` returns True + serviceIdBz := []byte(sh.sessionHeader.ServiceId.Id) + + sessionHeightBz := make([]byte, 8) + binary.LittleEndian.PutUint64(sessionHeightBz, uint64(sh.sessionHeader.SessionStartBlockHeight)) + + sh.sessionIdBz = concatWithDelimiter(SessionIDComponentDelimiter, prevHashBz, serviceIdBz, appPubKeyBz, sessionHeightBz) + sh.sessionHeader.SessionId = hex.EncodeToString(sha3Hash(sh.sessionIdBz)) + + return nil +} + +// hydrateSessionApplication hydrates the full Application actor based on the address provided +func (k Keeper) hydrateSessionApplication(ctx sdk.Context, sh *sessionHydrator) error { + app, appIsFound := k.appKeeper.GetApplication(ctx, sh.sessionHeader.ApplicationAddress) + if !appIsFound { + return sdkerrors.Wrapf(types.ErrAppNotFound, "could not find app with address: %s at height %d", sh.sessionHeader.ApplicationAddress, sh.sessionHeader.SessionStartBlockHeight) + } + sh.session.Application = &app + return nil +} + +// hydrateSessionSuppliers finds the suppliers that are staked at the session height and populates the session with them +func (k Keeper) hydrateSessionSuppliers(ctx sdk.Context, sh *sessionHydrator) error { + logger := k.Logger(ctx).With("method", "hydrateSessionSuppliers") + + // TODO_TECHDEBT(@Olshansk, @bryanchriswhite): Need to retrieve the suppliers at SessionStartBlockHeight, + // NOT THE CURRENT ONE which is what's provided by the context. For now, for simplicity, + // only retrieving the suppliers at the current block height which could create a discrepancy + // if new suppliers were staked mid session. + // TODO(@bryanchriswhite): Investigate if `BlockClient` + `ReplayObservable` where `N = SessionLength` could be used here.` + suppliers := k.supplierKeeper.GetAllSupplier(ctx) + + candidateSuppliers := make([]*sharedtypes.Supplier, 0) + for _, supplier := range suppliers { + // TODO_OPTIMIZE: If `supplier.Services` was a map[string]struct{}, we could eliminate `slices.Contains()`'s loop + for _, supplierServiceConfig := range supplier.Services { + if supplierServiceConfig.ServiceId.Id == sh.sessionHeader.ServiceId.Id { + candidateSuppliers = append(candidateSuppliers, &supplier) + break + } + } + } + + if len(candidateSuppliers) == 0 { + logger.Error("[ERROR] no suppliers found for session") + return sdkerrors.Wrapf(types.ErrSuppliersNotFound, "could not find suppliers for service %s at height %d", sh.sessionHeader.ServiceId, sh.sessionHeader.SessionStartBlockHeight) + } + + if len(candidateSuppliers) < NumSupplierPerSession { + logger.Info("[WARN] number of available suppliers (%d) is less than the number of suppliers per session (%d)", len(candidateSuppliers), NumSupplierPerSession) + sh.session.Suppliers = candidateSuppliers + } else { + sh.session.Suppliers = pseudoRandomSelection(candidateSuppliers, NumSupplierPerSession, sh.sessionIdBz) + } + + return nil +} + +// TODO_INVESTIGATE: We are using a `Go` native implementation for a pseudo-random number generator. In order +// for it to be language agnostic, a general purpose algorithm MUST be used. +// pseudoRandomSelection returns a random subset of the candidates. +func pseudoRandomSelection(candidates []*sharedtypes.Supplier, numTarget int, sessionIdBz []byte) []*sharedtypes.Supplier { + // Take the first 8 bytes of sessionId to use as the seed + // NB: There is specific reason why `BigEndian` was chosen over `LittleEndian` in this specific context. + seed := int64(binary.BigEndian.Uint64(sha3Hash(sessionIdBz)[:8])) + + // Retrieve the indices for the candidates + actors := make([]*sharedtypes.Supplier, 0) + uniqueIndices := uniqueRandomIndices(seed, int64(len(candidates)), int64(numTarget)) + for idx := range uniqueIndices { + actors = append(actors, candidates[idx]) + } + + return actors +} + +// uniqueRandomIndices returns a map of `numIndices` unique random numbers less than `maxIndex` +// seeded by `seed`. +// panics if `numIndicies > maxIndex` since that code path SHOULD never be executed. +// NB: A map pointing to empty structs is used to simulate set behavior. +func uniqueRandomIndices(seed, maxIndex, numIndices int64) map[int64]struct{} { + // This should never happen + if numIndices > maxIndex { + panic(fmt.Sprintf("uniqueRandomIndices: numIndices (%d) is greater than maxIndex (%d)", numIndices, maxIndex)) + } + + // create a new random source with the seed + randSrc := rand.NewSource(seed) + + // initialize a map to capture the indicesMap we'll return + indicesMap := make(map[int64]struct{}, maxIndex) + + // The random source could potentially return duplicates, so while loop until we have enough unique indices + for int64(len(indicesMap)) < numIndices { + indicesMap[randSrc.Int63()%int64(maxIndex)] = struct{}{} + } + + return indicesMap +} + +func concatWithDelimiter(delimiter string, b ...[]byte) (result []byte) { + for _, bz := range b { + result = append(result, bz...) + result = append(result, []byte(delimiter)...) + } + return result +} + +func sha3Hash(bz []byte) []byte { + hasher := crypto.SHA3_256.New() + hasher.Write(bz) + return hasher.Sum(nil) +} diff --git a/x/session/keeper/session_hydrator_test.go b/x/session/keeper/session_hydrator_test.go new file mode 100644 index 000000000..83d8a3876 --- /dev/null +++ b/x/session/keeper/session_hydrator_test.go @@ -0,0 +1,305 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + keepertest "pocket/testutil/keeper" + "pocket/testutil/sample" + "pocket/x/session/keeper" + "pocket/x/session/types" +) + +func TestSession_HydrateSession_Success_BaseCase(t *testing.T) { + sessionKeeper, ctx := keepertest.SessionKeeper(t) + blockHeight := int64(10) + + sessionHydrator := keeper.NewSessionHydrator(keepertest.TestApp1Address, keepertest.TestServiceId1, blockHeight) + session, err := sessionKeeper.HydrateSession(ctx, sessionHydrator) + require.NoError(t, err) + + // Check the header + sessionHeader := session.Header + require.Equal(t, keepertest.TestApp1Address, sessionHeader.ApplicationAddress) + require.Equal(t, keepertest.TestServiceId1, sessionHeader.ServiceId.Id) + require.Equal(t, "", sessionHeader.ServiceId.Name) + require.Equal(t, int64(8), sessionHeader.SessionStartBlockHeight) + require.Equal(t, "23f037a10f9d51d020d27763c42dd391d7e71765016d95d0d61f36c4a122efd0", sessionHeader.SessionId) + + // Check the session + require.Equal(t, int64(4), session.NumBlocksPerSession) + require.Equal(t, "23f037a10f9d51d020d27763c42dd391d7e71765016d95d0d61f36c4a122efd0", session.SessionId) + require.Equal(t, int64(2), session.SessionNumber) + + // Check the application + app := session.Application + require.Equal(t, keepertest.TestApp1Address, app.Address) + require.Len(t, app.ServiceIds, 2) + + // Check the suppliers + suppliers := session.Suppliers + require.Len(t, suppliers, 1) + supplier := suppliers[0] + require.Equal(t, keepertest.TestSupplierAddress, supplier.Address) + require.Len(t, supplier.Services, 2) +} + +func TestSession_HydrateSession_Metadata(t *testing.T) { + type test struct { + name string + blockHeight int64 + + expectedNumBlocksPerSession int64 + expectedSessionNumber int64 + expectedSessionStartBlock int64 + } + + // TODO_TECHDEBT: Extend these tests once `NumBlocksPerSession` is configurable. + // Currently assumes NumBlocksPerSession=4 + tests := []test{ + { + name: "blockHeight = 0", + blockHeight: 0, + + expectedNumBlocksPerSession: 4, + expectedSessionNumber: 0, + expectedSessionStartBlock: 0, + }, + { + name: "blockHeight = 1", + blockHeight: 1, + + expectedNumBlocksPerSession: 4, + expectedSessionNumber: 0, + expectedSessionStartBlock: 0, + }, + { + name: "blockHeight = sessionHeight", + blockHeight: 4, + + expectedNumBlocksPerSession: 4, + expectedSessionNumber: 1, + expectedSessionStartBlock: 4, + }, + { + name: "blockHeight != sessionHeight", + blockHeight: 5, + + expectedNumBlocksPerSession: 4, + expectedSessionNumber: 1, + expectedSessionStartBlock: 4, + }, + } + + appAddr := keepertest.TestApp1Address + serviceId := keepertest.TestServiceId1 + sessionKeeper, ctx := keepertest.SessionKeeper(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sessionHydrator := keeper.NewSessionHydrator(appAddr, serviceId, tt.blockHeight) + session, err := sessionKeeper.HydrateSession(ctx, sessionHydrator) + require.NoError(t, err) + + require.Equal(t, tt.expectedNumBlocksPerSession, session.NumBlocksPerSession) + require.Equal(t, tt.expectedSessionNumber, session.SessionNumber) + require.Equal(t, tt.expectedSessionStartBlock, session.Header.SessionStartBlockHeight) + }) + } +} + +func TestSession_HydrateSession_SessionId(t *testing.T) { + type test struct { + name string + + blockHeight1 int64 + blockHeight2 int64 + + appAddr1 string + appAddr2 string + + serviceId1 string + serviceId2 string + + expectedSessionId1 string + expectedSessionId2 string + } + + // TODO_TECHDEBT: Extend these tests once `NumBlocksPerSession` is configurable. + // Currently assumes NumBlocksPerSession=4 + tests := []test{ + { + name: "(app1, svc1): sessionId at first session block != sessionId at next session block", + + blockHeight1: 4, + blockHeight2: 8, + + appAddr1: keepertest.TestApp1Address, // app1 + appAddr2: keepertest.TestApp1Address, // app1 + + serviceId1: keepertest.TestServiceId1, // svc1 + serviceId2: keepertest.TestServiceId1, // svc1 + + expectedSessionId1: "aabaa25668538f80395170be95ce1d1536d9228353ced71cc3b763171316fe39", + expectedSessionId2: "23f037a10f9d51d020d27763c42dd391d7e71765016d95d0d61f36c4a122efd0", + }, + { + name: "app1: sessionId for svc1 != sessionId for svc2", + + blockHeight1: 4, + blockHeight2: 4, + + appAddr1: keepertest.TestApp1Address, // app1 + appAddr2: keepertest.TestApp1Address, // app1 + + serviceId1: keepertest.TestServiceId1, // svc1 + serviceId2: keepertest.TestServiceId2, // svc2 + + expectedSessionId1: "aabaa25668538f80395170be95ce1d1536d9228353ced71cc3b763171316fe39", + expectedSessionId2: "478d005769e5edf38d9bf2d8828a56d78b17348bb2c4796dd6d85b5d736a908a", + }, + { + name: "svc1: sessionId for app1 != sessionId for app2", + + blockHeight1: 4, + blockHeight2: 4, + + appAddr1: keepertest.TestApp1Address, // app1 + appAddr2: keepertest.TestApp2Address, // app2 + + serviceId1: keepertest.TestServiceId1, // svc1 + serviceId2: keepertest.TestServiceId1, // svc1 + + expectedSessionId1: "aabaa25668538f80395170be95ce1d1536d9228353ced71cc3b763171316fe39", + expectedSessionId2: "b4b0d8747b1cf67050a7bfefd7e93ebbad80c534fa14fb3c69339886f2ed7061", + }, + } + + sessionKeeper, ctx := keepertest.SessionKeeper(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sessionHydrator1 := keeper.NewSessionHydrator(tt.appAddr1, tt.serviceId1, tt.blockHeight1) + session1, err := sessionKeeper.HydrateSession(ctx, sessionHydrator1) + require.NoError(t, err) + + sessionHydrator2 := keeper.NewSessionHydrator(tt.appAddr2, tt.serviceId2, tt.blockHeight2) + session2, err := sessionKeeper.HydrateSession(ctx, sessionHydrator2) + require.NoError(t, err) + + require.NotEqual(t, session1.Header.SessionId, session2.Header.SessionId) + require.Equal(t, tt.expectedSessionId1, session1.Header.SessionId) + require.Equal(t, tt.expectedSessionId2, session2.Header.SessionId) + }) + } +} + +// TODO_TECHDEBT: Expand these tests to account for application joining/leaving the network at different heights as well changing the services they support +func TestSession_HydrateSession_Application(t *testing.T) { + type test struct { + name string + appAddr string + + expectedErr error + } + + tests := []test{ + { + name: "app is found", + appAddr: keepertest.TestApp1Address, + + expectedErr: nil, + }, + { + name: "app is not found", + appAddr: sample.AccAddress(), // Generating a random address on the fly + + expectedErr: types.ErrHydratingSession, + }, + { + name: "invalid app address", + appAddr: "invalid", + + expectedErr: types.ErrHydratingSession, + }, + // TODO_TECHDEBT: Add tests for when: + // - Application join/leaves (stakes/unstakes) altogether + // - Application adds/removes certain services mid-session + // - Application increases stakes mid-session + } + + serviceId := keepertest.TestServiceId1 + blockHeight := int64(10) + sessionKeeper, ctx := keepertest.SessionKeeper(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sessionHydrator := keeper.NewSessionHydrator(tt.appAddr, serviceId, blockHeight) + _, err := sessionKeeper.HydrateSession(ctx, sessionHydrator) + if tt.expectedErr != nil { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +// TODO_TECHDEBT: Expand these tests to account for supplier joining/leaving the network at different heights as well changing the services they support +func TestSession_HydrateSession_Suppliers(t *testing.T) { + type test struct { + name string + appAddr string + serviceId string + + numExpectedSuppliers int + expectedErr error + } + + // TODO_TECHDEBT: Extend these tests once `NumBlocksPerSession` is configurable. + // Currently assumes NumSupplierPerSession=15 + tests := []test{ + { + name: "num_suppliers_available = 0", + appAddr: keepertest.TestApp1Address, // app1 + serviceId: "svc_unknown", + + numExpectedSuppliers: 0, + expectedErr: types.ErrSuppliersNotFound, + }, + { + name: "num_suppliers_available < num_suppliers_per_session_param", + appAddr: keepertest.TestApp1Address, // app1 + serviceId: keepertest.TestServiceId1, // svc1 + + numExpectedSuppliers: 1, + expectedErr: nil, + }, + // TODO_TECHDEBT: Add this test once we make the num suppliers per session configurable + // { + // name: "num_suppliers_available > num_suppliers_per_session_param", + // }, + // TODO_TECHDEBT: Add tests for when: + // - Supplier join/leaves (stakes/unstakes) altogether + // - Supplier adds/removes certain services mid-session + // - Supplier increases stakes mid-session + } + + blockHeight := int64(10) + sessionKeeper, ctx := keepertest.SessionKeeper(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) {}) + + sessionHydrator := keeper.NewSessionHydrator(tt.appAddr, tt.serviceId, blockHeight) + session, err := sessionKeeper.HydrateSession(ctx, sessionHydrator) + + if tt.expectedErr != nil { + require.ErrorContains(t, err, tt.expectedErr.Error()) + continue + } + require.NoError(t, err) + require.Len(t, session.Suppliers, tt.numExpectedSuppliers) + } +} diff --git a/x/session/module.go b/x/session/module.go index 34016e04c..e6131b51f 100644 --- a/x/session/module.go +++ b/x/session/module.go @@ -4,18 +4,16 @@ import ( "context" "encoding/json" "fmt" - // this line is used by starport scaffolding # 1 - - "github.com/grpc-ecosystem/grpc-gateway/runtime" - "github.com/spf13/cobra" abci "github.com/cometbft/cometbft/abci/types" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + "pocket/x/session/client/cli" "pocket/x/session/keeper" "pocket/x/session/types" diff --git a/x/session/types/errors.go b/x/session/types/errors.go index 08a8609c2..3bf90eae7 100644 --- a/x/session/types/errors.go +++ b/x/session/types/errors.go @@ -8,5 +8,7 @@ import ( // x/session module sentinel errors var ( - ErrSample = sdkerrors.Register(ModuleName, 1100, "sample error") + ErrHydratingSession = sdkerrors.Register(ModuleName, 1, "error during session hydration") + ErrAppNotFound = sdkerrors.Register(ModuleName, 2, "application not found") + ErrSuppliersNotFound = sdkerrors.Register(ModuleName, 3, "suppliers not found") ) diff --git a/x/session/types/expected_keepers.go b/x/session/types/expected_keepers.go index 6aa6e9778..1bbae52a1 100644 --- a/x/session/types/expected_keepers.go +++ b/x/session/types/expected_keepers.go @@ -1,18 +1,29 @@ package types +//go:generate mockgen -destination ../../../testutil/session/mocks/expected_keepers_mock.go -package mocks . AccountKeeper,BankKeeper,ApplicationKeeper,SupplierKeeper + import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" + + apptypes "pocket/x/application/types" + sharedtypes "pocket/x/shared/types" ) // AccountKeeper defines the expected account keeper used for simulations (noalias) type AccountKeeper interface { GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI - // Methods imported from account should be defined here } // BankKeeper defines the expected interface needed to retrieve account balances. -type BankKeeper interface { - SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins - // Methods imported from bank should be defined here +type BankKeeper interface{} + +// ApplicationKeeper defines the expected application keeper to retrieve applications +type ApplicationKeeper interface { + GetApplication(ctx sdk.Context, address string) (app apptypes.Application, found bool) +} + +// SupplierKeeper defines the expected supplier keeper to retrieve suppliers +type SupplierKeeper interface { + GetAllSupplier(ctx sdk.Context) (suppliers []sharedtypes.Supplier) } diff --git a/x/supplier/keeper/supplier.go b/x/supplier/keeper/supplier.go index 4d7951766..f3ae6a310 100644 --- a/x/supplier/keeper/supplier.go +++ b/x/supplier/keeper/supplier.go @@ -63,3 +63,6 @@ func (k Keeper) GetAllSupplier(ctx sdk.Context) (list []sharedtypes.Supplier) { return } + +// TODO_OPTIMIZE: Index suppliers by serviceId so we can easily query `k.GetAllSupplier(ctx, ServiceId)` +// func (k Keeper) GetAllSupplier(ctx, sdkContext, serviceId string) (list []sharedtypes.Supplier) {} From 2c78154053ed2462f0afa9d39366a285af45e582 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Fri, 27 Oct 2023 14:41:14 +0200 Subject: [PATCH 03/28] [Miner] feat: add events query client (#64) * feat: add the map channel observable operator (cherry picked from commit 22371aa550eb0060b528f4573ba6908bbdfa0c1c) * feat: add replay observable (cherry picked from commit ab21790164ab544ae5f1508d3237a3faab33e71e) * chore: add query client interface * chore: add query client errors * test: fix false positive, prevent regression, & add comments * chore: add godoc comment * feat: add query client implementation * chore: add connection & dialer wrapper implementations * test: query client & add testquery helper pkg * chore: add go_test_integration make target * chore: add internal mocks pkg * test: query client integration test * docs: add event query client docs * chore: update go.mod * chore: re-order `eventsQueryClient` methods to improve readability * chore: add godoc comments to testclient helpers * fix: comment formatting * chore: improve comment & naming in evt query client test * test: tune events query client parameters * chore: improve godoc comments * chore: review improvements * refactor: `replayObservable` as its own interface type * refactor: `replayObservable#Next() V` to `ReplayObservable#Last(ctx, n) []V` * chore: add constructor func for `ReplayObservable` * test: reorder to improve readibility * refactor: rename and add godoc comments * chore: improve naming & comments * chore: add warning log and improve comments * test: improve and add tests * fix: interface assertion * fix: comment typo * chore: review improvements * fix: race * fix: race on eventsBytesAndConns map * fix: interface assertions Co-authored-by: Redouane Lakrache * fix: race * Small updates to the README * chore: review improvements (cherry picked from commit 31555cdc68211964358c43842e0581f565d1afff) * refactor: eliminate `EventsQueryClient#requestId` field (cherry picked from commit ccb1d6981f67ab860cb65dde4da15d89bcf57875) * refactor: eliminate `EventsQueryClient#requestId` field * refactor: move websocket dialer and connection to own pkg * chore: add comment * chore: move `EventsBytesObservable type above interfaces * chore: review improvements * fix: bug & improve naming & comments * chore: review improvements * chore: review improvements * chore: add comment Co-authored-by: Daniel Olshansky * revert: replay observable, merged into previous base branch --------- Co-authored-by: Redouane Lakrache Co-authored-by: Daniel Olshansky --- Makefile | 7 +- docs/pkg/client/events_query.md | 204 ++++++++++ go.mod | 4 +- internal/mocks/.gitkeep | 0 internal/testclient/common.go | 3 + internal/testclient/testeventsquery/client.go | 17 + .../testclient/testeventsquery/connection.go | 49 +++ pkg/client/events_query/client.go | 287 ++++++++++++++ .../events_query/client_integration_test.go | 60 +++ pkg/client/events_query/client_test.go | 374 ++++++++++++++++++ pkg/client/events_query/errors.go | 11 + pkg/client/events_query/options.go | 11 + .../events_query/websocket/connection.go | 35 ++ pkg/client/events_query/websocket/dialer.go | 35 ++ pkg/client/events_query/websocket/errors.go | 8 + pkg/client/interface.go | 63 +++ 16 files changed, 1165 insertions(+), 3 deletions(-) create mode 100644 docs/pkg/client/events_query.md create mode 100644 internal/mocks/.gitkeep create mode 100644 internal/testclient/common.go create mode 100644 internal/testclient/testeventsquery/client.go create mode 100644 internal/testclient/testeventsquery/connection.go create mode 100644 pkg/client/events_query/client.go create mode 100644 pkg/client/events_query/client_integration_test.go create mode 100644 pkg/client/events_query/client_test.go create mode 100644 pkg/client/events_query/errors.go create mode 100644 pkg/client/events_query/options.go create mode 100644 pkg/client/events_query/websocket/connection.go create mode 100644 pkg/client/events_query/websocket/dialer.go create mode 100644 pkg/client/events_query/websocket/errors.go create mode 100644 pkg/client/interface.go diff --git a/Makefile b/Makefile index 7663539a1..722b193b2 100644 --- a/Makefile +++ b/Makefile @@ -114,7 +114,11 @@ test_e2e: ## Run all E2E tests .PHONY: go_test go_test: go_version_check ## Run all go tests - go test -v ./... + go test -v -race -tags test ./... + +.PHONY: go_test_integration +go_test_integration: go_version_check ## Run all go tests, including integration + go test -v -race -tags test,integration ./... .PHONY: itest itest: go_version_check ## Run tests iteratively (see usage for more) @@ -132,6 +136,7 @@ go_mockgen: ## Use `mockgen` to generate mocks used for testing purposes of all go generate ./x/gateway/types/ go generate ./x/supplier/types/ go generate ./x/session/types/ + go generate ./pkg/... .PHONY: go_develop go_develop: proto_regen go_mockgen ## Generate protos and mocks diff --git a/docs/pkg/client/events_query.md b/docs/pkg/client/events_query.md new file mode 100644 index 000000000..787eecc8e --- /dev/null +++ b/docs/pkg/client/events_query.md @@ -0,0 +1,204 @@ +# Package `pocket/pkg/client/events_query` + +> An event query package for interfacing with [CometBFT](https://cometbft.com/) and the [Cosmos SDK](https://v1.cosmos.network/sdk), facilitating subscriptions to chain event messages. + +- [Overview](#overview) +- [Architecture Diagrams](#architecture-diagrams) +- [Installation](#installation) +- [Features](#features) +- [Usage](#usage) + - [Basic Example](#basic-example) + - [Advanced Usage](#advanced-usage) + - [Configuration](#configuration) +- [Best Practices](#best-practices) +- [FAQ](#faq) + - [Why use `events_query` over directly using Gorilla WebSockets?](#why-use-events_query-over-directly-using-gorilla-websockets) + - [How can I use a different connection mechanism other than WebSockets?](#how-can-i-use-a-different-connection-mechanism-other-than-websockets) + +## Overview + +The `events_query` package provides a client interface to subscribe to chain event messages. It abstracts the underlying connection mechanisms and offers a clear and easy-to-use way to get events from the chain. Highlights: + +- Offers subscription to chain event messages matching a given query. +- Uses the Gorilla WebSockets package for underlying connection operations. +- Provides a modular structure with interfaces allowing for mock implementations and testing. +- Offers considerations for potential improvements and replacements, such as integration with the cometbft RPC client. + +## Architecture Diagrams + +### Components +```mermaid +--- +title: Component Diagram Legend +--- + +flowchart + + a[Component A] + b[Component B] + c[Component C] + d[Component D] + + a --"A uses B via B#MethodName()"--> b +a =="A returns C from A#MethodName()"==> c +b -."A uses D via network IO".-> d +``` +```mermaid +--- +title: EventsQueryClient Components +--- + +flowchart + + subgraph comet[Cometbft Node] + subgraph rpc[JSON-RPC] + sub[subscribe endpoint] + end + end + + subgraph eqc[EventsQueryClient] + q1_eb[EventsBytesObservable] + q1_conn[Connection] + q1_dial[Dialer] + end + + q1_obsvr1[Observer 1] + q1_obsvr2[Observer 2] + + + q1_obsvr1 --"#Subscribe()"--> q1_eb +q1_obsvr2 --"#Subscribe()"--> q1_eb + + +q1_dial =="#DialContext()"==> q1_conn +q1_eb --"#Receive()"--> q1_conn + +q1_conn -.-> sub + +``` + +### Subscriptions +```mermaid +--- +title: Event Subscription Data Flow +--- + +flowchart + +subgraph comet[Cometbft Node] + subgraph rpc[JSON-RPC] + sub[subscribe endpoint] + end +end + +subgraph eqc[EventsQueryClient] + subgraph q1[Query 1] + q1_eb[EventsBytesObservable] + q1_conn[Connection] + end + subgraph q2[Query 2] + q2_conn[Connection] + q2_eb[EventsBytesObservable] + end +end + +q1_obsvr1[Query 1 Observer 1] +q1_obsvr2[Query 1 Observer 2] +q2_obsvr[Query 2 Observer] + +q1_eb -.-> q1_obsvr1 +q1_eb -.-> q1_obsvr2 +q2_eb -.-> q2_obsvr + + +q1_conn -.-> q1_eb +q2_conn -.-> q2_eb + +sub -.-> q1_conn +sub -.-> q2_conn + +``` + +## Installation + +```bash +go get github.com/pokt-network/poktroll/pkg/client/events_query +``` + +## Features + +- **Websocket Connection**: Uses the [Gorilla WebSockets](https://github.com/gorilla/websocket) for implementing the connection interface. +- **Events Subscription**: Subscribe to chain event messages using a simple query mechanism. +- **Dialer Interface**: Offers a `Dialer` interface for constructing connections, which can be easily mocked for tests. +- **Observable Pattern**: Integrates the observable pattern, making it easier to react to chain events. + +## Usage + +### Basic Example + +```go +ctx := context.Background() + +// Creating a new EventsQueryClient with the default, websocket dialer: +cometWebsocketURL := "ws://example.com" +evtClient := eventsquery.NewEventsQueryClient(cometWebsocketURL) + +// Subscribing to a specific event, e.g. newly committed blocks: +// (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) +observable := evtClient.EventsBytes(ctx, "tm.event='NewBlock'") + +// Subscribe and receive from the observer channel, typically in some other scope. +observer := observable.Subscribe(ctx) + +// Observer channel closes when the context is cancelled, observer is +// unsubscribed, or after the subscription returns an error. +for eitherEvent := range observer.Ch() { + // (see either.Either: https://github.com/pokt-network/poktroll/blob/main/pkg/either/either.go#L3) + eventBz, err := eitherEvent.ValueOrError() + + // ... +} +``` + +### Advanced Usage + +```go +// Given some custom dialer & connection implementation, e.g.: +var ( + tcpDialer eventsquery.Dialer = exampletcp.NewTcpDialerImpl() + grcpDialer eventsquery.Dialer = examplegrpc.NewGrpcDialerImpl() +) + +// Both TCP and gRPC use the TCP scheme as gRPC uses TCP for its transport layer. +cometUrl = "tcp://example.com" + +// Creating new EventsQueryClients with a custom tcpDialer: +tcpDialerOpt := eventsquery.WithDialer(tcpDialer) +tcpEvtClient := eventsquery.NewEventsQueryClient(cometUrl, tcpDialerOpt) + +// Alternatively, with a custom gRPC dialer: +gcpDialerOpt := eventsquery.WithDialer(grcpDialer) +grpcEvtClient := eventsquery.NewEventsQueryClient(cometUrl, grpcDialerOpt) + +// ... rest follows the same as the basic example. +``` + +### Configuration + +- **WithDialer**: Configure the client to use a custom dialer for connections. + +## Best Practices + +- **Connection Handling**: Ensure to close the `EventsQueryClient` when done to free up resources and avoid potential leaks. +- **Error Handling**: Always check both the synchronous error returned by `EventsBytes` as well as asynchronous errors send over the observable. + +## FAQ + +#### Why use `events_query` over directly using Gorilla WebSockets? + +`events_query` abstracts many of the underlying details and provides a streamlined interface for subscribing to chain events. +It also integrates the observable pattern and provides mockable interfaces for better testing. + +#### How can I use a different connection mechanism other than WebSockets? + +You can implement the `Dialer` and `Connection` interfaces and use the `WithDialer` configuration to provide your custom dialer. diff --git a/go.mod b/go.mod index 8931ee507..d2fd3985c 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.3 github.com/gorilla/mux v1.8.0 + github.com/gorilla/websocket v1.5.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 github.com/regen-network/gocuke v0.6.2 @@ -22,6 +23,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 + go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.12.0 golang.org/x/sync v0.3.0 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 @@ -123,7 +125,6 @@ require ( github.com/googleapis/gax-go/v2 v2.8.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/rpc v1.2.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect @@ -252,7 +253,6 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/dig v1.16.1 // indirect go.uber.org/fx v1.19.2 // indirect - go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/mod v0.11.0 // indirect diff --git a/internal/mocks/.gitkeep b/internal/mocks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/internal/testclient/common.go b/internal/testclient/common.go new file mode 100644 index 000000000..41248916e --- /dev/null +++ b/internal/testclient/common.go @@ -0,0 +1,3 @@ +package testclient + +const CometLocalWebsocketURL = "ws://localhost:36657/websocket" diff --git a/internal/testclient/testeventsquery/client.go b/internal/testclient/testeventsquery/client.go new file mode 100644 index 000000000..2a715aa4b --- /dev/null +++ b/internal/testclient/testeventsquery/client.go @@ -0,0 +1,17 @@ +package testeventsquery + +import ( + "testing" + + "pocket/internal/testclient" + "pocket/pkg/client" + eventsquery "pocket/pkg/client/events_query" +) + +// NewLocalnetClient returns a new events query client which is configured to +// connect to the localnet sequencer. +func NewLocalnetClient(t *testing.T, opts ...client.EventsQueryClientOption) client.EventsQueryClient { + t.Helper() + + return eventsquery.NewEventsQueryClient(testclient.CometLocalWebsocketURL, opts...) +} diff --git a/internal/testclient/testeventsquery/connection.go b/internal/testclient/testeventsquery/connection.go new file mode 100644 index 000000000..9351c05d6 --- /dev/null +++ b/internal/testclient/testeventsquery/connection.go @@ -0,0 +1,49 @@ +package testeventsquery + +import ( + "pocket/pkg/either" + "testing" + + "github.com/golang/mock/gomock" + + "pocket/internal/mocks/mockclient" +) + +// NewOneTimeMockConnAndDialer returns a new mock connection and mock dialer that +// will return the mock connection when DialContext is called. The mock dialer +// will expect DialContext to be called exactly once. The connection mock will +// expect Close to be called exactly once. +// Callers must mock the Receive method with an EXPECT call before the connection +// mock can be used. +func NewOneTimeMockConnAndDialer(t *testing.T) ( + *mockclient.MockConnection, + *mockclient.MockDialer, +) { + ctrl := gomock.NewController(t) + connMock := mockclient.NewMockConnection(ctrl) + connMock.EXPECT().Close(). + Return(nil). + Times(1) + + dialerMock := NewOneTimeMockDialer(t, either.Success(connMock)) + + return connMock, dialerMock +} + +// NewOneTimeMockDialer returns a mock dialer that will return either the given +// connection mock or error when DialContext is called. The mock dialer will +// expect DialContext to be called exactly once. +func NewOneTimeMockDialer( + t *testing.T, + eitherConnMock either.Either[*mockclient.MockConnection], +) *mockclient.MockDialer { + ctrl := gomock.NewController(t) + dialerMock := mockclient.NewMockDialer(ctrl) + + connMock, err := eitherConnMock.ValueOrError() + dialerMock.EXPECT().DialContext(gomock.Any(), gomock.Any()). + Return(connMock, err). + Times(1) + + return dialerMock +} diff --git a/pkg/client/events_query/client.go b/pkg/client/events_query/client.go new file mode 100644 index 000000000..23fb4b208 --- /dev/null +++ b/pkg/client/events_query/client.go @@ -0,0 +1,287 @@ +package eventsquery + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "sync" + + "go.uber.org/multierr" + + "pocket/pkg/client" + "pocket/pkg/client/events_query/websocket" + "pocket/pkg/either" + "pocket/pkg/observable" + "pocket/pkg/observable/channel" +) + +var _ client.EventsQueryClient = (*eventsQueryClient)(nil) + +// TODO_TECHDEBT: the cosmos-sdk CLI code seems to use a cometbft RPC client +// which includes a `#Subscribe()` method for a similar purpose. Perhaps we could +// replace this custom websocket client with that. +// See: +// - https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110 +// - https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L656 +// - https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114 +// - https://github.com/pokt-network/poktroll/pull/64#discussion_r1372378241 + +// eventsQueryClient implements the EventsQueryClient interface. +type eventsQueryClient struct { + // cometWebsocketURL is the websocket URL for the cometbft node. It is assigned + // in NewEventsQueryClient. + cometWebsocketURL string + // dialer is responsible for creating the connection instance which + // facilitates communication with the cometbft node via message passing. + dialer client.Dialer + // eventsBytesAndConnsMu protects the eventsBytesAndConns map. + eventsBytesAndConnsMu sync.RWMutex + // eventsBytesAndConns maps event subscription queries to their respective + // eventsBytes observable, connection, and isClosed status. + eventsBytesAndConns map[string]*eventsBytesAndConn +} + +// eventsBytesAndConn is a struct which holds an eventsBytes observable & the +// corresponding connection which produces its inputs. +type eventsBytesAndConn struct { + // eventsBytes is an observable which is notified about chain event messages + // matching the given query. It receives an either.Either[[]byte] which is + // either an error or the event message bytes. + eventsBytes observable.Observable[either.Either[[]byte]] + conn client.Connection + isClosed bool +} + +// Close unsubscribes all observers of eventsBytesAndConn's observable and also +// closes its connection. +func (ebc *eventsBytesAndConn) Close() { + ebc.eventsBytes.UnsubscribeAll() + _ = ebc.conn.Close() +} + +func NewEventsQueryClient(cometWebsocketURL string, opts ...client.EventsQueryClientOption) client.EventsQueryClient { + evtClient := &eventsQueryClient{ + cometWebsocketURL: cometWebsocketURL, + eventsBytesAndConns: make(map[string]*eventsBytesAndConn), + } + + for _, opt := range opts { + opt(evtClient) + } + + if evtClient.dialer == nil { + // default to using the websocket dialer + evtClient.dialer = websocket.NewWebsocketDialer() + } + + return evtClient +} + +// EventsBytes returns an eventsBytes observable which is notified about chain +// event messages matching the given query. It receives an either.Either[[]byte] +// which is either an error or the event message bytes. +// (see: https://pkg.go.dev/github.com/cometbft/cometbft/types#pkg-constants) +// (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) +func (eqc *eventsQueryClient) EventsBytes( + ctx context.Context, + query string, +) (client.EventsBytesObservable, error) { + // Must (write) lock eventsBytesAndConnsMu so that we can safely check for + // existing subscriptions to the given query or add a new eventsBytes to the + // observableConns map. + // The lock must be held for both checking and adding to prevent concurrent + // calls to this function from racing. + eqc.eventsBytesAndConnsMu.Lock() + // Deferred (write) unlock. + defer eqc.eventsBytesAndConnsMu.Unlock() + + // Check if an event subscription already exists for the given query. + if eventsBzConn := eqc.eventsBytesAndConns[query]; eventsBzConn != nil { + // If found it is returned. + return eventsBzConn.eventsBytes, nil + } + + // Otherwise, create a new event subscription for the given query. + eventsBzConn, err := eqc.newEventsBytesAndConn(ctx, query) + if err != nil { + return nil, err + } + + // Insert the new eventsBytes into the eventsBytesAndConns map. + eqc.eventsBytesAndConns[query] = eventsBzConn + + // Unsubscribe from the eventsBytes when the context is done. + go eqc.goUnsubscribeOnDone(ctx, query) + + // Return the new eventsBytes observable for the given query. + return eventsBzConn.eventsBytes, nil +} + +// Close unsubscribes all observers from all event subscription observables. +func (eqc *eventsQueryClient) Close() { + eqc.close() +} + +// close unsubscribes all observers from all event subscription observables. +func (eqc *eventsQueryClient) close() { + eqc.eventsBytesAndConnsMu.Lock() + defer eqc.eventsBytesAndConnsMu.Unlock() + + for query, eventsBzConn := range eqc.eventsBytesAndConns { + // Unsubscribe all observers of the eventsBzConn observable and close the + // connection for the given query. + eventsBzConn.Close() + // remove isClosed eventsBytesAndConns + delete(eqc.eventsBytesAndConns, query) + } +} + +// newEventwsBzAndConn creates a new eventsBytes and connection for the given query. +func (eqc *eventsQueryClient) newEventsBytesAndConn( + ctx context.Context, + query string, +) (*eventsBytesAndConn, error) { + // Get a connection for the query. + conn, err := eqc.openEventsBytesAndConn(ctx, query) + if err != nil { + return nil, err + } + + // Construct an eventsBytes for the given query. + eventsBzObservable, eventsBzPublishCh := channel.NewObservable[either.Either[[]byte]]() + + // Publish either events bytes or an error received from the connection to + // the eventsBz observable. + // NB: intentionally not retrying on error, leaving that to the caller. + // (see: https://github.com/pokt-network/poktroll/pull/64#discussion_r1373826542) + go eqc.goPublishEventsBz(ctx, conn, eventsBzPublishCh) + + return &eventsBytesAndConn{ + eventsBytes: eventsBzObservable, + conn: conn, + }, nil +} + +// openEventsBytesAndConn gets a connection using the configured dialer and sends +// an event subscription request on it, returning the connection. +func (eqc *eventsQueryClient) openEventsBytesAndConn( + ctx context.Context, + query string, +) (client.Connection, error) { + // Get a request for subscribing to events matching the given query. + req, err := eqc.eventSubscriptionRequest(query) + if err != nil { + return nil, err + } + + // Get a connection from the dialer. + conn, err := eqc.dialer.DialContext(ctx, eqc.cometWebsocketURL) + if err != nil { + return nil, ErrDial.Wrapf("%s", err) + } + + // Send the event subscription request on the connection. + if err := conn.Send(req); err != nil { + subscribeErr := ErrSubscribe.Wrapf("%s", err) + // assume the connection is bad + closeErr := conn.Close() + return nil, multierr.Combine(subscribeErr, closeErr) + } + return conn, nil +} + +// goPublishEventsBz blocks on reading messages from a websocket connection. +// It is intended to be called from within a go routine. +func (eqc *eventsQueryClient) goPublishEventsBz( + ctx context.Context, + conn client.Connection, + eventsBzPublishCh chan<- either.Either[[]byte], +) { + // Read and handle messages from the websocket. This loop will exit when the + // websocket connection is isClosed and/or returns an error. + for { + event, err := conn.Receive() + if err != nil { + // TODO_CONSIDERATION: should we close the publish channel here too? + + // Stop this goroutine if there's an error. + // + // See gorilla websocket `Conn#NextReader()` docs: + // | Applications must break out of the application's read loop when this method + // | returns a non-nil error value. Errors returned from this method are + // | permanent. Once this method returns a non-nil error, all subsequent calls to + // | this method return the same error. + + // Only propagate error if it's not a context cancellation error. + if !errors.Is(ctx.Err(), context.Canceled) { + // Populate the error side (left) of the either and publish it. + eventsBzPublishCh <- either.Error[[]byte](err) + } + + eqc.close() + return + } + + // Populate the []byte side (right) of the either and publish it. + eventsBzPublishCh <- either.Success(event) + } +} + +// goUnsubscribeOnDone unsubscribes from the subscription when the context is done. +// It is intended to be called in a goroutine. +func (eqc *eventsQueryClient) goUnsubscribeOnDone( + ctx context.Context, + query string, +) { + // Wait for the context to be done. + <-ctx.Done() + // Only close the eventsBytes for the given query. + eqc.eventsBytesAndConnsMu.RLock() + defer eqc.eventsBytesAndConnsMu.RUnlock() + + if eventsBzConn, ok := eqc.eventsBytesAndConns[query]; ok { + // Unsubscribe all observers of the given query's eventsBzConn's observable + // and close its connection. + eventsBzConn.Close() + // Remove the eventsBytesAndConn for the given query. + delete(eqc.eventsBytesAndConns, query) + } +} + +// eventSubscriptionRequest returns a JSON-RPC request for subscribing to events +// matching the given query. The request is serialized as JSON to a byte slice. +// (see: https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110) +// (see: https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114) +func (eqc *eventsQueryClient) eventSubscriptionRequest(query string) ([]byte, error) { + requestJson := map[string]any{ + "jsonrpc": "2.0", + "method": "subscribe", + "id": randRequestId(), + "params": map[string]interface{}{ + "query": query, + }, + } + requestBz, err := json.Marshal(requestJson) + if err != nil { + return nil, err + } + return requestBz, nil +} + +// randRequestId returns a random 8 byte, base64 request ID which is intended +// for in JSON-RPC requests to uniquely identify distinct RPC requests. +// These request IDs only need to be unique to the extent that they are useful +// to this client for identifying distinct RPC requests. Their size and keyspace +// are arbitrary. +func randRequestId() string { + requestIdBz := make([]byte, 8) // 8 bytes = 64 bits = uint64 + if _, err := rand.Read(requestIdBz); err != nil { + panic(fmt.Sprintf( + "failed to generate random request ID: %s", err, + )) + } + return base64.StdEncoding.EncodeToString(requestIdBz) +} diff --git a/pkg/client/events_query/client_integration_test.go b/pkg/client/events_query/client_integration_test.go new file mode 100644 index 000000000..1287775a5 --- /dev/null +++ b/pkg/client/events_query/client_integration_test.go @@ -0,0 +1,60 @@ +//go:build integration + +package eventsquery_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "pocket/internal/testclient/testeventsquery" +) + +// The query use to subscribe for new block events on the websocket endpoint exposed by CometBFT nodes +const committedBlockEventsQuery = "tm.event='NewBlock'" + +func TestQueryClient_EventsObservable_Integration(t *testing.T) { + const ( + eventReceiveTimeout = 5 * time.Second + observedEventsLimit = 3 + ) + ctx := context.Background() + + queryClient := testeventsquery.NewLocalnetClient(t) + require.NotNil(t, queryClient) + + // Start a subscription to the committed block events query. This begins + // publishing events to the returned observable. + eventsObservable, err := queryClient.EventsBytes(ctx, committedBlockEventsQuery) + require.NoError(t, err) + + eventsObserver := eventsObservable.Subscribe(ctx) + + var ( + eventCounter int + done = make(chan struct{}, 1) + ) + go func() { + for range eventsObserver.Ch() { + eventCounter++ + + if eventCounter >= observedEventsLimit { + done <- struct{}{} + return + } + } + }() + + select { + case <-done: + require.NoError(t, err) + require.Equal(t, observedEventsLimit, eventCounter) + case <-time.After(eventReceiveTimeout): + t.Fatalf( + "timed out waiting for block subscription; expected %d blocks, got %d", + observedEventsLimit, eventCounter, + ) + } +} diff --git a/pkg/client/events_query/client_test.go b/pkg/client/events_query/client_test.go new file mode 100644 index 000000000..393e68813 --- /dev/null +++ b/pkg/client/events_query/client_test.go @@ -0,0 +1,374 @@ +package eventsquery_test + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "pocket/internal/mocks/mockclient" + "pocket/internal/testchannel" + "pocket/internal/testclient/testeventsquery" + "pocket/internal/testerrors" + eventsquery "pocket/pkg/client/events_query" + "pocket/pkg/client/events_query/websocket" + "pocket/pkg/either" + "pocket/pkg/observable" +) + +func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { + var ( + readObserverEventsTimeout = time.Second + queryCounter int + queryLimit = 5 + connMocks = make([]*mockclient.MockConnection, queryLimit) + ctrl = gomock.NewController(t) + rootCtx, cancelRoot = context.WithCancel(context.Background()) + ) + t.Cleanup(cancelRoot) + + dialerMock := mockclient.NewMockDialer(ctrl) + // `Dialer#DialContext()` should be called once for each subscription (subtest). + dialerMock.EXPECT().DialContext(gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, _ string) (*mockclient.MockConnection, error) { + // Return the connection mock for the subscription with the given query. + // It should've been created in the respective test function. + connMock := connMocks[queryCounter] + queryCounter++ + return connMock, nil + }). + Times(queryLimit) + + // Set up events query client. + dialerOpt := eventsquery.WithDialer(dialerMock) + queryClient := eventsquery.NewEventsQueryClient("", dialerOpt) + t.Cleanup(queryClient.Close) + + for queryIdx := 0; queryIdx < queryLimit; queryIdx++ { + t.Run(testQuery(queryIdx), func(t *testing.T) { + var ( + // ReadEventCounter is the number of eventsBytesAndConns which have been + // received from the connection since the subtest started. + readEventCounter int + // HandleEventsLimit is the total number of eventsBytesAndConns to send and + // receive through the query client's eventsBytes for this subtest. + handleEventsLimit = 250 + connClosed atomic.Bool + queryCtx, cancelQuery = context.WithCancel(rootCtx) + ) + + // Must set up connection mock before calling EventsBytes() + connMock := mockclient.NewMockConnection(ctrl) + // `Connection#Close()` should be called once for each subscription. + connMock.EXPECT().Close(). + DoAndReturn(func() error { + // Simulate closing the connection. + connClosed.CompareAndSwap(false, true) + return nil + }). + Times(1) + // `Connection#Send()` should be called once for each subscription. + connMock.EXPECT().Send(gomock.Any()). + Return(nil). + Times(1) + // `Connection#Receive()` should be called once for each message plus + // one as it blocks in the loop which calls msgHandler after reading the + // last message. + connMock.EXPECT().Receive(). + DoAndReturn(func() (any, error) { + // Simulate ErrConnClosed if connection is isClosed. + if connClosed.Load() { + return nil, eventsquery.ErrConnClosed + } + + event := testEvent(int32(readEventCounter)) + readEventCounter++ + + // Simulate IO delay between sequential events. + time.Sleep(10 * time.Microsecond) + + return event, nil + }). + MinTimes(handleEventsLimit) + connMocks[queryIdx] = connMock + + // Set up events bytes observer for this query. + eventObservable, err := queryClient.EventsBytes(queryCtx, testQuery(queryIdx)) + require.NoError(t, err) + + eventObserver := eventObservable.Subscribe(queryCtx) + + onLimit := func() { + // Cancelling the context should close the connection. + cancelQuery() + // Closing the connection happens asynchronously, so we need to wait a bit + // for the connection to close to satisfy the connection mock expectations. + time.Sleep(10 * time.Millisecond) + + // Drain the observer channel and assert that it's isClosed. + err := testchannel.DrainChannel(eventObserver.Ch()) + require.NoError(t, err, "eventsBytesAndConns observer channel should be isClosed") + } + + // Concurrently consume eventsBytesAndConns from the observer channel. + behavesLikeEitherObserver( + t, eventObserver, + handleEventsLimit, + eventsquery.ErrConnClosed, + readObserverEventsTimeout, + onLimit, + ) + }) + } +} + +func TestEventsQueryClient_Subscribe_Close(t *testing.T) { + var ( + readAllEventsTimeout = 50 * time.Millisecond + handleEventsLimit = 10 + readEventCounter int + connClosed atomic.Bool + ctx = context.Background() + ) + + connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) + connMock.EXPECT().Send(gomock.Any()).Return(nil). + Times(1) + connMock.EXPECT().Receive(). + DoAndReturn(func() (any, error) { + if connClosed.Load() { + return nil, eventsquery.ErrConnClosed + } + + event := testEvent(int32(readEventCounter)) + readEventCounter++ + + // Simulate IO delay between sequential events. + time.Sleep(10 * time.Microsecond) + + return event, nil + }). + MinTimes(handleEventsLimit) + + dialerOpt := eventsquery.WithDialer(dialerMock) + queryClient := eventsquery.NewEventsQueryClient("", dialerOpt) + + // set up query observer + eventsObservable, err := queryClient.EventsBytes(ctx, testQuery(0)) + require.NoError(t, err) + + eventsObserver := eventsObservable.Subscribe(ctx) + + onLimit := func() { + // cancelling the context should close the connection + queryClient.Close() + // closing the connection happens asynchronously, so we need to wait a bit + // for the connection to close to satisfy the connection mock expectations. + time.Sleep(10 * time.Millisecond) + } + + // concurrently consume eventsBytesAndConns from the observer channel + behavesLikeEitherObserver( + t, eventsObserver, + handleEventsLimit, + eventsquery.ErrConnClosed, + readAllEventsTimeout, + onLimit, + ) +} + +func TestEventsQueryClient_Subscribe_DialError(t *testing.T) { + ctx := context.Background() + + eitherErrDial := either.Error[*mockclient.MockConnection](eventsquery.ErrDial) + dialerMock := testeventsquery.NewOneTimeMockDialer(t, eitherErrDial) + + dialerOpt := eventsquery.WithDialer(dialerMock) + queryClient := eventsquery.NewEventsQueryClient("", dialerOpt) + eventsObservable, err := queryClient.EventsBytes(ctx, testQuery(0)) + require.Nil(t, eventsObservable) + require.True(t, errors.Is(err, eventsquery.ErrDial)) +} + +func TestEventsQueryClient_Subscribe_RequestError(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) + connMock.EXPECT().Send(gomock.Any()). + Return(fmt.Errorf("mock send error")). + Times(1) + + dialerOpt := eventsquery.WithDialer(dialerMock) + queryClient := eventsquery.NewEventsQueryClient("url_ignored", dialerOpt) + eventsObservable, err := queryClient.EventsBytes(ctx, testQuery(0)) + require.Nil(t, eventsObservable) + require.True(t, errors.Is(err, eventsquery.ErrSubscribe)) + + // cancelling the context should close the connection + cancel() + // closing the connection happens asynchronously, so we need to wait a bit + // for the connection to close to satisfy the connection mock expectations. + time.Sleep(10 * time.Millisecond) +} + +// TODO_INVESTIGATE: why this test fails? +func TestEventsQueryClient_Subscribe_ReceiveError(t *testing.T) { + t.Skip("TODO_INVESTIGATE: why this test fails") + + var ( + handleEventLimit = 10 + readAllEventsTimeout = 100 * time.Millisecond + readEventCounter int + ) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) + connMock.EXPECT().Send(gomock.Any()).Return(nil). + Times(1) + connMock.EXPECT().Receive(). + DoAndReturn(func() (any, error) { + if readEventCounter >= handleEventLimit { + return nil, websocket.ErrReceive + } + + event := testEvent(int32(readEventCounter)) + readEventCounter++ + time.Sleep(10 * time.Microsecond) + + return event, nil + }). + MinTimes(handleEventLimit) + + dialerOpt := eventsquery.WithDialer(dialerMock) + queryClient := eventsquery.NewEventsQueryClient("", dialerOpt) + + // set up query observer + eventsObservable, err := queryClient.EventsBytes(ctx, testQuery(0)) + require.NoError(t, err) + + eventsObserver := eventsObservable.Subscribe(ctx) + // concurrently consume eventsBytesAndConns from the observer channel + behavesLikeEitherObserver( + t, eventsObserver, + handleEventLimit, + websocket.ErrReceive, + readAllEventsTimeout, + nil, + ) +} + +// TODO_TECHDEBT: add test coverage for multiple observers with distinct and overlapping queries +func TestEventsQueryClient_EventsBytes_MultipleObservers(t *testing.T) { + t.Skip("TODO_TECHDEBT: add test coverage for multiple observers with distinct and overlapping queries") +} + +// behavesLikeEitherObserver asserts that the given observer behaves like an +// observable.Observer[either.Either[V]] by consuming notifications from the +// observer channel and asserting that they match the expected notification. +// It also asserts that the observer channel is isClosed after the expected number +// of eventsBytes have been received. +// If onLimit is not nil, it is called when the expected number of events have +// been received. +// Otherwise, the observer channel is drained and the test fails if it is not +// isClosed after the timeout duration. +func behavesLikeEitherObserver[V any]( + t *testing.T, + observer observable.Observer[either.Either[V]], + notificationsLimit int, + expectedErr error, + timeout time.Duration, + onLimit func(), +) { + var ( + // eventsCounter is the number of events which have been received from the + // eventsBytes since this function was called. + eventsCounter int32 + // errCh is used to signal when the test completes and/or produces an error + errCh = make(chan error, 1) + ) + + go func() { + for eitherEvent := range observer.Ch() { + event, err := eitherEvent.ValueOrError() + if err != nil { + switch expectedErr { + case nil: + if !assert.NoError(t, err) { + errCh <- testerrors.ErrAsync + return + } + default: + if !assert.ErrorIs(t, err, expectedErr) { + errCh <- testerrors.ErrAsync + return + } + } + } + + currentEventCount := atomic.LoadInt32(&eventsCounter) + if int(currentEventCount) >= notificationsLimit { + // signal completion + errCh <- nil + return + } + + // TODO_IMPROVE: to make this test helper more generic, it should accept + // a generic function which generates the expected event for the given + // index. Perhaps this function could use an either type which could be + // used to consolidate the expectedErr and expectedEvent arguments. + expectedEvent := testEvent(currentEventCount) + // Require calls t.Fatal internally, which shouldn't happen in a + // goroutine other than the test function's. + // Use assert instead and stop the test by sending on errCh and + // returning. + if !assert.Equal(t, expectedEvent, event) { + errCh <- testerrors.ErrAsync + return + } + + atomic.AddInt32(&eventsCounter, 1) + + // unbounded consumption here can result in the condition below never + // being met due to the connection being isClosed before the "last" event + // is received + time.Sleep(10 * time.Microsecond) + } + }() + + select { + case err := <-errCh: + require.NoError(t, err) + require.Equal(t, notificationsLimit, int(atomic.LoadInt32(&eventsCounter))) + + // TODO_THIS_COMMIT: is this necessary? + time.Sleep(10 * time.Millisecond) + + if onLimit != nil { + onLimit() + } + case <-time.After(timeout): + t.Fatalf( + "timed out waiting for next event; expected %d events, got %d", + notificationsLimit, atomic.LoadInt32(&eventsCounter), + ) + } + + err := testchannel.DrainChannel(observer.Ch()) + require.NoError(t, err, "eventsBytesAndConns observer should be isClosed") +} + +func testEvent(idx int32) []byte { + return []byte(fmt.Sprintf("message_%d", idx)) +} + +func testQuery(idx int) string { + return fmt.Sprintf("query_%d", idx) +} diff --git a/pkg/client/events_query/errors.go b/pkg/client/events_query/errors.go new file mode 100644 index 000000000..48d60f0a7 --- /dev/null +++ b/pkg/client/events_query/errors.go @@ -0,0 +1,11 @@ +package eventsquery + +import errorsmod "cosmossdk.io/errors" + +var ( + ErrDial = errorsmod.Register(codespace, 1, "dialing for connection failed") + ErrConnClosed = errorsmod.Register(codespace, 2, "connection closed") + ErrSubscribe = errorsmod.Register(codespace, 3, "failed to subscribe to events") + + codespace = "events_query_client" +) diff --git a/pkg/client/events_query/options.go b/pkg/client/events_query/options.go new file mode 100644 index 000000000..affa437f3 --- /dev/null +++ b/pkg/client/events_query/options.go @@ -0,0 +1,11 @@ +package eventsquery + +import "pocket/pkg/client" + +// WithDialer returns a client.EventsQueryClientOption which sets the given dialer on the +// resulting eventsQueryClient when passed to NewEventsQueryClient(). +func WithDialer(dialer client.Dialer) client.EventsQueryClientOption { + return func(evtClient client.EventsQueryClient) { + evtClient.(*eventsQueryClient).dialer = dialer + } +} diff --git a/pkg/client/events_query/websocket/connection.go b/pkg/client/events_query/websocket/connection.go new file mode 100644 index 000000000..af82920df --- /dev/null +++ b/pkg/client/events_query/websocket/connection.go @@ -0,0 +1,35 @@ +package websocket + +import ( + gorillaws "github.com/gorilla/websocket" + + "pocket/pkg/client" +) + +var _ client.Connection = (*websocketConn)(nil) + +// websocketConn implements the Connection interface using the gorilla websocket +// transport implementation. +type websocketConn struct { + conn *gorillaws.Conn +} + +// Receive implements the respective interface method using the underlying websocket. +func (wsConn *websocketConn) Receive() ([]byte, error) { + _, msg, err := wsConn.conn.ReadMessage() + if err != nil { + return nil, ErrReceive.Wrapf("%s", err) + } + return msg, nil +} + +// Send implements the respective interface method using the underlying websocket. +func (wsConn *websocketConn) Send(msg []byte) error { + // Using the TextMessage message to indicate that msg is UTF-8 encoded. + return wsConn.conn.WriteMessage(gorillaws.TextMessage, msg) +} + +// Close implements the respective interface method using the underlying websocket. +func (wsConn *websocketConn) Close() error { + return wsConn.conn.Close() +} diff --git a/pkg/client/events_query/websocket/dialer.go b/pkg/client/events_query/websocket/dialer.go new file mode 100644 index 000000000..dc5e9a606 --- /dev/null +++ b/pkg/client/events_query/websocket/dialer.go @@ -0,0 +1,35 @@ +package websocket + +import ( + "context" + + "github.com/gorilla/websocket" + + "pocket/pkg/client" +) + +var _ client.Dialer = (*websocketDialer)(nil) + +// websocketDialer implements the Dialer interface using the gorilla websocket +// transport implementation. +type websocketDialer struct{} + +// NewWebsocketDialer creates a new websocketDialer. +func NewWebsocketDialer() client.Dialer { + return &websocketDialer{} +} + +// DialContext implements the respective interface method using the default gorilla +// websocket dialer. +func (wsDialer *websocketDialer) DialContext( + ctx context.Context, + urlString string, +) (client.Connection, error) { + // TODO_IMPROVE: check http response status and potential err + // TODO_TECHDEBT: add test coverage and ensure support for a 3xx responses + conn, _, err := websocket.DefaultDialer.DialContext(ctx, urlString, nil) + if err != nil { + return nil, err + } + return &websocketConn{conn: conn}, nil +} diff --git a/pkg/client/events_query/websocket/errors.go b/pkg/client/events_query/websocket/errors.go new file mode 100644 index 000000000..3c70d1eec --- /dev/null +++ b/pkg/client/events_query/websocket/errors.go @@ -0,0 +1,8 @@ +package websocket + +import errorsmod "cosmossdk.io/errors" + +var ( + ErrReceive = errorsmod.Register(codespace, 4, "failed to receive event") + codespace = "events_query_client_websocket_connection" +) diff --git a/pkg/client/interface.go b/pkg/client/interface.go new file mode 100644 index 000000000..731ab12b7 --- /dev/null +++ b/pkg/client/interface.go @@ -0,0 +1,63 @@ +//go:generate mockgen -destination=../../internal/mocks/mockclient/query_client_mock.go -package=mockclient . Dialer,Connection + +package client + +import ( + "context" + + "pocket/pkg/either" + "pocket/pkg/observable" +) + +// TODO_CONSIDERATION: the cosmos-sdk CLI code seems to use a cometbft RPC client +// which includes a `#Subscribe()` method for a similar purpose. Perhaps we could +// replace this custom websocket client with that. +// (see: https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110) +// (see: https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114) +// +// NOTE: a branch which attempts this is available at: +// https://github.com/pokt-network/poktroll/pull/74 + +// EventsBytesObservable is an observable which is notified with an either +// value which contains either an error or the event message bytes. +// TODO_HACK: The purpose of this type is to work around gomock's lack of +// support for generic types. For the same reason, this type cannot be an +// alias (i.e. EventsBytesObservable = observable.Observable[either.Either[[]byte]]). +type EventsBytesObservable observable.Observable[either.Either[[]byte]] + +// EventsQueryClient is used to subscribe to chain event messages matching the given query, +type EventsQueryClient interface { + // EventsBytes returns an observable which is notified about chain event messages + // matching the given query. It receives an either value which contains either an + // error or the event message bytes. + EventsBytes( + ctx context.Context, + query string, + ) (EventsBytesObservable, error) + // Close unsubscribes all observers of each active query's events bytes + // observable and closes the connection. + Close() +} + +// Connection is a transport agnostic, bi-directional, message-passing interface. +type Connection interface { + // Receive blocks until a message is received or an error occurs. + Receive() (msg []byte, err error) + // Send sends a message and may return a synchronous error. + Send(msg []byte) error + // Close closes the connection. + Close() error +} + +// Dialer encapsulates the construction of connections. +type Dialer interface { + // DialContext constructs a connection to the given URL and returns it or + // potentially a synchronous error. + DialContext(ctx context.Context, urlStr string) (Connection, error) +} + +// EventsQueryClientOption is an interface-wide type which can be implemented to use or modify the +// query client during construction. This would likely be done in an +// implementation-specific way; e.g. using a type assertion to assign to an +// implementation struct field(s). +type EventsQueryClientOption func(EventsQueryClient) From 96f5252e61634fe428947008b108f7064f8d91cc Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Fri, 27 Oct 2023 13:10:02 -0700 Subject: [PATCH 04/28] [Application] Add `ServiceConfigs` to `AppStaking` (#95) Update all related components (CLI, Tx, Message, etc...) so applications can stake for a service --- Co-authored-by: Bryan White Co-authored-by: Redouane Lakrache Co-authored-by: red-0ne Co-authored-by: Daniel Olshansky Co-authored-by: harry <53987565+h5law@users.noreply.github.com> --- Makefile | 10 +- docs/static/openapi.yml | 241 ++++++++++-------- go.mod | 4 +- proto/pocket/application/application.proto | 3 +- proto/pocket/application/tx.proto | 6 +- proto/pocket/shared/service.proto | 14 +- testutil/keeper/session.go | 12 +- testutil/network/network.go | 33 ++- .../client/cli/tx_stake_application.go | 16 +- .../client/cli/tx_stake_application_test.go | 101 ++++++-- x/application/keeper/application.go | 20 +- x/application/keeper/application_test.go | 47 ++-- .../keeper/msg_server_stake_application.go | 18 +- .../msg_server_stake_application_test.go | 94 ++++++- .../msg_server_unstake_application_test.go | 7 + x/application/types/errors.go | 9 +- x/application/types/genesis.go | 10 + x/application/types/genesis_test.go | 156 +++++++++--- .../types/message_stake_application.go | 27 +- .../types/message_stake_application_test.go | 97 ++++++- x/session/keeper/session_hydrator_test.go | 2 +- x/shared/helpers/service.go | 53 ++++ x/shared/helpers/service_configs.go | 33 +++ x/shared/helpers/service_test.go | 29 +++ 24 files changed, 794 insertions(+), 248 deletions(-) create mode 100644 x/shared/helpers/service.go create mode 100644 x/shared/helpers/service_configs.go create mode 100644 x/shared/helpers/service_test.go diff --git a/Makefile b/Makefile index 722b193b2..28b58e15e 100644 --- a/Makefile +++ b/Makefile @@ -251,20 +251,20 @@ app_list: ## List all the staked applications pocketd --home=$(POCKETD_HOME) q application list-application --node $(POCKET_NODE) .PHONY: app_stake -app_stake: ## Stake tokens for the application specified (must specify the APP env var) - pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt --keyring-backend test --from $(APP) --node $(POCKET_NODE) +app_stake: ## Stake tokens for the application specified (must specify the APP and SERVICES env vars) + pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt $(SERVICES) --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_stake app1_stake: ## Stake app1 - APP=app1 make app_stake + SERVICES=svc1,svc2 APP=app1 make app_stake .PHONY: app2_stake app2_stake: ## Stake app2 - APP=app2 make app_stake + SERVICES=svc2,svc3 APP=app2 make app_stake .PHONY: app3_stake app3_stake: ## Stake app3 - APP=app3 make app_stake + SERVICES=svc3,svc4 APP=app3 make app_stake .PHONY: app_unstake app_unstake: ## Unstake an application (must specify the APP env var) diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index 26b83a9b8..461aaedf0 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46472,26 +46472,37 @@ paths: custom method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: >- - (Optional) Semantic human readable name for the - service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but + was desigtned created to enable more complex + service identification + + gener + name: + type: string + description: >- + (Optional) Semantic human readable name for + the service + title: >- + TODO_TECHDEBT: Name is currently unused but + acts as a reminder than an optional onchain + representation of the service is necessary title: >- - ServiceId message to encapsulate unique and semantic - identifiers for a service on the network - description: The ID of the service this session is servicing - title: >- - TODO(@olshansk): Change this to - `shared.ApplicationServiceConfig` in #95 + ApplicationServiceConfig holds the service + configuration the application stakes for + title: The ID of the service this session is servicing title: >- Application defines the type used to store an on-chain definition and state for an application @@ -46634,26 +46645,37 @@ paths: custom method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: >- - (Optional) Semantic human readable name for the - service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + gener + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts + as a reminder than an optional onchain + representation of the service is necessary title: >- - ServiceId message to encapsulate unique and semantic - identifiers for a service on the network - description: The ID of the service this session is servicing - title: >- - TODO(@olshansk): Change this to - `shared.ApplicationServiceConfig` in #95 + ApplicationServiceConfig holds the service configuration + the application stakes for + title: The ID of the service this session is servicing title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76529,24 +76551,6 @@ definitions: NOTE: The amount field is an Int which implements the custom method signatures required by gogoproto. - service_ids: - type: array - items: - type: object - properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: (Optional) Semantic human readable name for the service - title: >- - ServiceId message to encapsulate unique and semantic identifiers for - a service on the network - description: The ID of the service this session is servicing - title: >- - TODO(@olshansk): Change this to `shared.ApplicationServiceConfig` in - #95 service_configs: type: array items: @@ -76563,9 +76567,7 @@ definitions: NOTE: `ServiceId.Id` may seem redundant but was desigtned created to enable more complex service identification - For example, what if we want to request a session for a - certain service but with some additional configs that - identify it? + gener name: type: string description: (Optional) Semantic human readable name for the service @@ -76620,24 +76622,37 @@ definitions: method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: (Optional) Semantic human readable name for the service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + gener + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of + the service is necessary title: >- - ServiceId message to encapsulate unique and semantic - identifiers for a service on the network - description: The ID of the service this session is servicing - title: >- - TODO(@olshansk): Change this to - `shared.ApplicationServiceConfig` in #95 + ApplicationServiceConfig holds the service configuration the + application stakes for + title: The ID of the service this session is servicing title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76694,24 +76709,35 @@ definitions: method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: (Optional) Semantic human readable name for the service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + gener + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary title: >- - ServiceId message to encapsulate unique and semantic identifiers - for a service on the network - description: The ID of the service this session is servicing - title: >- - TODO(@olshansk): Change this to `shared.ApplicationServiceConfig` - in #95 + ApplicationServiceConfig holds the service configuration the + application stakes for + title: The ID of the service this session is servicing title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76722,17 +76748,48 @@ definitions: description: params holds all the parameters of this module. type: object description: QueryParamsResponse is response type for the Query/Params RPC method. + pocket.shared.ApplicationServiceConfig: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created + to enable more complex service identification + + gener + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder + than an optional onchain representation of the service is + necessary + title: >- + ApplicationServiceConfig holds the service configuration the application + stakes for pocket.shared.ServiceId: type: object properties: id: type: string - title: Unique identifier for the service description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created to + enable more complex service identification + + gener name: type: string - title: (Optional) Semantic human readable name for the service description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder than an + optional onchain representation of the service is necessary title: >- ServiceId message to encapsulate unique and semantic identifiers for a service on the network @@ -77406,32 +77463,6 @@ definitions: It is the minimal amount of data required to hydrate & retrieve all data relevant to the session. - pocket.shared.ApplicationServiceConfig: - type: object - properties: - service_id: - title: Unique and semantic identifier for the service - type: object - properties: - id: - type: string - description: Unique identifier for the service - title: >- - NOTE: `ServiceId.Id` may seem redundant but was desigtned created - to enable more complex service identification - - For example, what if we want to request a session for a certain - service but with some additional configs that identify it? - name: - type: string - description: (Optional) Semantic human readable name for the service - title: >- - TODO_TECHDEBT: Name is currently unused but acts as a reminder - than an optional onchain representation of the service is - necessary - title: >- - ApplicationServiceConfig holds the service configuration the application - stakes for pocket.shared.ConfigOption: type: object properties: diff --git a/go.mod b/go.mod index d2fd3985c..cb8df3174 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( cosmossdk.io/math v1.0.1 github.com/cometbft/cometbft v0.37.2 github.com/cometbft/cometbft-db v0.8.0 - github.com/cosmos/cosmos-proto v1.0.0-beta.2 github.com/cosmos/cosmos-sdk v0.47.3 github.com/cosmos/gogoproto v1.4.10 github.com/cosmos/ibc-go/v7 v7.1.0 @@ -26,7 +25,6 @@ require ( go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.12.0 golang.org/x/sync v0.3.0 - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 google.golang.org/grpc v1.56.1 gopkg.in/yaml.v2 v2.4.0 ) @@ -71,6 +69,7 @@ require ( github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.0 // indirect @@ -266,6 +265,7 @@ require ( gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/api v0.122.0 // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/proto/pocket/application/application.proto b/proto/pocket/application/application.proto index 4ccb1940a..c7b0ae83b 100644 --- a/proto/pocket/application/application.proto +++ b/proto/pocket/application/application.proto @@ -11,7 +11,6 @@ import "pocket/shared/service.proto"; message Application { string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked - // TODO(@olshansk): Change this to `shared.ApplicationServiceConfig` in #95 - repeated shared.ServiceId service_ids = 3; // The ID of the service this session is servicing + repeated shared.ApplicationServiceConfig service_configs = 3; // The ID of the service this session is servicing } diff --git a/proto/pocket/application/tx.proto b/proto/pocket/application/tx.proto index 4bf9eb789..971d47b83 100644 --- a/proto/pocket/application/tx.proto +++ b/proto/pocket/application/tx.proto @@ -5,6 +5,7 @@ package pocket.application; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; import "cosmos/msg/v1/msg.proto"; +import "pocket/shared/service.proto"; option go_package = "pocket/x/application/types"; @@ -19,9 +20,8 @@ message MsgStakeApplication { option (cosmos.msg.v1.signer) = "address"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding - cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked. Must be ≥ to the current amount that the application has staked (if any) - // TODO(@Olshansk): Update the tx flow to add support for `services` - // repeated service.ApplicationServiceConfig services = 3; // The list of services this application is staked to request service for + cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked. Must be ≥ to the current amount that the application has staked (if any) + repeated shared.ApplicationServiceConfig services = 3; // The list of services this application is staked to request service for } message MsgStakeApplicationResponse {} diff --git a/proto/pocket/shared/service.proto b/proto/pocket/shared/service.proto index cff126d35..42827cef2 100644 --- a/proto/pocket/shared/service.proto +++ b/proto/pocket/shared/service.proto @@ -10,18 +10,24 @@ option go_package = "pocket/x/shared/types"; // ServiceId message to encapsulate unique and semantic identifiers for a service on the network message ServiceId { + // NOTE: `ServiceId.Id` may seem redundant but was desigtned created to enable more complex service identification + // For example, what if we want to request a session for a certain service but with some additional configs that identify it? string id = 1; // Unique identifier for the service + + // TODO_TECHDEBT: Name is currently unused but acts as a reminder than an optional onchain representation of the service is necessary string name = 2; // (Optional) Semantic human readable name for the service + // NOTE: `ServiceId.Id` may seem redundant but was designed to enable more complex service identification. // For example, what if we want to request a session for a certain service but with some additional configs that identify it? } -// SupplierServiceConfig holds the service configuration the application stakes for +// ApplicationServiceConfig holds the service configuration the application stakes for message ApplicationServiceConfig { - repeated ServiceId service_id = 1; // Unique and semantic identifier for the service + ServiceId service_id = 1; // Unique and semantic identifier for the service + // TODO_RESEARCH: There is an opportunity for applications to advertise the max // they're willing to pay for a certain configuration/price, but this is outside of scope. - // repeated RPCConfig rpc_configs = 2; // List of endpoints for the service + // RPCConfig rpc_configs = 2; // List of endpoints for the service } // SupplierServiceConfig holds the service configuration the supplier stakes for @@ -32,7 +38,7 @@ message SupplierServiceConfig { // they're willing to earn for a certain configuration/price, but this is outside of scope. } -// Endpoint message to hold service configuration details +// SupplierEndpoint message to hold service configuration details message SupplierEndpoint { string url = 1; // URL of the endpoint RPCType rpc_type = 2; // Type of RPC exposed on the url above diff --git a/testutil/keeper/session.go b/testutil/keeper/session.go index cd3ea868f..ef18152f6 100644 --- a/testutil/keeper/session.go +++ b/testutil/keeper/session.go @@ -34,12 +34,12 @@ var ( TestApp1 = apptypes.Application{ Address: TestApp1Address, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, - ServiceIds: []*sharedtypes.ServiceId{ + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ { - Id: TestServiceId1, + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId1}, }, { - Id: TestServiceId2, + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId2}, }, }, } @@ -48,12 +48,12 @@ var ( TestApp2 = apptypes.Application{ Address: TestApp1Address, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, - ServiceIds: []*sharedtypes.ServiceId{ + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ { - Id: TestServiceId1, + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId1}, }, { - Id: TestServiceId2, + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId2}, }, }, } diff --git a/testutil/network/network.go b/testutil/network/network.go index 1b5f3022d..dcd44a5a1 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -25,10 +25,10 @@ import ( "pocket/app" "pocket/testutil/nullify" "pocket/testutil/sample" - app_types "pocket/x/application/types" - gateway_types "pocket/x/gateway/types" - shared_types "pocket/x/shared/types" - supplier_types "pocket/x/supplier/types" + apptypes "pocket/x/application/types" + gatewaytypes "pocket/x/gateway/types" + sharedtypes "pocket/x/shared/types" + suppliertypes "pocket/x/supplier/types" ) type ( @@ -103,16 +103,21 @@ func DefaultConfig() network.Config { // DefaultApplicationModuleGenesisState generates a GenesisState object with a given number of applications. // It returns the populated GenesisState object. -func DefaultApplicationModuleGenesisState(t *testing.T, n int) *app_types.GenesisState { +func DefaultApplicationModuleGenesisState(t *testing.T, n int) *apptypes.GenesisState { t.Helper() - state := app_types.DefaultGenesis() + state := apptypes.DefaultGenesis() for i := 0; i < n; i++ { stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i+1))) - application := app_types.Application{ + application := apptypes.Application{ Address: sample.AccAddress(), Stake: &stake, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: fmt.Sprintf("svc%d", i)}, + }, + }, } - nullify.Fill(&application) + // nullify.Fill(&application) state.ApplicationList = append(state.ApplicationList, application) } return state @@ -120,12 +125,12 @@ func DefaultApplicationModuleGenesisState(t *testing.T, n int) *app_types.Genesi // DefaultGatewayModuleGenesisState generates a GenesisState object with a given number of gateways. // It returns the populated GenesisState object. -func DefaultGatewayModuleGenesisState(t *testing.T, n int) *gateway_types.GenesisState { +func DefaultGatewayModuleGenesisState(t *testing.T, n int) *gatewaytypes.GenesisState { t.Helper() - state := gateway_types.DefaultGenesis() + state := gatewaytypes.DefaultGenesis() for i := 0; i < n; i++ { stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i))) - gateway := gateway_types.Gateway{ + gateway := gatewaytypes.Gateway{ Address: strconv.Itoa(i), Stake: &stake, } @@ -137,12 +142,12 @@ func DefaultGatewayModuleGenesisState(t *testing.T, n int) *gateway_types.Genesi // DefaultSupplierModuleGenesisState generates a GenesisState object with a given number of suppliers. // It returns the populated GenesisState object. -func DefaultSupplierModuleGenesisState(t *testing.T, n int) *supplier_types.GenesisState { +func DefaultSupplierModuleGenesisState(t *testing.T, n int) *suppliertypes.GenesisState { t.Helper() - state := supplier_types.DefaultGenesis() + state := suppliertypes.DefaultGenesis() for i := 0; i < n; i++ { stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i))) - gateway := shared_types.Supplier{ + gateway := sharedtypes.Supplier{ Address: strconv.Itoa(i), Stake: &stake, } diff --git a/x/application/client/cli/tx_stake_application.go b/x/application/client/cli/tx_stake_application.go index 8a20f533c..b486bc6be 100644 --- a/x/application/client/cli/tx_stake_application.go +++ b/x/application/client/cli/tx_stake_application.go @@ -2,6 +2,7 @@ package cli import ( "strconv" + "strings" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -17,16 +18,20 @@ var _ = strconv.Itoa(0) func CmdStakeApplication() *cobra.Command { // fromAddress & signature is retrieved via `flags.FlagFrom` in the `clientCtx` cmd := &cobra.Command{ - Use: "stake-application [amount]", + // TODO_HACK: For now we are only specifying the service IDs as a list of of strings separated by commas. + // This needs to be expand to specify the full ApplicationServiceConfig. Furthermore, providing a flag to + // a file where ApplicationServiceConfig specifying full service configurations in the CLI by providing a flag that accepts a JSON string + Use: "stake-application [amount] [svcId1,svcId2,...,svcIdN]", Short: "Stake an application", Long: `Stake an application with the provided parameters. This is a broadcast operation that -will stake the tokens and associate them with the application specified by the 'from' address. +will stake the tokens and serviceIds and associate them with the application specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, - Args: cobra.ExactArgs(1), +$ pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt svc1,svc2,svc3 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { stakeString := args[0] + serviceIdsString := args[1] clientCtx, err := client.GetClientTxContext(cmd) if err != nil { @@ -38,9 +43,12 @@ $ pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt --ke return err } + serviceIds := strings.Split(serviceIdsString, ",") + msg := types.NewMsgStakeApplication( clientCtx.GetFromAddress().String(), stake, + serviceIds, ) if err := msg.ValidateBasic(); err != nil { diff --git a/x/application/client/cli/tx_stake_application_test.go b/x/application/client/cli/tx_stake_application_test.go index 8a0dac4b6..3721e8ed4 100644 --- a/x/application/client/cli/tx_stake_application_test.go +++ b/x/application/client/cli/tx_stake_application_test.go @@ -39,51 +39,95 @@ func TestCLI_StakeApplication(t *testing.T) { } tests := []struct { - desc string - address string - stakeString string - err *sdkerrors.Error + desc string + address string + stakeString string + serviceIdsString string + err *sdkerrors.Error }{ + // Happy Paths { - desc: "stake application: valid", - address: appAccount.Address.String(), - stakeString: "1000upokt", + desc: "valid", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "svc1,svc2,svc3", + err: nil, }, + + // Error Paths - Address Related { - desc: "stake application: missing address", + desc: "address_test: missing address", // address: "explicitly missing", - stakeString: "1000upokt", - err: types.ErrAppInvalidAddress, + stakeString: "1000upokt", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidAddress, }, { - desc: "stake application: invalid address", - address: "invalid", - stakeString: "1000upokt", - err: types.ErrAppInvalidAddress, + desc: "stake application: invalid address", + address: "invalid", + stakeString: "1000upokt", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidAddress, }, + + // Error Paths - Stake Related { - desc: "stake application: missing stake", + desc: "address_test: missing stake", address: appAccount.Address.String(), // stakeString: "explicitly missing", - err: types.ErrAppInvalidStake, + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidStake, + }, + { + desc: "address_test: invalid stake denom", + address: appAccount.Address.String(), + stakeString: "1000invalid", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidStake, + }, + { + desc: "address_test: invalid stake amount (zero)", + address: appAccount.Address.String(), + stakeString: "0upokt", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidStake, + }, + { + desc: "address_test: invalid stake amount (negative)", + address: appAccount.Address.String(), + stakeString: "-1000upokt", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidStake, + }, + + // Error Paths - Service Related + { + desc: "services_test: invalid services (empty string)", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "", + err: types.ErrAppInvalidStake, }, { - desc: "stake application: invalid stake denom", - address: appAccount.Address.String(), - stakeString: "1000invalid", - err: types.ErrAppInvalidStake, + desc: "services_test: single invalid service contains spaces", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "svc1 svc1_part2 svc1_part3", + err: types.ErrAppInvalidStake, }, { - desc: "stake application: invalid stake amount (zero)", - address: appAccount.Address.String(), - stakeString: "0upokt", - err: types.ErrAppInvalidStake, + desc: "services_test: one of two services is invalid because it contains spaces", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "svc1 svc1_part2,svc2", + err: types.ErrAppInvalidStake, }, { - desc: "stake application: invalid stake amount (negative)", - address: appAccount.Address.String(), - stakeString: "-1000upokt", - err: types.ErrAppInvalidStake, + desc: "services_test: service ID is too long (8 chars is the max)", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "svc1,abcdefghi", + err: types.ErrAppInvalidStake, }, } @@ -99,6 +143,7 @@ func TestCLI_StakeApplication(t *testing.T) { // Prepare the arguments for the CLI command args := []string{ tt.stakeString, + tt.serviceIdsString, fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.address), } args = append(args, commonArgs...) diff --git a/x/application/keeper/application.go b/x/application/keeper/application.go index 256328744..64025ff1b 100644 --- a/x/application/keeper/application.go +++ b/x/application/keeper/application.go @@ -19,36 +19,36 @@ func (k Keeper) SetApplication(ctx sdk.Context, application types.Application) { // GetApplication returns a application from its index func (k Keeper) GetApplication( ctx sdk.Context, - address string, + appAddr string, -) (val types.Application, found bool) { +) (app types.Application, found bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ApplicationKeyPrefix)) b := store.Get(types.ApplicationKey( - address, + appAddr, )) if b == nil { - return val, false + return app, false } - k.cdc.MustUnmarshal(b, &val) - return val, true + k.cdc.MustUnmarshal(b, &app) + return app, true } // RemoveApplication removes a application from the store func (k Keeper) RemoveApplication( ctx sdk.Context, - address string, + appAddr string, ) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ApplicationKeyPrefix)) store.Delete(types.ApplicationKey( - address, + appAddr, )) } // GetAllApplication returns all application -func (k Keeper) GetAllApplication(ctx sdk.Context) (list []types.Application) { +func (k Keeper) GetAllApplication(ctx sdk.Context) (apps []types.Application) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ApplicationKeyPrefix)) iterator := sdk.KVStorePrefixIterator(store, []byte{}) @@ -57,7 +57,7 @@ func (k Keeper) GetAllApplication(ctx sdk.Context) (list []types.Application) { for ; iterator.Valid(); iterator.Next() { var val types.Application k.cdc.MustUnmarshal(iterator.Value(), &val) - list = append(list, val) + apps = append(apps, val) } return diff --git a/x/application/keeper/application_test.go b/x/application/keeper/application_test.go index 76f5ad1a3..38c16c0e6 100644 --- a/x/application/keeper/application_test.go +++ b/x/application/keeper/application_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "fmt" "strconv" "testing" @@ -11,8 +12,10 @@ import ( "pocket/cmd/pocketd/cmd" keepertest "pocket/testutil/keeper" "pocket/testutil/nullify" + "pocket/testutil/sample" "pocket/x/application/keeper" "pocket/x/application/types" + sharedtypes "pocket/x/shared/types" ) // Prevent strconv unused error @@ -23,38 +26,44 @@ func init() { } func createNApplication(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.Application { - items := make([]types.Application, n) - for i := range items { - items[i].Address = strconv.Itoa(i) - - keeper.SetApplication(ctx, items[i]) + apps := make([]types.Application, n) + for i := range apps { + app := &apps[i] + app.Address = sample.AccAddress() + app.Stake = &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(int64(i))} + app.ServiceConfigs = []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: fmt.Sprintf("svc%d", i)}, + }, + } + keeper.SetApplication(ctx, *app) } - return items + return apps } func TestApplicationGet(t *testing.T) { keeper, ctx := keepertest.ApplicationKeeper(t) - items := createNApplication(keeper, ctx, 10) - for _, item := range items { - rst, found := keeper.GetApplication(ctx, - item.Address, + apps := createNApplication(keeper, ctx, 10) + for _, app := range apps { + appFound, isAppFound := keeper.GetApplication(ctx, + app.Address, ) - require.True(t, found) + require.True(t, isAppFound) require.Equal(t, - nullify.Fill(&item), - nullify.Fill(&rst), + nullify.Fill(&app), + nullify.Fill(&appFound), ) } } func TestApplicationRemove(t *testing.T) { keeper, ctx := keepertest.ApplicationKeeper(t) - items := createNApplication(keeper, ctx, 10) - for _, item := range items { + apps := createNApplication(keeper, ctx, 10) + for _, app := range apps { keeper.RemoveApplication(ctx, - item.Address, + app.Address, ) _, found := keeper.GetApplication(ctx, - item.Address, + app.Address, ) require.False(t, found) } @@ -62,9 +71,9 @@ func TestApplicationRemove(t *testing.T) { func TestApplicationGetAll(t *testing.T) { keeper, ctx := keepertest.ApplicationKeeper(t) - items := createNApplication(keeper, ctx, 10) + apps := createNApplication(keeper, ctx, 10) require.ElementsMatch(t, - nullify.Fill(items), + nullify.Fill(apps), nullify.Fill(keeper.GetAllApplication(ctx)), ) } diff --git a/x/application/keeper/msg_server_stake_application.go b/x/application/keeper/msg_server_stake_application.go index 7fd06a529..6aa824e24 100644 --- a/x/application/keeper/msg_server_stake_application.go +++ b/x/application/keeper/msg_server_stake_application.go @@ -19,6 +19,7 @@ func (k msgServer) StakeApplication( logger.Info("About to stake application with msg: %v", msg) if err := msg.ValidateBasic(); err != nil { + logger.Error("invalid MsgStakeApplication: %v", err) return nil, err } @@ -46,6 +47,7 @@ func (k msgServer) StakeApplication( return nil, err } + // TODO_IMPROVE: Should we avoid making this call if `coinsToDelegate` = 0? // Send the coins from the application to the staked application pool err = k.bankKeeper.DelegateCoinsFromAccountToModule(ctx, appAddress, types.ModuleName, []sdk.Coin{coinsToDelegate}) if err != nil { @@ -65,8 +67,9 @@ func (k msgServer) createApplication( msg *types.MsgStakeApplication, ) types.Application { return types.Application{ - Address: msg.Address, - Stake: msg.Stake, + Address: msg.Address, + Stake: msg.Stake, + ServiceConfigs: msg.Services, } } @@ -80,16 +83,21 @@ func (k msgServer) updateApplication( return sdkerrors.Wrapf(types.ErrAppUnauthorized, "msg Address (%s) != application address (%s)", msg.Address, app.Address) } + // Validate that the stake is not being lowered if msg.Stake == nil { return sdkerrors.Wrapf(types.ErrAppInvalidStake, "stake amount cannot be nil") } - if msg.Stake.IsLTE(*app.Stake) { - return sdkerrors.Wrapf(types.ErrAppInvalidStake, "stake amount %v must be higher than previous stake amount %v", msg.Stake, app.Stake) } - app.Stake = msg.Stake + // Validate that the service configs maintain at least one service. Additional validation is done in + // `msg.ValidateBasic` above. + if len(msg.Services) == 0 { + return sdkerrors.Wrapf(types.ErrAppInvalidServiceConfigs, "must have at least one service") + } + app.ServiceConfigs = msg.Services + return nil } diff --git a/x/application/keeper/msg_server_stake_application_test.go b/x/application/keeper/msg_server_stake_application_test.go index 0a4747957..98d3069ea 100644 --- a/x/application/keeper/msg_server_stake_application_test.go +++ b/x/application/keeper/msg_server_stake_application_test.go @@ -10,6 +10,7 @@ import ( "pocket/testutil/sample" "pocket/x/application/keeper" "pocket/x/application/types" + sharedtypes "pocket/x/shared/types" ) func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { @@ -28,6 +29,11 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { stakeMsg := &types.MsgStakeApplication{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, } // Stake the application @@ -39,11 +45,21 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { require.True(t, isAppFound) require.Equal(t, addr, foundApp.Address) require.Equal(t, int64(100), foundApp.Stake.Amount.Int64()) + require.Len(t, foundApp.ServiceConfigs, 1) + require.Equal(t, "svc1", foundApp.ServiceConfigs[0].ServiceId.Id) - // Prepare an updated application with a higher stake + // Prepare an updated application with a higher stake and another service updateStakeMsg := &types.MsgStakeApplication{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(200)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + { + ServiceId: &sharedtypes.ServiceId{Id: "svc2"}, + }, + }, } // Update the staked application @@ -52,6 +68,72 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { foundApp, isAppFound = k.GetApplication(ctx, addr) require.True(t, isAppFound) require.Equal(t, int64(200), foundApp.Stake.Amount.Int64()) + require.Len(t, foundApp.ServiceConfigs, 2) + require.Equal(t, "svc1", foundApp.ServiceConfigs[0].ServiceId.Id) + require.Equal(t, "svc2", foundApp.ServiceConfigs[1].ServiceId.Id) +} + +func TestMsgServer_StakeApplication_FailRestakingDueToInvalidServices(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + appAddr := sample.AccAddress() + + // Prepare the application stake message + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + + // Prepare the application stake message without any services + updateStakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{}, + } + + // Fail updating the application when the list of services is empty + _, err = srv.StakeApplication(wctx, updateStakeMsg) + require.Error(t, err) + + // Verify the app still exists and is staked for svc1 + app, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, app.Address) + require.Len(t, app.ServiceConfigs, 1) + require.Equal(t, "svc1", app.ServiceConfigs[0].ServiceId.Id) + + // Prepare the application stake message with an invalid service ID + updateStakeMsg = &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1 INVALID ! & *"}, + }, + }, + } + + // Fail updating the application when the list of services is empty + _, err = srv.StakeApplication(wctx, updateStakeMsg) + require.Error(t, err) + + // Verify the app still exists and is staked for svc1 + app, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, app.Address) + require.Len(t, app.ServiceConfigs, 1) + require.Equal(t, "svc1", app.ServiceConfigs[0].ServiceId.Id) } func TestMsgServer_StakeApplication_FailLoweringStake(t *testing.T) { @@ -64,6 +146,11 @@ func TestMsgServer_StakeApplication_FailLoweringStake(t *testing.T) { stakeMsg := &types.MsgStakeApplication{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, } // Stake the application & verify that the application exists @@ -76,6 +163,11 @@ func TestMsgServer_StakeApplication_FailLoweringStake(t *testing.T) { updateMsg := &types.MsgStakeApplication{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(50)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, } // Verify that it fails diff --git a/x/application/keeper/msg_server_unstake_application_test.go b/x/application/keeper/msg_server_unstake_application_test.go index 7d2a2799c..9f45647f0 100644 --- a/x/application/keeper/msg_server_unstake_application_test.go +++ b/x/application/keeper/msg_server_unstake_application_test.go @@ -10,6 +10,7 @@ import ( "pocket/testutil/sample" "pocket/x/application/keeper" "pocket/x/application/types" + sharedtypes "pocket/x/shared/types" ) func TestMsgServer_UnstakeApplication_Success(t *testing.T) { @@ -29,6 +30,11 @@ func TestMsgServer_UnstakeApplication_Success(t *testing.T) { stakeMsg := &types.MsgStakeApplication{ Address: addr, Stake: &initialStake, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, } // Stake the application @@ -40,6 +46,7 @@ func TestMsgServer_UnstakeApplication_Success(t *testing.T) { require.True(t, isAppFound) require.Equal(t, addr, foundApp.Address) require.Equal(t, initialStake.Amount, foundApp.Stake.Amount) + require.Len(t, foundApp.ServiceConfigs, 1) // Unstake the application unstakeMsg := &types.MsgUnstakeApplication{Address: addr} diff --git a/x/application/types/errors.go b/x/application/types/errors.go index 94d76ccad..0a84948da 100644 --- a/x/application/types/errors.go +++ b/x/application/types/errors.go @@ -8,8 +8,9 @@ import ( // x/application module sentinel errors var ( - ErrAppInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid application stake") - ErrAppInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid application address") - ErrAppUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized application signer") - ErrAppNotFound = sdkerrors.Register(ModuleName, 4, "application not found") + ErrAppInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid application stake") + ErrAppInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid application address") + ErrAppUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized application signer") + ErrAppNotFound = sdkerrors.Register(ModuleName, 4, "application not found") + ErrAppInvalidServiceConfigs = sdkerrors.Register(ModuleName, 5, "invalid service configs") ) diff --git a/x/application/types/genesis.go b/x/application/types/genesis.go index b42c4070c..1d8d5f892 100644 --- a/x/application/types/genesis.go +++ b/x/application/types/genesis.go @@ -5,6 +5,8 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + + servicehelpers "pocket/x/shared/helpers" ) // DefaultIndex is the default global index @@ -34,6 +36,8 @@ func (gs GenesisState) Validate() error { // Check that the stake value for the apps is valid for _, app := range gs.ApplicationList { + // TODO_TECHDEBT: Consider creating shared helpers across the board for stake validation, + // similar to how we have `AreValidAppServiceConfigs` below if app.Stake == nil { return sdkerrors.Wrapf(ErrAppInvalidStake, "nil stake amount for application") } @@ -50,6 +54,12 @@ func (gs GenesisState) Validate() error { if stake.Denom != "upokt" { return sdkerrors.Wrapf(ErrAppInvalidStake, "invalid stake amount denom for application %v", app.Stake) } + + // Valid the application service configs + // Validate the application service configs + if reason, ok := servicehelpers.AreValidAppServiceConfigs(app.ServiceConfigs); !ok { + return sdkerrors.Wrapf(ErrAppInvalidStake, reason) + } } // this line is used by starport scaffolding # genesis/types/validate diff --git a/x/application/types/genesis_test.go b/x/application/types/genesis_test.go index 69bc318c2..f1eed28b3 100644 --- a/x/application/types/genesis_test.go +++ b/x/application/types/genesis_test.go @@ -8,14 +8,21 @@ import ( "pocket/testutil/sample" "pocket/x/application/types" + sharedtypes "pocket/x/shared/types" ) func TestGenesisState_Validate(t *testing.T) { addr1 := sample.AccAddress() stake1 := sdk.NewCoin("upokt", sdk.NewInt(100)) + svc1AppConfig := &sharedtypes.ApplicationServiceConfig{ + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + } addr2 := sample.AccAddress() stake2 := sdk.NewCoin("upokt", sdk.NewInt(100)) + svc2AppConfig := &sharedtypes.ApplicationServiceConfig{ + ServiceId: &sharedtypes.ServiceId{Id: "svc2"}, + } tests := []struct { desc string @@ -33,12 +40,14 @@ func TestGenesisState_Validate(t *testing.T) { ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, }, { - Address: addr2, - Stake: &stake2, + Address: addr2, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, }, }, // this line is used by starport scaffolding # types/genesis/validField @@ -50,12 +59,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, }, }, }, @@ -66,12 +77,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, }, }, }, @@ -82,12 +95,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, }, }, }, @@ -98,12 +113,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, }, }, }, @@ -114,12 +131,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, }, { - Address: addr1, - Stake: &stake2, + Address: addr1, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, }, }, }, @@ -127,36 +146,115 @@ func TestGenesisState_Validate(t *testing.T) { }, { desc: "invalid - due to nil app stake", + genState: &types.GenesisState{ + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + }, + { + Address: addr2, + Stake: nil, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - due to missing app stake", + genState: &types.GenesisState{ + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + }, + { + Address: addr2, + // Explicitly missing stake + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - service config not present", genState: &types.GenesisState{ ApplicationList: []types.Application{ { Address: addr1, Stake: &stake1, + // ServiceConfigs: omitted }, + }, + }, + valid: false, + }, + { + desc: "invalid - empty service config", + genState: &types.GenesisState{ + ApplicationList: []types.Application{ { - Address: addr2, - Stake: nil, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{}, }, }, }, valid: false, }, { - desc: "invalid - due to missing app stake", + desc: "invalid - service ID too long", + genState: &types.GenesisState{ + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "12345678901"}}, + }, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - service name too long", genState: &types.GenesisState{ ApplicationList: []types.Application{ { Address: addr1, Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{ + Id: "123", + Name: "abcdefghijklmnopqrstuvwxyzab-abcdefghijklmnopqrstuvwxyzab", + }}, + }, }, + }, + }, + valid: false, + }, + { + desc: "invalid - service ID with invalid characters", + genState: &types.GenesisState{ + ApplicationList: []types.Application{ { - Address: addr2, - // Explicitly missing stake + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "12 45 !"}}, + }, }, }, }, valid: false, }, + // this line is used by starport scaffolding # types/genesis/testcase } for _, tc := range tests { diff --git a/x/application/types/message_stake_application.go b/x/application/types/message_stake_application.go index 219a65701..f813cb809 100644 --- a/x/application/types/message_stake_application.go +++ b/x/application/types/message_stake_application.go @@ -4,6 +4,9 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/types" + + servicehelpers "pocket/x/shared/helpers" + sharedtypes "pocket/x/shared/types" ) const TypeMsgStakeApplication = "stake_application" @@ -13,11 +16,22 @@ var _ sdk.Msg = (*MsgStakeApplication)(nil) func NewMsgStakeApplication( address string, stake types.Coin, - + serviceIds []string, ) *MsgStakeApplication { + // Convert the serviceIds to the proper ApplicationServiceConfig type (enables future expansion) + appServiceConfigs := make([]*sharedtypes.ApplicationServiceConfig, len(serviceIds)) + for idx, serviceId := range serviceIds { + appServiceConfigs[idx] = &sharedtypes.ApplicationServiceConfig{ + ServiceId: &sharedtypes.ServiceId{ + Id: serviceId, + }, + } + } + return &MsgStakeApplication{ - Address: address, - Stake: &stake, + Address: address, + Stake: &stake, + Services: appServiceConfigs, } } @@ -64,7 +78,12 @@ func (msg *MsgStakeApplication) ValidateBasic() error { return sdkerrors.Wrapf(ErrAppInvalidStake, "invalid stake amount for application: %v <= 0", msg.Stake) } if stake.Denom != "upokt" { - return sdkerrors.Wrapf(ErrAppInvalidStake, "invalid stake amount denom for application %v", msg.Stake) + return sdkerrors.Wrapf(ErrAppInvalidStake, "invalid stake amount denom for application: %v", msg.Stake) + } + + // Validate the application service configs + if reason, ok := servicehelpers.AreValidAppServiceConfigs(msg.Services); !ok { + return sdkerrors.Wrapf(ErrAppInvalidServiceConfigs, reason) } return nil diff --git a/x/application/types/message_stake_application_test.go b/x/application/types/message_stake_application_test.go index 3e36098a7..6b5ceec7a 100644 --- a/x/application/types/message_stake_application_test.go +++ b/x/application/types/message_stake_application_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "pocket/testutil/sample" + sharedtypes "pocket/x/shared/types" ) func TestMsgStakeApplication_ValidateBasic(t *testing.T) { @@ -15,18 +16,28 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg MsgStakeApplication err error }{ + // address tests { name: "invalid address - nil stake", msg: MsgStakeApplication{ Address: "invalid_address", // Stake explicitly nil + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidAddress, - }, { + }, + + // stake related tests + { name: "valid address - nil stake", msg: MsgStakeApplication{ Address: sample.AccAddress(), // Stake explicitly nil + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, { @@ -34,12 +45,18 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, }, { name: "valid address - zero stake", msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, { @@ -47,6 +64,9 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, { @@ -54,6 +74,9 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, { @@ -61,16 +84,86 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, + + // service related tests + { + name: "invalid service configs - not present", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + // Services: omitted + }, + err: ErrAppInvalidServiceConfigs, + }, + { + name: "invalid service configs - empty", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{}, + }, + err: ErrAppInvalidServiceConfigs, + }, + { + name: "invalid service configs - invalid service ID that's too long", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "123456790"}}, + }, + }, + err: ErrAppInvalidServiceConfigs, + }, + { + name: "invalid service configs - invalid service Name that's too long", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{ + Id: "123", + Name: "abcdefghijklmnopqrstuvwxyzab-abcdefghijklmnopqrstuvwxyzab", + }}, + }, + }, + err: ErrAppInvalidServiceConfigs, + }, + { + name: "invalid service configs - invalid service ID that contains invalid characters", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "12 45 !"}}, + }, + }, + err: ErrAppInvalidServiceConfigs, + }, + { + name: "valid service configs - multiple services", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + {ServiceId: &sharedtypes.ServiceId{Id: "svc2"}}, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.msg.ValidateBasic() if tt.err != nil { - require.ErrorIs(t, err, tt.err) + require.ErrorContains(t, err, tt.err.Error()) return } require.NoError(t, err) diff --git a/x/session/keeper/session_hydrator_test.go b/x/session/keeper/session_hydrator_test.go index 83d8a3876..7a1c10a19 100644 --- a/x/session/keeper/session_hydrator_test.go +++ b/x/session/keeper/session_hydrator_test.go @@ -35,7 +35,7 @@ func TestSession_HydrateSession_Success_BaseCase(t *testing.T) { // Check the application app := session.Application require.Equal(t, keepertest.TestApp1Address, app.Address) - require.Len(t, app.ServiceIds, 2) + require.Len(t, app.ServiceConfigs, 2) // Check the suppliers suppliers := session.Suppliers diff --git a/x/shared/helpers/service.go b/x/shared/helpers/service.go new file mode 100644 index 000000000..1eb302958 --- /dev/null +++ b/x/shared/helpers/service.go @@ -0,0 +1,53 @@ +package helpers + +import "regexp" + +const ( + maxServiceIdLength = 8 // Limiting all serviceIds to 8 characters + maxServiceIdName = 42 // Limit the the name of the + + regexServiceId = "^[a-zA-Z0-9_-]+$" // Define the regex pattern to match allowed characters + regexServiceName = "^[a-zA-Z0-9-_ ]+$" // Define the regex pattern to match allowed characters (allows spaces) +) + +var ( + regexExprServiceId *regexp.Regexp + regexExprServiceName *regexp.Regexp +) + +func init() { + // Compile the regex pattern + regexExprServiceId = regexp.MustCompile(regexServiceId) + regexExprServiceName = regexp.MustCompile(regexServiceName) + +} + +// IsValidServiceId checks if the input string is a valid serviceId +func IsValidServiceId(serviceId string) bool { + // ServiceId CANNOT be empty + if len(serviceId) == 0 { + return false + } + + if len(serviceId) > maxServiceIdLength { + return false + } + + // Use the regex to match against the input string + return regexExprServiceId.MatchString(serviceId) +} + +// IsValidServiceName checks if the input string is a valid serviceName +func IsValidServiceName(serviceName string) bool { + // ServiceName CAN be empty + if len(serviceName) == 0 { + return true + } + + if len(serviceName) > maxServiceIdName { + return false + } + + // Use the regex to match against the input string + return regexExprServiceName.MatchString(serviceName) +} diff --git a/x/shared/helpers/service_configs.go b/x/shared/helpers/service_configs.go new file mode 100644 index 000000000..7a7baaf33 --- /dev/null +++ b/x/shared/helpers/service_configs.go @@ -0,0 +1,33 @@ +package helpers + +import ( + "fmt" + + sharedtypes "pocket/x/shared/types" +) + +// AreValidAppServiceConfigs returns an error if the provided service configs are invalid +// by wrapping the provided around with additional details +func AreValidAppServiceConfigs(services []*sharedtypes.ApplicationServiceConfig) (string, bool) { + if len(services) == 0 { + return fmt.Sprintf("no services configs provided for application: %v", services), false + } + for _, serviceConfig := range services { + if serviceConfig == nil { + return fmt.Sprintf("serviceConfig cannot be nil: %v", services), false + } + if serviceConfig.ServiceId == nil { + return fmt.Sprintf("serviceId cannot be nil: %v", serviceConfig), false + } + if serviceConfig.ServiceId.Id == "" { + return fmt.Sprintf("serviceId.Id cannot be empty: %v", serviceConfig), false + } + if !IsValidServiceId(serviceConfig.ServiceId.Id) { + return fmt.Sprintf("invalid serviceId.Id: %v", serviceConfig), false + } + if !IsValidServiceName(serviceConfig.ServiceId.Name) { + return fmt.Sprintf("invalid serviceId.Name: %v", serviceConfig), false + } + } + return "", true +} diff --git a/x/shared/helpers/service_test.go b/x/shared/helpers/service_test.go new file mode 100644 index 000000000..1d344b095 --- /dev/null +++ b/x/shared/helpers/service_test.go @@ -0,0 +1,29 @@ +package helpers + +import "testing" + +func TestIsValidServiceId(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"Hello-1", true}, + {"Hello_2", true}, + {"hello-world", false}, // exceeds maxServiceIdLength + {"Hello@", false}, // contains invalid character '@' + {"HELLO", true}, + {"12345678", true}, // exactly maxServiceIdLength + {"123456789", false}, // exceeds maxServiceIdLength + {"Hello.World", false}, // contains invalid character '.' + {"", false}, // empty string + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result := IsValidServiceId(test.input) + if result != test.expected { + t.Errorf("For input %s, expected %v but got %v", test.input, test.expected, result) + } + }) + } +} From aa4bdc3db20f5aaa96c5ce0c1016760b87679513 Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Fri, 27 Oct 2023 13:18:19 -0700 Subject: [PATCH 05/28] [Code Health] Replace `mocks.go` w/ `.gitkeep` (#103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![Screenshot 2023-10-26 at 2 05 45 PM](https://github.com/pokt-network/poktroll/assets/1892194/7506e514-e3a7-4660-8a12-4496f7a0cc89) --- testutil/application/mocks/.gitkeep | 0 testutil/application/mocks/mocks.go | 6 ------ testutil/gateway/mocks/.gitkeep | 0 testutil/gateway/mocks/mocks.go | 3 --- testutil/supplier/mocks/.gitkeep | 0 testutil/supplier/mocks/mocks.go | 6 ------ x/application/client/cli/helpers_test.go | 4 ++-- 7 files changed, 2 insertions(+), 17 deletions(-) create mode 100644 testutil/application/mocks/.gitkeep delete mode 100644 testutil/application/mocks/mocks.go create mode 100644 testutil/gateway/mocks/.gitkeep delete mode 100644 testutil/gateway/mocks/mocks.go create mode 100644 testutil/supplier/mocks/.gitkeep delete mode 100644 testutil/supplier/mocks/mocks.go diff --git a/testutil/application/mocks/.gitkeep b/testutil/application/mocks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/testutil/application/mocks/mocks.go b/testutil/application/mocks/mocks.go deleted file mode 100644 index 4ccc3e251..000000000 --- a/testutil/application/mocks/mocks.go +++ /dev/null @@ -1,6 +0,0 @@ -package mocks - -// This file is in place to declare the package for dynamically generated structs. -// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. -// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go -// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests diff --git a/testutil/gateway/mocks/.gitkeep b/testutil/gateway/mocks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/testutil/gateway/mocks/mocks.go b/testutil/gateway/mocks/mocks.go deleted file mode 100644 index 16355b5a1..000000000 --- a/testutil/gateway/mocks/mocks.go +++ /dev/null @@ -1,3 +0,0 @@ -package mocks - -// This file is in place to declare the package for dynamically generated mocks diff --git a/testutil/supplier/mocks/.gitkeep b/testutil/supplier/mocks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/testutil/supplier/mocks/mocks.go b/testutil/supplier/mocks/mocks.go deleted file mode 100644 index 4ccc3e251..000000000 --- a/testutil/supplier/mocks/mocks.go +++ /dev/null @@ -1,6 +0,0 @@ -package mocks - -// This file is in place to declare the package for dynamically generated structs. -// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. -// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go -// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests diff --git a/x/application/client/cli/helpers_test.go b/x/application/client/cli/helpers_test.go index aa60d57ed..fb6bb5152 100644 --- a/x/application/client/cli/helpers_test.go +++ b/x/application/client/cli/helpers_test.go @@ -5,11 +5,11 @@ import ( "strconv" "testing" + "github.com/stretchr/testify/require" + "pocket/cmd/pocketd/cmd" "pocket/testutil/network" "pocket/x/application/types" - - "github.com/stretchr/testify/require" ) // Dummy variable to avoid unused import error. From f91bb788f00d82763cdad6386dfc93d1a26a1ba9 Mon Sep 17 00:00:00 2001 From: Redouane Lakrache Date: Sat, 28 Oct 2023 03:40:55 +0200 Subject: [PATCH 06/28] [Proxy] feat: add general proxy logic and server builder (#96) * feat: add general proxy logic and server builder * fix: make var names and comments more consistent * feat: add relay verification and signature * fix: interface assignment * chore: change native terms to proxied * fix: serviceId assignment --- pkg/relayer/proxy/errors.go | 8 +++ pkg/relayer/proxy/interface.go | 26 +++++++- pkg/relayer/proxy/jsonrpc.go | 85 ++++++++++++++++++++++++++ pkg/relayer/proxy/proxy.go | 95 +++++++++++++++++++---------- pkg/relayer/proxy/server_builder.go | 62 +++++++++++++++++++ proto/pocket/shared/supplier.proto | 1 - 6 files changed, 241 insertions(+), 36 deletions(-) create mode 100644 pkg/relayer/proxy/errors.go create mode 100644 pkg/relayer/proxy/jsonrpc.go create mode 100644 pkg/relayer/proxy/server_builder.go diff --git a/pkg/relayer/proxy/errors.go b/pkg/relayer/proxy/errors.go new file mode 100644 index 000000000..1aa42ab7e --- /dev/null +++ b/pkg/relayer/proxy/errors.go @@ -0,0 +1,8 @@ +package proxy + +import sdkerrors "cosmossdk.io/errors" + +var ( + codespace = "relayer/proxy" + ErrUnsupportedRPCType = sdkerrors.Register(codespace, 1, "unsupported rpc type") +) diff --git a/pkg/relayer/proxy/interface.go b/pkg/relayer/proxy/interface.go index 016b27057..3987c757c 100644 --- a/pkg/relayer/proxy/interface.go +++ b/pkg/relayer/proxy/interface.go @@ -5,22 +5,42 @@ import ( "pocket/pkg/observable" "pocket/x/service/types" + sharedtypes "pocket/x/shared/types" ) // RelayerProxy is the interface for the proxy that serves relays to the application. -// It is responsible for starting and stopping all supported proxies. +// It is responsible for starting and stopping all supported RelayServers. // While handling requests and responding in a closed loop, it also notifies // the miner about the relays that have been served. type RelayerProxy interface { - // Start starts all supported proxies and returns an error if any of them fail to start. + // Start starts all advertised relay servers and returns an error if any of them fail to start. Start(ctx context.Context) error - // Stop stops all supported proxies and returns an error if any of them fail. + // Stop stops all advertised relay servers and returns an error if any of them fail. Stop(ctx context.Context) error // ServedRelays returns an observable that notifies the miner about the relays that have been served. // A served relay is one whose RelayRequest's signature and session have been verified, // and its RelayResponse has been signed and successfully sent to the client. ServedRelays() observable.Observable[*types.Relay] + + // VerifyRelayRequest is a shared method used by RelayServers to check the + // relay request signature and session validity. + VerifyRelayRequest(relayRequest *types.RelayRequest) (isValid bool, err error) + + // SignRelayResponse is a shared method used by RelayServers to sign the relay response. + SignRelayResponse(relayResponse *types.RelayResponse) ([]byte, error) +} + +// RelayServer is the interface of the advertised relay servers provided by the RelayerProxy. +type RelayServer interface { + // Start starts the service server and returns an error if it fails. + Start(ctx context.Context) error + + // Stop terminates the service server and returns an error if it fails. + Stop(ctx context.Context) error + + // ServiceId returns the serviceId of the service. + ServiceId() *sharedtypes.ServiceId } diff --git a/pkg/relayer/proxy/jsonrpc.go b/pkg/relayer/proxy/jsonrpc.go new file mode 100644 index 000000000..3c105b429 --- /dev/null +++ b/pkg/relayer/proxy/jsonrpc.go @@ -0,0 +1,85 @@ +package proxy + +import ( + "context" + "net/http" + "net/url" + + "pocket/x/service/types" + sharedtypes "pocket/x/shared/types" +) + +var _ RelayServer = (*jsonRPCServer)(nil) + +type jsonRPCServer struct { + // serviceId is the id of the service that the server is responsible for. + serviceId *sharedtypes.ServiceId + + // serverEndpoint is the advertised endpoint configuration that the server uses to + // listen for incoming relay requests. + serverEndpoint *sharedtypes.SupplierEndpoint + + // proxiedServiceEndpoint is the address of the proxied service that the server relays requests to. + proxiedServiceEndpoint url.URL + + // server is the http server that listens for incoming relay requests. + server *http.Server + + // relayerProxy is the main relayer proxy that the server uses to perform its operations. + relayerProxy RelayerProxy + + // servedRelaysProducer is a channel that emits the relays that have been served so that the + // servedRelays observable can fan out the notifications to its subscribers. + servedRelaysProducer chan<- *types.Relay +} + +// NewJSONRPCServer creates a new HTTP server that listens for incoming relay requests +// and forwards them to the supported proxied service endpoint. +// It takes the serviceId, endpointUrl, and the main RelayerProxy as arguments and returns +// a RelayServer that listens to incoming RelayRequests +func NewJSONRPCServer( + serviceId *sharedtypes.ServiceId, + supplierEndpoint *sharedtypes.SupplierEndpoint, + proxiedServiceEndpoint url.URL, + servedRelaysProducer chan<- *types.Relay, + proxy RelayerProxy, +) RelayServer { + return &jsonRPCServer{ + serviceId: serviceId, + serverEndpoint: supplierEndpoint, + server: &http.Server{Addr: supplierEndpoint.Url}, + relayerProxy: proxy, + proxiedServiceEndpoint: proxiedServiceEndpoint, + servedRelaysProducer: servedRelaysProducer, + } +} + +// Start starts the service server and returns an error if it fails. +// It also waits for the passed in context to end before shutting down. +// This method is blocking and should be called in a goroutine. +func (j *jsonRPCServer) Start(ctx context.Context) error { + go func() { + <-ctx.Done() + j.server.Shutdown(ctx) + }() + + return j.server.ListenAndServe() +} + +// Stop terminates the service server and returns an error if it fails. +func (j *jsonRPCServer) Stop(ctx context.Context) error { + return j.server.Shutdown(ctx) +} + +// ServiceId returns the serviceId of the JSON-RPC service. +func (j *jsonRPCServer) ServiceId() *sharedtypes.ServiceId { + return j.serviceId +} + +// ServeHTTP listens for incoming relay requests. It implements the respective +// method of the http.Handler interface. It is called by http.ListenAndServe() +// when jsonRPCServer is used as an http.Handler with an http.Server. +// (see https://pkg.go.dev/net/http#Handler) +func (j *jsonRPCServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + panic("TODO: implement jsonRPCServer.ServeHTTP") +} diff --git a/pkg/relayer/proxy/proxy.go b/pkg/relayer/proxy/proxy.go index f626f184f..2e14459a3 100644 --- a/pkg/relayer/proxy/proxy.go +++ b/pkg/relayer/proxy/proxy.go @@ -2,10 +2,12 @@ package proxy import ( "context" + "net/url" sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/crypto/keyring" accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "golang.org/x/sync/errgroup" // TODO_INCOMPLETE(@red-0ne): Import the appropriate block client interface once available. // blocktypes "pocket/pkg/client" @@ -18,6 +20,12 @@ import ( var _ RelayerProxy = (*relayerProxy)(nil) +type ( + serviceId = string + relayServersMap = map[serviceId][]RelayServer + servicesEndpointsMap = map[serviceId]url.URL +) + type relayerProxy struct { // keyName is the supplier's key name in the Cosmos's keybase. It is used along with the keyring to // get the supplier address and sign the relay responses. @@ -41,10 +49,13 @@ type relayerProxy struct { // which is needed to check if the relay proxy should be serving an incoming relay request. sessionQuerier sessiontypes.QueryClient - // providedServices is a map of the services provided by the relayer proxy. Each provided service + // 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 proxies the request to the supported native service. - providedServices map[string][]*ProvidedService + // the client that relays the request to the supported proxied service. + advertisedRelayServers relayServersMap + + // proxiedServicesEndpoints is a map of the proxied services endpoints that the relayer proxy supports. + proxiedServicesEndpoints servicesEndpointsMap // servedRelays is an observable that notifies the miner about the relays that have been served. servedRelays observable.Observable[*types.Relay] @@ -59,60 +70,80 @@ func NewRelayerProxy( clientCtx sdkclient.Context, keyName string, keyring keyring.Keyring, - + proxiedServicesEndpoints servicesEndpointsMap, // TODO_INCOMPLETE(@red-0ne): Uncomment once the BlockClient interface is available. // blockClient blocktypes.BlockClient, ) RelayerProxy { accountQuerier := accounttypes.NewQueryClient(clientCtx) supplierQuerier := suppliertypes.NewQueryClient(clientCtx) sessionQuerier := sessiontypes.NewQueryClient(clientCtx) - providedServices := buildProvidedServices(ctx, supplierQuerier) servedRelays, servedRelaysProducer := channel.NewObservable[*types.Relay]() return &relayerProxy{ // TODO_INCOMPLETE(@red-0ne): Uncomment once the BlockClient interface is available. - // blockClient: blockClient, - keyName: keyName, - keyring: keyring, - accountsQuerier: accountQuerier, - supplierQuerier: supplierQuerier, - sessionQuerier: sessionQuerier, - providedServices: providedServices, - servedRelays: servedRelays, - servedRelaysProducer: servedRelaysProducer, + // blockClient: blockClient, + keyName: keyName, + keyring: keyring, + accountsQuerier: accountQuerier, + supplierQuerier: supplierQuerier, + sessionQuerier: sessionQuerier, + proxiedServicesEndpoints: proxiedServicesEndpoints, + servedRelays: servedRelays, + servedRelaysProducer: servedRelaysProducer, } } -// Start starts all supported proxies and returns an error if any of them fail to start. +// Start concurrently starts all advertised relay servers and returns an error if any of them fails to start. +// This method is blocking until all RelayServers are started. func (rp *relayerProxy) Start(ctx context.Context) error { - panic("TODO: implement relayerProxy.Start") + // The provided services map is built from the supplier's on-chain advertised information, + // which is a runtime parameter that can be changed by the supplier. + // NOTE: We build the provided services map at Start instead of NewRelayerProxy to avoid having to + // return an error from the constructor. + if err := rp.BuildProvidedServices(ctx); err != nil { + return err + } + + 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) }) + } + } + + return startGroup.Wait() } -// Stop stops all supported proxies and returns an error if any of them fail. +// Stop concurrently stops all advertised relay servers and returns an error if any of them fails. +// This method is blocking until all RelayServers are stopped. func (rp *relayerProxy) Stop(ctx context.Context) error { - panic("TODO: implement relayerProxy.Stop") + 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) }) + } + } + + return stopGroup.Wait() } // ServedRelays returns an observable that notifies the miner about the relays that have been served. // A served relay is one whose RelayRequest's signature and session have been verified, // and its RelayResponse has been signed and successfully sent to the client. func (rp *relayerProxy) ServedRelays() observable.Observable[*types.Relay] { - panic("TODO: implement relayerProxy.ServedRelays") + return rp.servedRelays } -// buildProvidedServices builds the provided services map from the supplier's advertised information. -// It loops over the retrieved `SupplierServiceConfig` and, for each `SupplierEndpoint`, it creates the necessary -// server and client to populate the corresponding `ProvidedService` struct in the map. -func buildProvidedServices( - ctx context.Context, - supplierQuerier suppliertypes.QueryClient, -) map[string][]*ProvidedService { - panic("TODO: implement buildProvidedServices") +// VerifyRelayRequest is a shared method used by RelayServers to check the relay request signature and session validity. +func (rp *relayerProxy) VerifyRelayRequest(relayRequest *types.RelayRequest) (isValid bool, err error) { + panic("TODO: implement relayerProxy.VerifyRelayRequest") } -// TODO_INCOMPLETE(@red-0ne): Add the appropriate server and client interfaces to be implemented by each RPC type. -type ProvidedService struct { - serviceId string - server struct{} - client struct{} +// SignRelayResponse is a shared method used by RelayServers to sign the relay response. +func (rp *relayerProxy) SignRelayResponse(relayResponse *types.RelayResponse) ([]byte, error) { + panic("TODO: implement relayerProxy.SignRelayResponse") } diff --git a/pkg/relayer/proxy/server_builder.go b/pkg/relayer/proxy/server_builder.go new file mode 100644 index 000000000..492a4bcd6 --- /dev/null +++ b/pkg/relayer/proxy/server_builder.go @@ -0,0 +1,62 @@ +package proxy + +import ( + "context" + + sharedtypes "pocket/x/shared/types" + suppliertypes "pocket/x/supplier/types" +) + +// BuildProvidedServices builds the advertised relay servers from the supplier's on-chain advertised services. +// It populates the relayerProxy's `advertisedRelayServers` map of servers for each service, where each server +// is responsible for listening for incoming relay requests and relaying them to the supported proxied service. +func (rp *relayerProxy) BuildProvidedServices(ctx context.Context) error { + // Get the supplier address from the keyring + supplierAddress, err := rp.keyring.Key(rp.keyName) + if err != nil { + return err + } + + // Get the supplier's advertised information from the blockchain + supplierQuery := &suppliertypes.QueryGetSupplierRequest{Address: supplierAddress.String()} + supplierQueryResponse, err := rp.supplierQuerier.Supplier(ctx, supplierQuery) + if err != nil { + return err + } + + services := supplierQueryResponse.Supplier.Services + + // Build the advertised relay servers map. For each service's endpoint, create the appropriate RelayServer. + providedServices := make(relayServersMap) + for _, serviceConfig := range services { + serviceId := serviceConfig.ServiceId + proxiedServicesEndpoints := rp.proxiedServicesEndpoints[serviceId.Id] + serviceEndpoints := make([]RelayServer, len(serviceConfig.Endpoints)) + + for _, endpoint := range serviceConfig.Endpoints { + var server RelayServer + + // Switch to the RPC type to create the appropriate RelayServer + switch endpoint.RpcType { + case sharedtypes.RPCType_JSON_RPC: + server = NewJSONRPCServer( + serviceId, + endpoint, + proxiedServicesEndpoints, + rp.servedRelaysProducer, + rp, + ) + default: + return ErrUnsupportedRPCType + } + + serviceEndpoints = append(serviceEndpoints, server) + } + + providedServices[serviceId.Id] = serviceEndpoints + } + + rp.advertisedRelayServers = providedServices + + return nil +} diff --git a/proto/pocket/shared/supplier.proto b/proto/pocket/shared/supplier.proto index 1aa626b16..f16cd9a11 100644 --- a/proto/pocket/shared/supplier.proto +++ b/proto/pocket/shared/supplier.proto @@ -16,4 +16,3 @@ message Supplier { cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the supplier has staked repeated SupplierServiceConfig services = 3; // The service configs this supplier can support } - From 8791c17a176579ed3ed7dc53929f9fe298da53f0 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Mon, 30 Oct 2023 18:04:30 +0100 Subject: [PATCH 07/28] [Miner] feat: add replay observable (#93) * feat: add replay observable (cherry picked from commit ab21790164ab544ae5f1508d3237a3faab33e71e) * refactor: `replayObservable` as its own interface type * refactor: `replayObservable#Next() V` to `ReplayObservable#Last(ctx, n) []V` * chore: add constructor func for `ReplayObservable` * test: reorder to improve readibility * refactor: rename and add godoc comments * chore: improve naming & comments * chore: add warning log and improve comments * test: improve and add tests * fix: interface assertion * fix: comment typo * chore: review improvements * fix: race * refactor: add observableInternals interface (cherry picked from commit 5d149e5297ce7d11dad77983f53be53efd8dae15) * chore: update last; only block for 1 value min (cherry picked from commit b24a5e586e9c776a962008043d065a2294fd921c) * chore: review improvements * refactor: move add `channelObservableInternals` & migrate its relevant methods & state from channelObservable * refactor: simplify, cleanup, & improve comments * chore: review improvements * fix: bug in `accumulateReplayValues()` * chore: review feedback improvements Co-authored-by: Daniel Olshansky * fix: use american spelling of cancelation & canceled --------- Co-authored-by: Daniel Olshansky --- pkg/observable/channel/observable.go | 134 ++---------- pkg/observable/channel/observable_test.go | 2 +- pkg/observable/channel/observer.go | 9 +- pkg/observable/channel/observer_manager.go | 152 +++++++++++++ pkg/observable/channel/observer_test.go | 25 ++- pkg/observable/channel/replay.go | 237 +++++++++++++++++++++ pkg/observable/channel/replay_test.go | 228 ++++++++++++++++++++ pkg/observable/interface.go | 10 +- 8 files changed, 669 insertions(+), 128 deletions(-) create mode 100644 pkg/observable/channel/observer_manager.go create mode 100644 pkg/observable/channel/replay.go create mode 100644 pkg/observable/channel/replay_test.go diff --git a/pkg/observable/channel/observable.go b/pkg/observable/channel/observable.go index 26958a75b..6c92d29d3 100644 --- a/pkg/observable/channel/observable.go +++ b/pkg/observable/channel/observable.go @@ -2,8 +2,6 @@ package channel import ( "context" - "sync" - "pocket/pkg/observable" ) @@ -13,7 +11,10 @@ import ( // defaultSubscribeBufferSize is the buffer size of a observable's publish channel. const defaultPublishBufferSize = 50 -var _ observable.Observable[any] = (*channelObservable[any])(nil) +var ( + _ observable.Observable[any] = (*channelObservable[any])(nil) + _ observerManager[any] = (*channelObservable[any])(nil) +) // option is a function which receives and can modify the channelObservable state. type option[V any] func(obs *channelObservable[V]) @@ -21,14 +22,14 @@ type option[V any] func(obs *channelObservable[V]) // channelObservable implements the observable.Observable interface and can be notified // by sending on its corresponding publishCh channel. type channelObservable[V any] struct { + // embed observerManager to encapsulate concurrent-safe read/write access to + // observers. This also allows higher-level objects to wrap this observable + // without knowing its specific type by asserting that it implements the + // observerManager interface. + observerManager[V] // publishCh is an observable-wide channel that is used to receive values // which are subsequently fanned out to observers. publishCh chan V - // observersMu protects observers from concurrent access/updates - observersMu *sync.RWMutex - // observers is a list of channelObservers that will be notified when publishCh - // receives a new value. - observers []*channelObserver[V] } // NewObservable creates a new observable which is notified when the publishCh @@ -36,8 +37,7 @@ type channelObservable[V any] struct { func NewObservable[V any](opts ...option[V]) (observable.Observable[V], chan<- V) { // initialize an observable that publishes messages from 1 publishCh to N observers obs := &channelObservable[V]{ - observersMu: &sync.RWMutex{}, - observers: []*channelObserver[V]{}, + observerManager: newObserverManager[V](), } for _, opt := range opts { @@ -64,125 +64,37 @@ func WithPublisher[V any](publishCh chan V) option[V] { } } -// Next synchronously returns the next value from the observable. -func (obsvbl *channelObservable[V]) Next(ctx context.Context) V { - tempObserver := obsvbl.Subscribe(ctx) - defer tempObserver.Unsubscribe() - - val := <-tempObserver.Ch() - return val -} - // Subscribe returns an observer which is notified when the publishCh channel // receives a value. -func (obsvbl *channelObservable[V]) Subscribe(ctx context.Context) observable.Observer[V] { - // must (write) lock observersMu so that we can safely append to the observers list - obsvbl.observersMu.Lock() - defer obsvbl.observersMu.Unlock() - - observer := NewObserver[V](ctx, obsvbl.onUnsubscribe) - obsvbl.observers = append(obsvbl.observers, observer) +func (obs *channelObservable[V]) Subscribe(ctx context.Context) observable.Observer[V] { + // Create a new observer and add it to the list of observers to be notified + // when publishCh receives a new value. + observer := NewObserver[V](ctx, obs.observerManager.remove) + obs.observerManager.add(observer) - // caller can rely on context cancellation or call UnsubscribeAll() to unsubscribe + // caller can rely on context cancelation or call UnsubscribeAll() to unsubscribe // active observers if ctx != nil { // asynchronously wait for the context to be done and then unsubscribe // this observer. - go goUnsubscribeOnDone[V](ctx, observer) + go obs.observerManager.goUnsubscribeOnDone(ctx, observer) } return observer } // UnsubscribeAll unsubscribes and removes all observers from the observable. -func (obsvbl *channelObservable[V]) UnsubscribeAll() { - obsvbl.unsubscribeAll() -} - -// unsubscribeAll unsubscribes and removes all observers from the observable. -func (obsvbl *channelObservable[V]) unsubscribeAll() { - // Copy currentObservers to avoid holding the lock while unsubscribing them. - // The observers at the time of locking, prior to copying, are the canonical - // set of observers which are unsubscribed. - // New or existing Observers may (un)subscribe while the observable is closing. - // Any such observers won't be isClosed but will also stop receiving notifications - // immediately (if they receive any at all). - currentObservers := obsvbl.copyObservers() - for _, observer := range currentObservers { - observer.Unsubscribe() - } - - // Reset observers to an empty list. This purges any observers which might have - // subscribed while the observable was closing. - obsvbl.observersMu.Lock() - obsvbl.observers = []*channelObserver[V]{} - obsvbl.observersMu.Unlock() +func (obs *channelObservable[V]) UnsubscribeAll() { + obs.observerManager.removeAll() } // goPublish to the publishCh and notify observers when values are received. // This function is blocking and should be run in a goroutine. -func (obsvbl *channelObservable[V]) goPublish() { - for notification := range obsvbl.publishCh { - // Copy currentObservers to avoid holding the lock while notifying them. - // New or existing Observers may (un)subscribe while this notification - // is being fanned out. - // The observers at the time of locking, prior to copying, are the canonical - // set of observers which receive this notification. - currentObservers := obsvbl.copyObservers() - for _, obsvr := range currentObservers { - // TODO_CONSIDERATION: perhaps continue trying to avoid making this - // notification async as it would effectively use goroutines - // in memory as a buffer (unbounded). - obsvr.notify(notification) - } +func (obs *channelObservable[V]) goPublish() { + for notification := range obs.publishCh { + obs.observerManager.notifyAll(notification) } // Here we know that the publisher channel has been closed. // Unsubscribe all observers as they can no longer receive notifications. - obsvbl.unsubscribeAll() -} - -// copyObservers returns a copy of the current observers list. It is safe to -// call concurrently. -func (obsvbl *channelObservable[V]) copyObservers() (observers []*channelObserver[V]) { - defer obsvbl.observersMu.RUnlock() - - // This loop blocks on acquiring a read lock on observersMu. If TryRLock - // fails, the loop continues until it succeeds. This is intended to give - // callers a guarantee that this copy operation won't contribute to a deadlock. - for { - // block until a read lock can be acquired - if obsvbl.observersMu.TryRLock() { - break - } - } - - observers = make([]*channelObserver[V], len(obsvbl.observers)) - copy(observers, obsvbl.observers) - - return observers -} - -// goUnsubscribeOnDone unsubscribes from the subscription when the context is done. -// It is a blocking function and intended to be called in a goroutine. -func goUnsubscribeOnDone[V any](ctx context.Context, observer observable.Observer[V]) { - <-ctx.Done() - if observer.IsClosed() { - return - } - observer.Unsubscribe() -} - -// onUnsubscribe returns a function that removes a given observer from the -// observable's list of observers. -func (obsvbl *channelObservable[V]) onUnsubscribe(toRemove *channelObserver[V]) { - // must (write) lock to iterate over and modify the observers list - obsvbl.observersMu.Lock() - defer obsvbl.observersMu.Unlock() - - for i, observer := range obsvbl.observers { - if observer == toRemove { - obsvbl.observers = append((obsvbl.observers)[:i], (obsvbl.observers)[i+1:]...) - break - } - } + obs.observerManager.removeAll() } diff --git a/pkg/observable/channel/observable_test.go b/pkg/observable/channel/observable_test.go index 6ec301cfa..e94679630 100644 --- a/pkg/observable/channel/observable_test.go +++ b/pkg/observable/channel/observable_test.go @@ -337,7 +337,7 @@ func TestChannelObservable_SequentialPublishAndUnsubscription(t *testing.T) { // TODO_TECHDEBT/TODO_INCOMPLETE: add coverage for active observers closing when publishCh closes. func TestChannelObservable_ObserversCloseOnPublishChannelClose(t *testing.T) { - t.Skip("add coverage: all observers should unsubscribeAll when publishCh closes") + t.Skip("add coverage: all observers should unsubscribe when publishCh closes") } func delayedPublishFactory[V any](publishCh chan<- V, delay time.Duration) func(value V) { diff --git a/pkg/observable/channel/observer.go b/pkg/observable/channel/observer.go index 3a2455e64..a989b2092 100644 --- a/pkg/observable/channel/observer.go +++ b/pkg/observable/channel/observer.go @@ -29,9 +29,10 @@ var _ observable.Observer[any] = (*channelObserver[any])(nil) // channelObserver implements the observable.Observer interface. type channelObserver[V any] struct { ctx context.Context - // onUnsubscribe is called in Observer#Unsubscribe, removing the respective - // observer from observers in a concurrency-safe manner. - onUnsubscribe func(toRemove *channelObserver[V]) + // onUnsubscribe is called in Observer#Unsubscribe, closing this observer's + // channel and removing it from the respective obervable's observers list + // in a concurrency-safe manner. + onUnsubscribe func(toRemove observable.Observer[V]) // observerMu protects the observerCh and isClosed fields. observerMu *sync.RWMutex // observerCh is the channel that is used to emit values to the observer. @@ -43,7 +44,7 @@ type channelObserver[V any] struct { isClosed bool } -type UnsubscribeFunc[V any] func(toRemove *channelObserver[V]) +type UnsubscribeFunc[V any] func(toRemove observable.Observer[V]) func NewObserver[V any]( ctx context.Context, diff --git a/pkg/observable/channel/observer_manager.go b/pkg/observable/channel/observer_manager.go new file mode 100644 index 000000000..44807c047 --- /dev/null +++ b/pkg/observable/channel/observer_manager.go @@ -0,0 +1,152 @@ +package channel + +import ( + "context" + "sync" + + "pocket/pkg/observable" +) + +var _ observerManager[any] = (*channelObserverManager[any])(nil) + +// observerManager is an interface intended to be used between an observable and some +// higher-level abstraction and/or observable implementation which would embed it. +// Embedding this interface rather than a channelObservable directly allows for +// more transparency and flexibility in higher-level code. +// NOTE: this interface MUST be used with a common concrete Observer type. +// TODO_CONSIDERATION: Consider whether `observerManager` and `Observable` should remain as separate +// types after some more time and experience using both. +type observerManager[V any] interface { + notifyAll(notification V) + add(toAdd observable.Observer[V]) + remove(toRemove observable.Observer[V]) + removeAll() + goUnsubscribeOnDone(ctx context.Context, observer observable.Observer[V]) +} + +// TODO_CONSIDERATION: if this were a generic implementation, we wouldn't need +// to cast `toAdd` to a channelObserver in add. There are two things +// currently preventing a generic observerManager implementation: +// 1. channelObserver#notify() is not part of the observable.Observer interface +// and is therefore not accessible here. If we move everything into the +// `observable` pkg so that the unexported member is in scope, then the channel +// pkg can't implement it for the same reason, it's an unexported method defined +// in a different pkg. +// 2. == is not defined for a generic Observer type. We would have to add an Equals() +// to the Observer interface. + +// channelObserverManager implements the observerManager interface using +// channelObservers. +type channelObserverManager[V any] struct { + // observersMu protects observers from concurrent access/updates + observersMu *sync.RWMutex + // observers is a list of channelObservers that will be notified when new value + // are received. + observers []*channelObserver[V] +} + +func newObserverManager[V any]() *channelObserverManager[V] { + return &channelObserverManager[V]{ + observersMu: &sync.RWMutex{}, + observers: make([]*channelObserver[V], 0), + } +} + +func (com *channelObserverManager[V]) notifyAll(notification V) { + // Copy currentObservers to avoid holding the lock while notifying them. + // New or existing Observers may (un)subscribe while this notification + // is being fanned out. + // The observers at the time of locking, prior to copying, are the canonical + // set of observers which receive this notification. + currentObservers := com.copyObservers() + for _, obsvr := range currentObservers { + // TODO_TECHDEBT: since this synchronously notifies all observers in a loop, + // it is possible to block here, part-way through notifying all observers, + // on a slow observer consumer (i.e. full buffer). Instead, we should notify + // observers with some limited concurrency of "worker" goroutines. + // The storj/common repo contains such a `Limiter` implementation, see: + // https://github.com/storj/common/blob/main/sync2/limiter.go. + obsvr.notify(notification) + } +} + +// addObserver implements the respective member of observerManager. It is used +// by the channelObservable implementation as well as embedders of observerManager +// (e.g. replayObservable). +// It panics if toAdd is not a channelObserver. +func (com *channelObserverManager[V]) add(toAdd observable.Observer[V]) { + // must (write) lock observersMu so that we can safely append to the observers list + com.observersMu.Lock() + defer com.observersMu.Unlock() + + com.observers = append(com.observers, toAdd.(*channelObserver[V])) +} + +// remove removes a given observer from the observable's list of observers. +// It implements the respective member of observerManager and is used by +// the channelObservable implementation as well as embedders of observerManager +// (e.g. replayObservable). +func (com *channelObserverManager[V]) remove(toRemove observable.Observer[V]) { + // must (write) lock to iterate over and modify the observers list + com.observersMu.Lock() + defer com.observersMu.Unlock() + + for i, observer := range com.observers { + if observer == toRemove { + com.observers = append((com.observers)[:i], (com.observers)[i+1:]...) + break + } + } +} + +// removeAll unsubscribes and removes all observers from the observable. +// It implements the respective member of observerManager and is used by +// the channelObservable implementation as well as embedders of observerManager +// (e.g. replayObservable). +func (com *channelObserverManager[V]) removeAll() { + // Copy currentObservers to avoid holding the lock while unsubscribing them. + // The observers at the time of locking, prior to copying, are the canonical + // set of observers which are unsubscribed. + // New or existing Observers may (un)subscribe while the observable is closing. + // Any such observers won't be isClosed but will also stop receiving notifications + // immediately (if they receive any at all). + currentObservers := com.copyObservers() + for _, observer := range currentObservers { + observer.Unsubscribe() + } + + // Reset observers to an empty list. This purges any observers which might have + // subscribed while the observable was closing. + com.observersMu.Lock() + com.observers = []*channelObserver[V]{} + com.observersMu.Unlock() +} + +// goUnsubscribeOnDone unsubscribes from the subscription when the context is done. +// It is a blocking function and intended to be called in a goroutine. +func (com *channelObserverManager[V]) goUnsubscribeOnDone( + ctx context.Context, + observer observable.Observer[V], +) { + <-ctx.Done() + if observer.IsClosed() { + return + } + observer.Unsubscribe() +} + +// copyObservers returns a copy of the current observers list. It is safe to +// call concurrently. Notably, it is not part of the observerManager interface. +func (com *channelObserverManager[V]) copyObservers() (observers []*channelObserver[V]) { + defer com.observersMu.RUnlock() + + // This loop blocks on acquiring a read lock on observersMu. If TryRLock + // fails, the loop continues until it succeeds. This is intended to give + // callers a guarantee that this copy operation won't contribute to a deadlock. + com.observersMu.RLock() + + observers = make([]*channelObserver[V], len(com.observers)) + copy(observers, com.observers) + + return observers +} diff --git a/pkg/observable/channel/observer_test.go b/pkg/observable/channel/observer_test.go index f8730a422..ccda5c66c 100644 --- a/pkg/observable/channel/observer_test.go +++ b/pkg/observable/channel/observer_test.go @@ -7,20 +7,23 @@ import ( "time" "github.com/stretchr/testify/require" + + "pocket/pkg/observable" ) func TestObserver_Unsubscribe(t *testing.T) { var ( - onUnsubscribeCalled = false publishCh = make(chan int, 1) + onUnsubscribeCalled = false + onUnsubscribe = func(toRemove observable.Observer[int]) { + onUnsubscribeCalled = true + } ) obsvr := &channelObserver[int]{ observerMu: &sync.RWMutex{}, // using a buffered channel to keep the test synchronous - observerCh: publishCh, - onUnsubscribe: func(toRemove *channelObserver[int]) { - onUnsubscribeCalled = true - }, + observerCh: publishCh, + onUnsubscribe: onUnsubscribe, } // should initially be open @@ -37,17 +40,19 @@ func TestObserver_Unsubscribe(t *testing.T) { func TestObserver_ConcurrentUnsubscribe(t *testing.T) { var ( - onUnsubscribeCalled = false publishCh = make(chan int, 1) + onUnsubscribeCalled = false + onUnsubscribe = func(toRemove observable.Observer[int]) { + onUnsubscribeCalled = true + } ) + obsvr := &channelObserver[int]{ ctx: context.Background(), observerMu: &sync.RWMutex{}, // using a buffered channel to keep the test synchronous - observerCh: publishCh, - onUnsubscribe: func(toRemove *channelObserver[int]) { - onUnsubscribeCalled = true - }, + observerCh: publishCh, + onUnsubscribe: onUnsubscribe, } require.Equal(t, false, obsvr.isClosed, "observer channel should initially be open") diff --git a/pkg/observable/channel/replay.go b/pkg/observable/channel/replay.go new file mode 100644 index 000000000..b7c54f877 --- /dev/null +++ b/pkg/observable/channel/replay.go @@ -0,0 +1,237 @@ +package channel + +import ( + "context" + "log" + "sync" + "time" + + "pocket/pkg/observable" +) + +// replayPartialBufferTimeout is the duration to wait for the replay buffer to +// accumulate at least 1 value before returning the accumulated values. +// TODO_CONSIDERATION: perhaps this should be parameterized. +const replayPartialBufferTimeout = 100 * time.Millisecond + +var _ observable.ReplayObservable[any] = (*replayObservable[any])(nil) + +type replayObservable[V any] struct { + // embed observerManager to encapsulate concurrent-safe read/write access to + // observers. This also allows higher-level objects to wrap this observable + // without knowing its specific type by asserting that it implements the + // observerManager interface. + observerManager[V] + // replayBufferSize is the number of notifications to buffer so that they + // can be replayed to new observers. + replayBufferSize int + // replayBufferMu protects replayBuffer from concurrent access/updates. + replayBufferMu sync.RWMutex + // replayBuffer holds the last relayBufferSize number of notifications received + // by this observable. This buffer is replayed to new observers, on subscribing, + // prior to any new notifications being propagated. + replayBuffer []V +} + +// NewReplayObservable returns a new ReplayObservable with the given replay buffer +// replayBufferSize and the corresponding publish channel to notify it of new values. +func NewReplayObservable[V any]( + ctx context.Context, + replayBufferSize int, +) (observable.ReplayObservable[V], chan<- V) { + obsvbl, publishCh := NewObservable[V]() + return ToReplayObservable[V](ctx, replayBufferSize, obsvbl), publishCh +} + +// ToReplayObservable returns an observable which replays the last replayBufferSize +// number of values published to the source observable to new observers, before +// publishing new values. +// It panics if srcObservable does not implement the observerManager interface. +// It should only be used with a srcObservable which contains channelObservers +// (i.e. channelObservable or similar). +func ToReplayObservable[V any]( + ctx context.Context, + replayBufferSize int, + srcObsvbl observable.Observable[V], +) observable.ReplayObservable[V] { + // Assert that the source observable implements the observerMngr required + // to embed and wrap it. + observerMngr := srcObsvbl.(observerManager[V]) + + replayObsvbl := &replayObservable[V]{ + observerManager: observerMngr, + replayBufferSize: replayBufferSize, + replayBuffer: make([]V, 0, replayBufferSize), + } + + srcObserver := srcObsvbl.Subscribe(ctx) + go replayObsvbl.goBufferReplayNotifications(srcObserver) + + return replayObsvbl +} + +// Last synchronously returns the last n values from the replay buffer. It blocks +// until at least 1 notification has been accumulated, then waits replayPartialBufferTimeout +// duration before returning all notifications accumulated notifications by that time. +// If the replay buffer contains at least n notifications, this function will only +// block as long as it takes to accumulate and return them. +// If n is greater than the replay buffer size, the entire replay buffer is returned. +func (ro *replayObservable[V]) Last(ctx context.Context, n int) []V { + // Use a temporary observer to accumulate replay values. + // Subscribe will always start with the replay buffer, so we can safely + // leverage it here for syncrhonization (i.e. blocking until at least 1 + // notification has been accumulated). This also eliminates the need for + // locking and/or copying the replay buffer. + tempObserver := ro.Subscribe(ctx) + defer tempObserver.Unsubscribe() + + // If n is greater than the replay buffer size, return the entire replay buffer. + if n > ro.replayBufferSize { + n = ro.replayBufferSize + log.Printf( + "WARN: requested replay buffer size %d is greater than replay buffer capacity %d; returning entire replay buffer", + n, cap(ro.replayBuffer), + ) + } + + // accumulateReplayValues works concurrently and returns a context and cancelation + // function for signaling completion. + return accumulateReplayValues(tempObserver, n) +} + +// Subscribe returns an observer which is notified when the publishCh channel +// receives a value. +func (ro *replayObservable[V]) Subscribe(ctx context.Context) observable.Observer[V] { + ro.replayBufferMu.RLock() + defer ro.replayBufferMu.RUnlock() + + observer := NewObserver[V](ctx, ro.observerManager.remove) + + // Replay all buffered replayBuffer to the observer channel buffer before + // any new values have an opportunity to send on observerCh (i.e. appending + // observer to ro.observers). + // + // TODO_IMPROVE: this assumes that the observer channel buffer is large enough + // to hold all replay (buffered) notifications. + for _, notification := range ro.replayBuffer { + observer.notify(notification) + } + + ro.observerManager.add(observer) + + // caller can rely on context cancelation or call UnsubscribeAll() to unsubscribe + // active observers + if ctx != nil { + // asynchronously wait for the context to be done and then unsubscribe + // this observer. + go ro.observerManager.goUnsubscribeOnDone(ctx, observer) + } + + return observer +} + +// UnsubscribeAll unsubscribes and removes all observers from the observable. +func (ro *replayObservable[V]) UnsubscribeAll() { + ro.observerManager.removeAll() +} + +// goBufferReplayNotifications buffers the last n notifications from a source +// observer. It is intended to be run in a goroutine. +func (ro *replayObservable[V]) goBufferReplayNotifications(srcObserver observable.Observer[V]) { + for notification := range srcObserver.Ch() { + ro.replayBufferMu.Lock() + // Add the notification to the buffer. + if len(ro.replayBuffer) < ro.replayBufferSize { + ro.replayBuffer = append(ro.replayBuffer, notification) + } else { + // buffer full, make room for the new notification by removing the + // oldest notification. + ro.replayBuffer = append(ro.replayBuffer[1:], notification) + } + ro.replayBufferMu.Unlock() + } +} + +// accumulateReplayValues synchronously (but concurrently) accumulates n values +// from the observer channel into the slice pointed to by accValues and then returns +// said slice. It cancels the context either when n values have been accumulated +// or when at least 1 value has been accumulated and replayPartialBufferTimeout +// has elapsed. +func accumulateReplayValues[V any](observer observable.Observer[V], n int) []V { + var ( + // accValuesMu protects accValues from concurrent access. + accValuesMu sync.Mutex + // Accumulate replay values in a new slice to avoid (read) locking replayBufferMu. + accValues = new([]V) + // canceling the context will cause the loop in the goroutine to exit. + ctx, cancel = context.WithCancel(context.Background()) + ) + + // Concurrently accumulate n values from the observer channel. + go func() { + // Defer canceling the context and unlocking accValuesMu. The function + // assumes that the mutex is locked when it gets execution control back + // from the loop. + defer func() { + cancel() + accValuesMu.Unlock() + }() + for { + // Lock the mutex to read accValues here and potentially write in + // the first case branch in the select below. + accValuesMu.Lock() + + // The context was canceled since the last iteration. + if ctx.Err() != nil { + return + } + + // We've accumulated n values. + if len(*accValues) >= n { + return + } + + // Receive from the observer's channel if we can, otherwise let + // the loop run. + select { + // Receiving from the observer channel blocks if replayBuffer is empty. + case value, ok := <-observer.Ch(): + // tempObserver was closed concurrently. + if !ok { + return + } + + // Update the accumulated values pointed to by accValues. + *accValues = append(*accValues, value) + default: + // If we can't receive from the observer channel immediately, + // let the loop run. + } + + // Unlock accValuesMu so that the select below gets a chance to check + // the length of *accValues to decide whether to cancel, and it can + // be relocked at the top of the loop as it must be locked when the + // loop exits. + accValuesMu.Unlock() + // Wait a tick before continuing the loop. + time.Sleep(time.Millisecond) + } + }() + + // Wait for N values to be accumulated or timeout. When timing out, if we + // have at least 1 value, we can return it. Otherwise, we need to wait for + // the next value to be published (i.e. continue the loop). + for { + select { + case <-ctx.Done(): + return *accValues + case <-time.After(replayPartialBufferTimeout): + accValuesMu.Lock() + if len(*accValues) > 1 { + cancel() + return *accValues + } + accValuesMu.Unlock() + } + } +} diff --git a/pkg/observable/channel/replay_test.go b/pkg/observable/channel/replay_test.go new file mode 100644 index 000000000..a34fb0f92 --- /dev/null +++ b/pkg/observable/channel/replay_test.go @@ -0,0 +1,228 @@ +package channel_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "pocket/internal/testerrors" + "pocket/pkg/observable/channel" +) + +func TestReplayObservable(t *testing.T) { + var ( + replayBufferSize = 3 + values = []int{1, 2, 3, 4, 5} + // the replay buffer is full and has shifted out values with index < + // len(values)-replayBufferSize so Last should return values starting + // from there. + expectedValues = values[len(values)-replayBufferSize:] + errCh = make(chan error, 1) + ctx, cancel = context.WithCancel(context.Background()) + ) + t.Cleanup(cancel) + + // NB: intentionally not using NewReplayObservable() to test ToReplayObservable() directly + // and to retain a reference to the wrapped observable for testing. + obsvbl, publishCh := channel.NewObservable[int]() + replayObsvbl := channel.ToReplayObservable[int](ctx, replayBufferSize, obsvbl) + + // vanilla observer, should be able to receive all values published after subscribing + observer := obsvbl.Subscribe(ctx) + go func() { + for _, expected := range values { + select { + case v := <-observer.Ch(): + if !assert.Equal(t, expected, v) { + errCh <- testerrors.ErrAsync + return + } + case <-time.After(1 * time.Second): + t.Errorf("Did not receive expected value %d in time", expected) + errCh <- testerrors.ErrAsync + return + } + } + }() + + // send all values to the observable's publish channel + for _, value := range values { + publishCh <- value + } + + // allow some time for values to be buffered by the replay observable + time.Sleep(time.Millisecond) + + // replay observer, should receive the last lastN values published prior to + // subscribing followed by subsequently published values + replayObserver := replayObsvbl.Subscribe(ctx) + for _, expected := range expectedValues { + select { + case v := <-replayObserver.Ch(): + require.Equal(t, expected, v) + case <-time.After(1 * time.Second): + t.Fatalf("Did not receive expected value %d in time", expected) + } + } + + // second replay observer, should receive the same values as the first + // even though it subscribed after all values were published and the + // values were already replayed by the first. + replayObserver2 := replayObsvbl.Subscribe(ctx) + for _, expected := range expectedValues { + select { + case v := <-replayObserver2.Ch(): + require.Equal(t, expected, v) + case <-time.After(1 * time.Second): + t.Fatalf("Did not receive expected value %d in time", expected) + } + } +} + +func TestReplayObservable_Last_Full_ReplayBuffer(t *testing.T) { + values := []int{1, 2, 3, 4, 5} + tests := []struct { + name string + replayBufferSize int + // lastN is the number of values to return from the replay buffer + lastN int + expectedValues []int + }{ + { + name: "n < replayBufferSize", + replayBufferSize: 5, + lastN: 3, + // the replay buffer is not full so Last should return values + // starting from the first published value. + expectedValues: values[:3], // []int{1, 2, 3}, + }, + { + name: "n = replayBufferSize", + replayBufferSize: 5, + lastN: 5, + expectedValues: values, + }, + { + name: "n > replayBufferSize", + replayBufferSize: 3, + lastN: 5, + // the replay buffer is full so Last should return values starting + // from lastN - replayBufferSize. + expectedValues: values[2:], // []int{3, 4, 5}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ctx = context.Background() + + replayObsvbl, publishCh := + channel.NewReplayObservable[int](ctx, tt.replayBufferSize) + + for _, value := range values { + publishCh <- value + time.Sleep(time.Millisecond) + } + + actualValues := replayObsvbl.Last(ctx, tt.lastN) + require.ElementsMatch(t, tt.expectedValues, actualValues) + }) + } +} + +func TestReplayObservable_Last_Blocks_And_Times_Out(t *testing.T) { + var ( + replayBufferSize = 5 + lastN = 5 + // splitIdx is the index at which this test splits the set of values. + // The two groups of values are published at different points in the + // test to test the behavior of Last under different conditions. + splitIdx = 3 + values = []int{1, 2, 3, 4, 5} + ctx = context.Background() + ) + + replayObsvbl, publishCh := channel.NewReplayObservable[int](ctx, replayBufferSize) + + // getLastValues is a helper function which returns a channel that will + // receive the result of a call to Last, the method under test. + getLastValues := func() chan []int { + lastValuesCh := make(chan []int, 1) + go func() { + // Last should block until lastN values have been published. + // NOTE: this will produce a warning log which can safely be ignored: + // > WARN: requested replay buffer size 3 is greater than replay buffer + // > capacity 3; returning entire replay buffer + lastValuesCh <- replayObsvbl.Last(ctx, lastN) + }() + return lastValuesCh + } + + // Ensure that last blocks when the replay buffer is empty + select { + case actualValues := <-getLastValues(): + t.Fatalf( + "Last should block until at lest 1 value has been published; actualValues: %v", + actualValues, + ) + case <-time.After(200 * time.Millisecond): + } + + // Publish some values (up to splitIdx). + for _, value := range values[:splitIdx] { + publishCh <- value + time.Sleep(time.Millisecond) + } + + // Ensure Last works as expected when n <= len(published_values). + require.ElementsMatch(t, []int{1}, replayObsvbl.Last(ctx, 1)) + require.ElementsMatch(t, []int{1, 2}, replayObsvbl.Last(ctx, 2)) + require.ElementsMatch(t, []int{1, 2, 3}, replayObsvbl.Last(ctx, 3)) + + // Ensure that Last blocks when n > len(published_values) and the replay + // buffer is not full. + select { + case actualValues := <-getLastValues(): + t.Fatalf( + "Last should block until replayPartialBufferTimeout has elapsed; received values: %v", + actualValues, + ) + default: + t.Log("OK: Last is blocking, as expected") + } + + // Ensure that Last returns the correct values when n > len(published_values) + // and the replay buffer is not full. + select { + case actualValues := <-getLastValues(): + require.ElementsMatch(t, values[:splitIdx], actualValues) + case <-time.After(250 * time.Millisecond): + t.Fatal("timed out waiting for Last to return") + } + + // Publish the rest of the values (from splitIdx on). + for _, value := range values[splitIdx:] { + publishCh <- value + time.Sleep(time.Millisecond) + } + + // Ensure that Last doesn't block when n = len(published_values) and the + // replay buffer is full. + select { + case actualValues := <-getLastValues(): + require.Len(t, actualValues, lastN) + require.ElementsMatch(t, values, actualValues) + case <-time.After(10 * time.Millisecond): + t.Fatal("timed out waiting for Last to return") + } + + // Ensure that Last still works as expected when n <= len(published_values). + require.ElementsMatch(t, []int{1}, replayObsvbl.Last(ctx, 1)) + require.ElementsMatch(t, []int{1, 2}, replayObsvbl.Last(ctx, 2)) + require.ElementsMatch(t, []int{1, 2, 3}, replayObsvbl.Last(ctx, 3)) + require.ElementsMatch(t, []int{1, 2, 3, 4}, replayObsvbl.Last(ctx, 4)) + require.ElementsMatch(t, []int{1, 2, 3, 4, 5}, replayObsvbl.Last(ctx, 5)) +} diff --git a/pkg/observable/interface.go b/pkg/observable/interface.go index 452c18dcd..d86da414f 100644 --- a/pkg/observable/interface.go +++ b/pkg/observable/interface.go @@ -7,12 +7,18 @@ import "context" // grow, other packages (e.g. https://github.com/ReactiveX/RxGo) can be considered. // (see: https://github.com/ReactiveX/RxGo/pull/377) +// ReplayObservable is an observable which replays the last n values published +// to new observers, before publishing new values to observers. +type ReplayObservable[V any] interface { + Observable[V] + // Last synchronously returns the last n values from the replay buffer. + Last(ctx context.Context, n int) []V +} + // Observable is a generic interface that allows multiple subscribers to be // notified of new values asynchronously. // It is analogous to a publisher in a "Fan-Out" system design. type Observable[V any] interface { - // Next synchronously returns the next value from the observable. - Next(context.Context) V // Subscribe returns an observer which is notified when the publishCh channel // receives a value. Subscribe(context.Context) Observer[V] From ce4a3c5ca1448c2523c62e78da787692b7e5fd04 Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Mon, 30 Oct 2023 12:23:02 -0700 Subject: [PATCH 08/28] Update main README.md Add details on why we have no docs yet and where others should look in the meantime. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index c73a4b62d..659bd96a6 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,16 @@ - [Development](#development) - [LocalNet](#localnet) +## Where are the docs? + +_This repository is still young & early._ + +It is the result of a research spike conducted by the Core [Pocket Network](https://pokt.network/) Protocol Team at [GROVE](https://grove.city/) documented [here](https://www.pokt.network/why-pokt-network-is-rolling-with-rollkit-a-technical-deep-dive/) (deep dive) and [here](https://www.pokt.network/a-sovereign-rollup-and-a-modular-future/) (summary). + +For now, we recommend visiting the links in [pokt-network/pocket/README.md](https://github.com/pokt-network/pocket/blob/main/README.md) as a starting point. + +If you want to contribute to this repository at this stage, you know where to find us. + ## Getting Started ### Makefile From 2fb66bc16088037c2ac4718ddc678c4479329acc Mon Sep 17 00:00:00 2001 From: Bryan White Date: Mon, 30 Oct 2023 23:37:10 +0100 Subject: [PATCH 09/28] feat: add either.AsyncErr type & helpers (#115) * feat: add either.AsyncErr type & helpers * fix: comment typo Co-authored-by: Daniel Olshansky --------- Co-authored-by: Daniel Olshansky --- pkg/either/errors.go | 35 +++++++++++++++++++++++++++++++++++ pkg/either/types.go | 6 ++++++ 2 files changed, 41 insertions(+) create mode 100644 pkg/either/errors.go create mode 100644 pkg/either/types.go diff --git a/pkg/either/errors.go b/pkg/either/errors.go new file mode 100644 index 000000000..f464b8886 --- /dev/null +++ b/pkg/either/errors.go @@ -0,0 +1,35 @@ +package either + +// SyncErr creates an AsyncError either from a synchronous error. +// It wraps the Error into the left field (conventionally associated with the +// error value in the Either pattern) of the Either type. It casts the result +// to the AsyncError type. +func SyncErr(err error) AsyncError { + return AsyncError(Error[chan error](err)) +} + +// AsyncErr creates an AsyncError from an error channel. +// It wraps the error channel into the right field (conventionally associated with +// successful values in the Either pattern) of the Either type. +func AsyncErr(errCh chan error) AsyncError { + return AsyncError(Success[chan error](errCh)) +} + +// SyncOrAsyncError decomposes the AsyncError into its components, returning +// a synchronous error and an error channel. If the AsyncError represents a +// synchronous error, the error channel will be nil and vice versa. +func (soaErr AsyncError) SyncOrAsyncError() (error, chan error) { + errCh, err := Either[chan error](soaErr).ValueOrError() + return err, errCh +} + +// IsSyncError checks if the AsyncError represents a synchronous error. +func (soaErr AsyncError) IsSyncError() bool { + return Either[chan error](soaErr).IsError() +} + +// IsAsyncError checks if the AsyncError represents an asynchronous error +// (sent through a channel). +func (soaErr AsyncError) IsAsyncError() bool { + return Either[chan error](soaErr).IsSuccess() +} diff --git a/pkg/either/types.go b/pkg/either/types.go new file mode 100644 index 000000000..2d08f7945 --- /dev/null +++ b/pkg/either/types.go @@ -0,0 +1,6 @@ +package either + +// AsyncError represents a value which could either be a synchronous error or +// an asynchronous error (sent through a channel). It wraps the more generic +// `Either` type specific for error channels. +type AsyncError Either[chan error] From fd0db1d154aa49d5f6c3be85a0d96087e3cbd7d5 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Tue, 31 Oct 2023 00:18:45 +0100 Subject: [PATCH 10/28] refactor: add `either.Bytes` alias (#117) --- pkg/client/events_query/client.go | 10 +++++----- pkg/client/interface.go | 4 ++-- pkg/either/types.go | 5 ++++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pkg/client/events_query/client.go b/pkg/client/events_query/client.go index 23fb4b208..f41e3e536 100644 --- a/pkg/client/events_query/client.go +++ b/pkg/client/events_query/client.go @@ -48,9 +48,9 @@ type eventsQueryClient struct { // corresponding connection which produces its inputs. type eventsBytesAndConn struct { // eventsBytes is an observable which is notified about chain event messages - // matching the given query. It receives an either.Either[[]byte] which is + // matching the given query. It receives an either.Bytes which is // either an error or the event message bytes. - eventsBytes observable.Observable[either.Either[[]byte]] + eventsBytes observable.Observable[either.Bytes] conn client.Connection isClosed bool } @@ -81,7 +81,7 @@ func NewEventsQueryClient(cometWebsocketURL string, opts ...client.EventsQueryCl } // EventsBytes returns an eventsBytes observable which is notified about chain -// event messages matching the given query. It receives an either.Either[[]byte] +// event messages matching the given query. It receives an either.Bytes // which is either an error or the event message bytes. // (see: https://pkg.go.dev/github.com/cometbft/cometbft/types#pkg-constants) // (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) @@ -151,7 +151,7 @@ func (eqc *eventsQueryClient) newEventsBytesAndConn( } // Construct an eventsBytes for the given query. - eventsBzObservable, eventsBzPublishCh := channel.NewObservable[either.Either[[]byte]]() + eventsBzObservable, eventsBzPublishCh := channel.NewObservable[either.Bytes]() // Publish either events bytes or an error received from the connection to // the eventsBz observable. @@ -198,7 +198,7 @@ func (eqc *eventsQueryClient) openEventsBytesAndConn( func (eqc *eventsQueryClient) goPublishEventsBz( ctx context.Context, conn client.Connection, - eventsBzPublishCh chan<- either.Either[[]byte], + eventsBzPublishCh chan<- either.Bytes, ) { // Read and handle messages from the websocket. This loop will exit when the // websocket connection is isClosed and/or returns an error. diff --git a/pkg/client/interface.go b/pkg/client/interface.go index 731ab12b7..bd811a153 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -22,8 +22,8 @@ import ( // value which contains either an error or the event message bytes. // TODO_HACK: The purpose of this type is to work around gomock's lack of // support for generic types. For the same reason, this type cannot be an -// alias (i.e. EventsBytesObservable = observable.Observable[either.Either[[]byte]]). -type EventsBytesObservable observable.Observable[either.Either[[]byte]] +// alias (i.e. EventsBytesObservable = observable.Observable[either.Bytes]). +type EventsBytesObservable observable.Observable[either.Bytes] // EventsQueryClient is used to subscribe to chain event messages matching the given query, type EventsQueryClient interface { diff --git a/pkg/either/types.go b/pkg/either/types.go index 2d08f7945..af171b6c1 100644 --- a/pkg/either/types.go +++ b/pkg/either/types.go @@ -3,4 +3,7 @@ package either // AsyncError represents a value which could either be a synchronous error or // an asynchronous error (sent through a channel). It wraps the more generic // `Either` type specific for error channels. -type AsyncError Either[chan error] +type ( + AsyncError Either[chan error] + Bytes = Either[[]byte] +) From dd18066fc066dadef9dfc8481903f7a2aee50952 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:08:13 +0000 Subject: [PATCH 11/28] [E2E] Add (Un)Stake Tests (#88) --- e2e/tests/help.feature | 10 ++--- e2e/tests/init_test.go | 84 ++++++++++++++++++++++++++++++++++++++++- e2e/tests/send.feature | 22 +++++------ e2e/tests/stake.feature | 25 ++++++++++++ 4 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 e2e/tests/stake.feature diff --git a/e2e/tests/help.feature b/e2e/tests/help.feature index 156ec5194..7d0867f99 100644 --- a/e2e/tests/help.feature +++ b/e2e/tests/help.feature @@ -1,7 +1,7 @@ Feature: Root Namespace - Scenario: User Needs Help - Given the user has the pocketd binary installed - When the user runs the command "help" - Then the user should be able to see standard output containing "Available Commands" - And the pocketd binary should exit without error \ No newline at end of file + Scenario: User Needs Help + Given the user has the pocketd binary installed + When the user runs the command "help" + Then the user should be able to see standard output containing "Available Commands" + And the pocketd binary should exit without error diff --git a/e2e/tests/init_test.go b/e2e/tests/init_test.go index fe831a507..e1284417a 100644 --- a/e2e/tests/init_test.go +++ b/e2e/tests/init_test.go @@ -22,8 +22,8 @@ var ( ) func init() { - addrRe = regexp.MustCompile(`address: (\S+)\s+name: (\S+)`) - amountRe = regexp.MustCompile(`amount: "(.+?)"\s+denom: upokt`) + addrRe = regexp.MustCompile(`address:\s+(\S+)\s+name:\s+(\S+)`) + amountRe = regexp.MustCompile(`amount:\s+"(.+?)"\s+denom:\s+upokt`) } type suite struct { @@ -123,6 +123,85 @@ func (s *suite) TheUserShouldWaitForSeconds(dur int64) { time.Sleep(time.Duration(dur) * time.Second) } +func (s *suite) TheUserStakesAWithUpoktFromTheAccount(actorType string, amount int64, accName string) { + args := []string{ + "tx", + actorType, + fmt.Sprintf("stake-%s", actorType), + fmt.Sprintf("%dupokt", amount), + "--from", + accName, + keyRingFlag, + "-y", + } + res, err := s.pocketd.RunCommandOnHost("", args...) + if err != nil { + s.Fatalf("error staking %s: %s", actorType, err) + } + s.pocketd.result = res +} + +func (s *suite) TheUserUnstakesAFromTheAccount(actorType string, accName string) { + args := []string{ + "tx", + actorType, + fmt.Sprintf("unstake-%s", actorType), + "--from", + accName, + keyRingFlag, + "-y", + } + res, err := s.pocketd.RunCommandOnHost("", args...) + if err != nil { + s.Fatalf("error unstaking %s: %s", actorType, err) + } + s.pocketd.result = res +} + +func (s *suite) TheForAccountIsNotStaked(actorType, accName string) { + found, _ := s.getStakedAmount(actorType, accName) + if found { + s.Fatalf("account %s should not be staked", accName) + } +} + +func (s *suite) TheForAccountIsStakedWithUpokt(actorType, accName string, amount int64) { + found, stakeAmount := s.getStakedAmount(actorType, accName) + if !found { + s.Fatalf("account %s should be staked", accName) + } + if int64(stakeAmount) != amount { + s.Fatalf("account %s stake amount is not %d", accName, amount) + } +} + +func (s *suite) getStakedAmount(actorType, accName string) (bool, int) { + s.Helper() + args := []string{ + "query", + actorType, + fmt.Sprintf("list-%s", actorType), + } + res, err := s.pocketd.RunCommandOnHost("", args...) + if err != nil { + s.Fatalf("error getting %s: %s", actorType, err) + } + s.pocketd.result = res + found := strings.Contains(res.Stdout, accNameToAddrMap[accName]) + amount := 0 + if found { + escapedAddress := regexp.QuoteMeta(accNameToAddrMap[accName]) + stakedAmountRe := regexp.MustCompile(`address: ` + escapedAddress + `\s+stake:\s+amount: "(\d+)"`) + matches := stakedAmountRe.FindStringSubmatch(res.Stdout) + if len(matches) < 2 { + s.Fatalf("no stake amount found for %s", accName) + } + amount, err = strconv.Atoi(matches[1]) + require.NoError(s, err) + } + return found, amount +} + func (s *suite) buildAddrMap() { s.Helper() res, err := s.pocketd.RunCommand( @@ -131,6 +210,7 @@ func (s *suite) buildAddrMap() { if err != nil { s.Fatalf("error getting keys: %s", err) } + s.pocketd.result = res matches := addrRe.FindAllStringSubmatch(res.Stdout, -1) for _, match := range matches { name := match[2] diff --git a/e2e/tests/send.feature b/e2e/tests/send.feature index dc5fc4504..4df818bf2 100644 --- a/e2e/tests/send.feature +++ b/e2e/tests/send.feature @@ -1,13 +1,13 @@ Feature: Tx Namespace - Scenario: User can send uPOKT - Given the user has the pocketd binary installed - And the account "app1" has a balance greater than "1000" uPOKT - And an account exists for "app2" - When the user sends "1000" uPOKT from account "app1" to account "app2" - Then the user should be able to see standard output containing "txhash:" - And the user should be able to see standard output containing "code: 0" - And the pocketd binary should exit without error - And the user should wait for "5" seconds - And the account balance of "app1" should be "1000" uPOKT "less" than before - And the account balance of "app2" should be "1000" uPOKT "more" than before + Scenario: User can send uPOKT + Given the user has the pocketd binary installed + And the account "app1" has a balance greater than "1000" uPOKT + And an account exists for "app2" + When the user sends "1000" uPOKT from account "app1" to account "app2" + Then the user should be able to see standard output containing "txhash:" + And the user should be able to see standard output containing "code: 0" + And the pocketd binary should exit without error + And the user should wait for "5" seconds + And the account balance of "app1" should be "1000" uPOKT "less" than before + And the account balance of "app2" should be "1000" uPOKT "more" than before diff --git a/e2e/tests/stake.feature b/e2e/tests/stake.feature new file mode 100644 index 000000000..1171ef3d6 --- /dev/null +++ b/e2e/tests/stake.feature @@ -0,0 +1,25 @@ +Feature: Stake Namespaces + + Scenario: User can stake a Gateway + Given the user has the pocketd binary installed + And the "gateway" for account "gateway1" is not staked + And the account "gateway1" has a balance greater than "1000" uPOKT + When the user stakes a "gateway" with "1000" uPOKT from the account "gateway1" + Then the user should be able to see standard output containing "txhash:" + And the user should be able to see standard output containing "code: 0" + And the pocketd binary should exit without error + And the user should wait for "5" seconds + And the "gateway" for account "gateway1" is staked with "1000" uPOKT + And the account balance of "gateway1" should be "1000" uPOKT "less" than before + + Scenario: User can unstake a Gateway + Given the user has the pocketd binary installed + And the "gateway" for account "gateway1" is staked with "1000" uPOKT + And an account exists for "gateway1" + When the user unstakes a "gateway" from the account "gateway1" + Then the user should be able to see standard output containing "txhash:" + And the user should be able to see standard output containing "code: 0" + And the pocketd binary should exit without error + And the user should wait for "5" seconds + And the "gateway" for account "gateway1" is not staked + And the account balance of "gateway1" should be "1000" uPOKT "more" than before From dc5c61be2dabc041e517067c41d5ad3855180d10 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Tue, 31 Oct 2023 21:14:30 +0000 Subject: [PATCH 12/28] [AppGate] Implement DelegateToGateway and add Tests (#90) --- Makefile | 16 + app/app.go | 24 +- docs/static/openapi.yml | 2219 ++++++++++++++--- go.mod | 4 +- proto/pocket/application/application.proto | 4 +- proto/pocket/application/tx.proto | 4 +- testutil/keeper/application.go | 25 + testutil/keeper/gateway.go | 1 - .../client/cli/tx_delegate_to_gateway.go | 17 +- .../client/cli/tx_delegate_to_gateway_test.go | 117 + x/application/keeper/keeper.go | 10 +- .../keeper/msg_server_delegate_to_gateway.go | 40 +- .../msg_server_delegate_to_gateway_test.go | 184 ++ .../keeper/msg_server_stake_application.go | 7 +- .../simulation/delegate_to_gateway.go | 11 +- x/application/types/errors.go | 5 +- x/application/types/expected_keepers.go | 9 +- x/application/types/genesis.go | 10 +- x/application/types/genesis_test.go | 166 +- .../types/message_delegate_to_gateway.go | 19 +- .../types/message_delegate_to_gateway_test.go | 28 +- x/gateway/keeper/keeper.go | 7 +- 22 files changed, 2532 insertions(+), 395 deletions(-) create mode 100644 x/application/client/cli/tx_delegate_to_gateway_test.go create mode 100644 x/application/keeper/msg_server_delegate_to_gateway_test.go diff --git a/Makefile b/Makefile index 28b58e15e..ba08bdf84 100644 --- a/Makefile +++ b/Makefile @@ -282,6 +282,22 @@ app2_unstake: ## Unstake app2 app3_unstake: ## Unstake app3 APP=app3 make app_unstake +.PHONY: app_delegate +app_delegate: ## Delegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked + pocketd --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + +.PHONY: app1_delegate_gateway1 +app1_delegate_gateway1: ## Delegate trust to gateway1 + APP=app1 GATEWAY_ADDR=pokt15vzxjqklzjtlz7lahe8z2dfe9nm5vxwwmscne4 make app_delegate + +.PHONY: app2_delegate_gateway2 +app2_delegate_gateway2: ## Delegate trust to gateway2 + APP=app2 GATEWAY_ADDR=pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz make app_delegate + +.PHONY: app3_delegate_gateway3 +app3_delegate_gateway3: ## Delegate trust to gateway3 + APP=app3 GATEWAY_ADDR=pokt1zhmkkd0rh788mc9prfq0m2h88t9ge0j83gnxya make app_delegate + ################# ### Suppliers ### ################# diff --git a/app/app.go b/app/app.go index f640a6ee7..05cad6ae7 100644 --- a/app/app.go +++ b/app/app.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + // this line is used by starport scaffolding # stargate/app/moduleImport autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" @@ -585,16 +586,6 @@ func New( ) sessionModule := sessionmodule.NewAppModule(appCodec, app.SessionKeeper, app.AccountKeeper, app.BankKeeper) - app.ApplicationKeeper = *applicationmodulekeeper.NewKeeper( - appCodec, - keys[applicationmoduletypes.StoreKey], - keys[applicationmoduletypes.MemStoreKey], - app.GetSubspace(applicationmoduletypes.ModuleName), - - app.BankKeeper, - ) - applicationModule := applicationmodule.NewAppModule(appCodec, app.ApplicationKeeper, app.AccountKeeper, app.BankKeeper) - app.SupplierKeeper = *suppliermodulekeeper.NewKeeper( appCodec, keys[suppliermoduletypes.StoreKey], @@ -612,10 +603,21 @@ func New( app.GetSubspace(gatewaymoduletypes.ModuleName), app.BankKeeper, - app.AccountKeeper, ) gatewayModule := gatewaymodule.NewAppModule(appCodec, app.GatewayKeeper, app.AccountKeeper, app.BankKeeper) + app.ApplicationKeeper = *applicationmodulekeeper.NewKeeper( + appCodec, + keys[applicationmoduletypes.StoreKey], + keys[applicationmoduletypes.MemStoreKey], + app.GetSubspace(applicationmoduletypes.ModuleName), + + app.BankKeeper, + app.AccountKeeper, + app.GatewayKeeper, + ) + applicationModule := applicationmodule.NewAppModule(appCodec, app.ApplicationKeeper, app.AccountKeeper, app.BankKeeper) + // this line is used by starport scaffolding # stargate/app/keeperDefinition /**** IBC Routing ****/ diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index 461aaedf0..3e0e46e33 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46489,7 +46489,9 @@ paths: was desigtned created to enable more complex service identification - gener + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? name: type: string description: >- @@ -46503,6 +46505,13 @@ paths: ApplicationServiceConfig holds the service configuration the application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, + in a non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -46662,7 +46671,9 @@ paths: desigtned created to enable more complex service identification - gener + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? name: type: string description: >- @@ -46676,6 +46687,13 @@ paths: ApplicationServiceConfig holds the service configuration the application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, + in a non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -47176,6 +47194,189 @@ paths: ApplicationServiceConfig holds the service configuration the application stakes for title: The ID of the service this session is servicing + delegatee_gateway_pub_keys: + type: array + items: + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the + type of the serialized + + protocol buffer message. This string must + contain at least + + one "/" character. The last segment of the URL's + path must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name + should be in a canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the + binary all types that they + + expect it to use in the context of Any. However, + for URLs which use the + + scheme `http`, `https`, or no scheme, one can + optionally set up a type + + server that maps type URLs to message + definitions as follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a + [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup + results based on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently + available in the official + + protobuf release, and it is not used for type + URLs beginning with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty + scheme) might be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol + buffer message along with a + + URL that describes the type of the serialized + message. + + + Protobuf library provides support to pack/unpack Any + values in the form + + of utility functions or additional generated methods + of the Any type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will + by default use + + 'type.googleapis.com/full.type.name' as the type URL + and the unpack + + methods only use the fully qualified type name after + the last '/' + + in the type URL, for example "foo.bar.com/x/y.z" + will yield type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the + regular + + representation of the deserialized, embedded + message, with an + + additional field `@type` which contains the type + URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a + custom JSON + + representation, that representation will be embedded + adding a field + + `value` which holds the custom JSON in addition to + the `@type` + + field. Example (for message + [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: >- + The corresponding cosmos.crypto.PubKey (interface) + encoded into an cosmos.codec.Any for use in the non + nullable slice of delegatee Gateways the application + is delegated to. suppliers: type: array items: @@ -47322,147 +47523,648 @@ paths: properties: '@type': type: string - additionalProperties: {} - parameters: - - name: application_address - description: >- - The Bech32 address of the application using cosmos' ScalarDescriptor - to ensure deterministic encoding - in: query - required: false - type: string - - name: service_id.id - description: >- - NOTE: `ServiceId.Id` may seem redundant but was desigtned created to - enable more complex service identification + description: >- + A URL/resource name that uniquely identifies the type of + the serialized - For example, what if we want to request a session for a certain - service but with some additional configs that identify it? + protocol buffer message. This string must contain at + least + one "/" character. The last segment of the URL's path + must represent - Unique identifier for the service - in: query - required: false - type: string - - name: service_id.name - description: >- - TODO_TECHDEBT: Name is currently unused but acts as a reminder than - an optional onchain representation of the service is necessary + the fully qualified name of the type (as in + `path/google.protobuf.Duration`). The name should be in + a canonical form - (Optional) Semantic human readable name for the service - in: query - required: false - type: string - - name: block_height - description: The block height to query the session for - in: query - required: false - type: string - format: int64 - tags: - - Query - /pocket/session/params: - get: - summary: Parameters queries the parameters of the module. - operationId: PocketSessionParams - responses: - '200': - description: A successful response. - schema: - type: object - properties: - params: - description: params holds all the parameters of this module. - type: object - description: >- - QueryParamsResponse is response type for the Query/Params RPC - method. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - tags: - - Query - /pocket/supplier/params: - get: - summary: Parameters queries the parameters of the module. - operationId: PocketSupplierParams - responses: - '200': - description: A successful response. - schema: - type: object - properties: - params: - description: params holds all the parameters of this module. - type: object - description: >- - QueryParamsResponse is response type for the Query/Params RPC - method. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - tags: - - Query - /pocket/supplier/supplier: - get: - operationId: PocketSupplierSupplierAll - responses: - '200': - description: A successful response. - schema: - type: object - properties: - supplier: - type: array - items: - type: object - properties: - address: - type: string - title: >- - The Bech32 address of the supplier using cosmos' - ScalarDescriptor to ensure deterministic encoding - stake: - title: The total amount of uPOKT the supplier has staked - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary + all types that they + + expect it to use in the context of Any. However, for + URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally + set up a type + + server that maps type URLs to message definitions as + follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a + [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based + on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in + the official + + protobuf release, and it is not used for type URLs + beginning with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) + might be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer + message along with a + + URL that describes the type of the serialized message. + + + Protobuf library provides support to pack/unpack Any values + in the form + + of utility functions or additional generated methods of the + Any type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by + default use + + 'type.googleapis.com/full.type.name' as the type URL and the + unpack + + methods only use the fully qualified type name after the + last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield + type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with + an + + additional field `@type` which contains the type URL. + Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom + JSON + + representation, that representation will be embedded adding + a field + + `value` which holds the custom JSON in addition to the + `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + parameters: + - name: application_address + description: >- + The Bech32 address of the application using cosmos' ScalarDescriptor + to ensure deterministic encoding + in: query + required: false + type: string + - name: service_id.id + description: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created to + enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? + + + Unique identifier for the service + in: query + required: false + type: string + - name: service_id.name + description: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder than + an optional onchain representation of the service is necessary + + + (Optional) Semantic human readable name for the service + in: query + required: false + type: string + - name: block_height + description: The block height to query the session for + in: query + required: false + type: string + format: int64 + tags: + - Query + /pocket/session/params: + get: + summary: Parameters queries the parameters of the module. + operationId: PocketSessionParams + responses: + '200': + description: A successful response. + schema: + type: object + properties: + params: + description: params holds all the parameters of this module. + type: object + description: >- + QueryParamsResponse is response type for the Query/Params RPC + method. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of + the serialized + + protocol buffer message. This string must contain at + least + + one "/" character. The last segment of the URL's path + must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in + a canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary + all types that they + + expect it to use in the context of Any. However, for + URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally + set up a type + + server that maps type URLs to message definitions as + follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a + [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based + on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in + the official + + protobuf release, and it is not used for type URLs + beginning with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) + might be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer + message along with a + + URL that describes the type of the serialized message. + + + Protobuf library provides support to pack/unpack Any values + in the form + + of utility functions or additional generated methods of the + Any type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by + default use + + 'type.googleapis.com/full.type.name' as the type URL and the + unpack + + methods only use the fully qualified type name after the + last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield + type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with + an + + additional field `@type` which contains the type URL. + Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom + JSON + + representation, that representation will be embedded adding + a field + + `value` which holds the custom JSON in addition to the + `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + tags: + - Query + /pocket/supplier/params: + get: + summary: Parameters queries the parameters of the module. + operationId: PocketSupplierParams + responses: + '200': + description: A successful response. + schema: + type: object + properties: + params: + description: params holds all the parameters of this module. + type: object + description: >- + QueryParamsResponse is response type for the Query/Params RPC + method. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of + the serialized + + protocol buffer message. This string must contain at + least + + one "/" character. The last segment of the URL's path + must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in + a canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary + all types that they + + expect it to use in the context of Any. However, for + URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally + set up a type + + server that maps type URLs to message definitions as + follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a + [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based + on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in + the official + + protobuf release, and it is not used for type URLs + beginning with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) + might be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer + message along with a + + URL that describes the type of the serialized message. + + + Protobuf library provides support to pack/unpack Any values + in the form + + of utility functions or additional generated methods of the + Any type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by + default use + + 'type.googleapis.com/full.type.name' as the type URL and the + unpack + + methods only use the fully qualified type name after the + last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield + type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with + an + + additional field `@type` which contains the type URL. + Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom + JSON + + representation, that representation will be embedded adding + a field + + `value` which holds the custom JSON in addition to the + `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + tags: + - Query + /pocket/supplier/supplier: + get: + operationId: PocketSupplierSupplierAll + responses: + '200': + description: A successful response. + schema: + type: object + properties: + supplier: + type: array + items: + type: object + properties: + address: + type: string + title: >- + The Bech32 address of the supplier using cosmos' + ScalarDescriptor to ensure deterministic encoding + stake: + title: The total amount of uPOKT the supplier has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. NOTE: The amount field is an Int which implements the @@ -47480,10 +48182,24 @@ paths: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but + was desigtned created to enable more complex + service identification + + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? name: type: string - title: Semantic name for the service + description: >- + (Optional) Semantic human readable name for + the service + title: >- + TODO_TECHDEBT: Name is currently unused but + acts as a reminder than an optional onchain + representation of the service is necessary endpoints: type: array items: @@ -47538,8 +48254,8 @@ paths: Additional configuration options for the endpoint title: >- - Endpoint message to hold service configuration - details + SupplierEndpoint message to hold service + configuration details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration @@ -47565,35 +48281,202 @@ paths: total is total number of results available if PageRequest.count_total - was set, its value is undefined otherwise - description: >- - PageResponse is to be embedded in gRPC response messages where - the + was set, its value is undefined otherwise + description: >- + PageResponse is to be embedded in gRPC response messages where + the + + corresponding request message has used PageRequest. + + message SomeResponse { + repeated Bar results = 1; + PageResponse page = 2; + } + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of + the serialized + + protocol buffer message. This string must contain at + least + + one "/" character. The last segment of the URL's path + must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in + a canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary + all types that they + + expect it to use in the context of Any. However, for + URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally + set up a type + + server that maps type URLs to message definitions as + follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a + [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based + on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in + the official + + protobuf release, and it is not used for type URLs + beginning with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) + might be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer + message along with a + + URL that describes the type of the serialized message. + + + Protobuf library provides support to pack/unpack Any values + in the form + + of utility functions or additional generated methods of the + Any type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by + default use + + 'type.googleapis.com/full.type.name' as the type URL and the + unpack + + methods only use the fully qualified type name after the + last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield + type - corresponding request message has used PageRequest. + name "y.z". - message SomeResponse { - repeated Bar results = 1; - PageResponse page = 2; - } - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with + an + + additional field `@type` which contains the type URL. + Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom + JSON + + representation, that representation will be embedded adding + a field + + `value` which holds the custom JSON in addition to the + `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } parameters: - name: pagination.key description: |- @@ -47698,10 +48581,24 @@ paths: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? name: type: string - title: Semantic name for the service + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts + as a reminder than an optional onchain + representation of the service is necessary endpoints: type: array items: @@ -47739,51 +48636,218 @@ paths: description: >- Enum to define configuration options - TODO_RESEARCH: Should these be configs, - SLAs or something else? There will be - more discussion once we get closer to - implementing on-chain QoS. + TODO_RESEARCH: Should these be configs, + SLAs or something else? There will be + more discussion once we get closer to + implementing on-chain QoS. + + - UNKNOWN_CONFIG: Undefined config option + - TIMEOUT: Timeout setting + value: + type: string + title: Config option value + title: >- + Key-value wrapper for config options, as + proto maps can't be keyed by enums + title: >- + Additional configuration options for the + endpoint + title: >- + SupplierEndpoint message to hold service + configuration details + title: List of endpoints for the service + title: >- + SupplierServiceConfig holds the service configuration + the supplier stakes for + title: The service configs this supplier can support + description: >- + Supplier is the type defining the actor in Pocket Network that + provides RPC services. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of + the serialized + + protocol buffer message. This string must contain at + least + + one "/" character. The last segment of the URL's path + must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in + a canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary + all types that they + + expect it to use in the context of Any. However, for + URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally + set up a type + + server that maps type URLs to message definitions as + follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a + [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based + on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in + the official + + protobuf release, and it is not used for type URLs + beginning with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) + might be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer + message along with a + + URL that describes the type of the serialized message. + + + Protobuf library provides support to pack/unpack Any values + in the form + + of utility functions or additional generated methods of the + Any type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by + default use + + 'type.googleapis.com/full.type.name' as the type URL and the + unpack + + methods only use the fully qualified type name after the + last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield + type + + name "y.z". + + + + JSON + + ==== - - UNKNOWN_CONFIG: Undefined config option - - TIMEOUT: Timeout setting - value: - type: string - title: Config option value - title: >- - Key-value wrapper for config options, as - proto maps can't be keyed by enums - title: >- - Additional configuration options for the - endpoint - title: >- - Endpoint message to hold service configuration - details - title: List of endpoints for the service - title: >- - SupplierServiceConfig holds the service configuration - the supplier stakes for - title: The service configs this supplier can support - description: >- - Supplier is the type defining the actor in Pocket Network that - provides RPC services. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with + an + + additional field `@type` which contains the type URL. + Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom + JSON + + representation, that representation will be embedded adding + a field + + `value` which holds the custom JSON in addition to the + `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } parameters: - name: address in: path @@ -76549,36 +77613,211 @@ definitions: description: |- Coin defines a token with a denomination and an amount. - NOTE: The amount field is an Int which implements the custom method - signatures required by gogoproto. - service_configs: - type: array - items: - type: object - properties: - service_id: - title: Unique and semantic identifier for the service - type: object - properties: - id: - type: string - description: Unique identifier for the service - title: >- - NOTE: `ServiceId.Id` may seem redundant but was desigtned - created to enable more complex service identification + NOTE: The amount field is an Int which implements the custom method + signatures required by gogoproto. + service_configs: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary + title: >- + ApplicationServiceConfig holds the service configuration the + application stakes for + title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice + delegatee_gateway_pub_keys: + type: array + items: + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of the + serialized + + protocol buffer message. This string must contain at least + + one "/" character. The last segment of the URL's path must + represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a + canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary all types + that they + + expect it to use in the context of Any. However, for URLs which + use the + + scheme `http`, `https`, or no scheme, one can optionally set up + a type + + server that maps type URLs to message definitions as follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the + official + + protobuf release, and it is not used for type URLs beginning + with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) might + be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer message along + with a + + URL that describes the type of the serialized message. + + + Protobuf library provides support to pack/unpack Any values in the + form + + of utility functions or additional generated methods of the Any + type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + + 'type.googleapis.com/full.type.name' as the type URL and the unpack + + methods only use the fully qualified type name after the last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } - gener - name: - type: string - description: (Optional) Semantic human readable name for the service - title: >- - TODO_TECHDEBT: Name is currently unused but acts as a - reminder than an optional onchain representation of the - service is necessary - title: >- - ApplicationServiceConfig holds the service configuration the - application stakes for - title: The ID of the service this session is servicing + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: >- + The corresponding cosmos.crypto.PubKey (interface) encoded into an + cosmos.codec.Any for use in the non nullable slice of delegatee + Gateways the application is delegated to. title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76639,7 +77878,9 @@ definitions: desigtned created to enable more complex service identification - gener + For example, what if we want to request a session for + a certain service but with some additional configs + that identify it? name: type: string description: >- @@ -76653,6 +77894,13 @@ definitions: ApplicationServiceConfig holds the service configuration the application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76726,7 +77974,9 @@ definitions: desigtned created to enable more complex service identification - gener + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? name: type: string description: (Optional) Semantic human readable name for the service @@ -76738,6 +77988,13 @@ definitions: ApplicationServiceConfig holds the service configuration the application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76762,7 +78019,8 @@ definitions: NOTE: `ServiceId.Id` may seem redundant but was desigtned created to enable more complex service identification - gener + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? name: type: string description: (Optional) Semantic human readable name for the service @@ -76783,7 +78041,8 @@ definitions: NOTE: `ServiceId.Id` may seem redundant but was desigtned created to enable more complex service identification - gener + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? name: type: string description: (Optional) Semantic human readable name for the service @@ -77011,43 +78270,222 @@ definitions: Coin defines a token with a denomination and an amount. - NOTE: The amount field is an Int which implements the custom - method + NOTE: The amount field is an Int which implements the custom + method + + signatures required by gogoproto. + service_configs: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session + for a certain service but with some additional + configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as + a reminder than an optional onchain representation + of the service is necessary + title: >- + ApplicationServiceConfig holds the service configuration the + application stakes for + title: The ID of the service this session is servicing + delegatee_gateway_pub_keys: + type: array + items: + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of + the serialized + + protocol buffer message. This string must contain at + least + + one "/" character. The last segment of the URL's path + must represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in + a canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary + all types that they + + expect it to use in the context of Any. However, for + URLs which use the + + scheme `http`, `https`, or no scheme, one can optionally + set up a type + + server that maps type URLs to message definitions as + follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a + [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based + on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in + the official + + protobuf release, and it is not used for type URLs + beginning with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) + might be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer + message along with a + + URL that describes the type of the serialized message. + + + Protobuf library provides support to pack/unpack Any values + in the form + + of utility functions or additional generated methods of the + Any type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by + default use + + 'type.googleapis.com/full.type.name' as the type URL and the + unpack + + methods only use the fully qualified type name after the + last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield + type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with + an + + additional field `@type` which contains the type URL. + Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom + JSON + + representation, that representation will be embedded adding + a field - signatures required by gogoproto. - service_configs: - type: array - items: - type: object - properties: - service_id: - title: Unique and semantic identifier for the service - type: object - properties: - id: - type: string - description: Unique identifier for the service - title: >- - NOTE: `ServiceId.Id` may seem redundant but was - desigtned created to enable more complex service - identification + `value` which holds the custom JSON in addition to the + `@type` - For example, what if we want to request a session - for a certain service but with some additional - configs that identify it? - name: - type: string - description: >- - (Optional) Semantic human readable name for the - service - title: >- - TODO_TECHDEBT: Name is currently unused but acts as - a reminder than an optional onchain representation - of the service is necessary - title: >- - ApplicationServiceConfig holds the service configuration the - application stakes for - title: The ID of the service this session is servicing + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: >- + The corresponding cosmos.crypto.PubKey (interface) encoded + into an cosmos.codec.Any for use in the non nullable slice of + delegatee Gateways the application is delegated to. suppliers: type: array items: @@ -77296,6 +78734,179 @@ definitions: ApplicationServiceConfig holds the service configuration the application stakes for title: The ID of the service this session is servicing + delegatee_gateway_pub_keys: + type: array + items: + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of the + serialized + + protocol buffer message. This string must contain at least + + one "/" character. The last segment of the URL's path must + represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a + canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary all + types that they + + expect it to use in the context of Any. However, for URLs + which use the + + scheme `http`, `https`, or no scheme, one can optionally set + up a type + + server that maps type URLs to message definitions as + follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a + [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on + the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the + official + + protobuf release, and it is not used for type URLs beginning + with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) + might be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer message + along with a + + URL that describes the type of the serialized message. + + + Protobuf library provides support to pack/unpack Any values in + the form + + of utility functions or additional generated methods of the Any + type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default + use + + 'type.googleapis.com/full.type.name' as the type URL and the + unpack + + methods only use the fully qualified type name after the last + '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a + field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: >- + The corresponding cosmos.crypto.PubKey (interface) encoded into an + cosmos.codec.Any for use in the non nullable slice of delegatee + Gateways the application is delegated to. suppliers: type: array items: @@ -77800,10 +79411,24 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for + a certain service but with some additional configs + that identify it? name: type: string - title: Semantic name for the service + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of + the service is necessary endpoints: type: array items: @@ -77855,7 +79480,9 @@ definitions: Key-value wrapper for config options, as proto maps can't be keyed by enums title: Additional configuration options for the endpoint - title: Endpoint message to hold service configuration details + title: >- + SupplierEndpoint message to hold service configuration + details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration the @@ -77928,10 +79555,22 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? name: type: string - title: Semantic name for the service + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary endpoints: type: array items: @@ -77982,7 +79621,9 @@ definitions: Key-value wrapper for config options, as proto maps can't be keyed by enums title: Additional configuration options for the endpoint - title: Endpoint message to hold service configuration details + title: >- + SupplierEndpoint message to hold service configuration + details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration the diff --git a/go.mod b/go.mod index cb8df3174..d2fd3985c 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( cosmossdk.io/math v1.0.1 github.com/cometbft/cometbft v0.37.2 github.com/cometbft/cometbft-db v0.8.0 + github.com/cosmos/cosmos-proto v1.0.0-beta.2 github.com/cosmos/cosmos-sdk v0.47.3 github.com/cosmos/gogoproto v1.4.10 github.com/cosmos/ibc-go/v7 v7.1.0 @@ -25,6 +26,7 @@ require ( go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.12.0 golang.org/x/sync v0.3.0 + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 google.golang.org/grpc v1.56.1 gopkg.in/yaml.v2 v2.4.0 ) @@ -69,7 +71,6 @@ require ( github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect - github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.0 // indirect @@ -265,7 +266,6 @@ require ( gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/api v0.122.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/proto/pocket/application/application.proto b/proto/pocket/application/application.proto index c7b0ae83b..2c754a5dc 100644 --- a/proto/pocket/application/application.proto +++ b/proto/pocket/application/application.proto @@ -5,6 +5,8 @@ option go_package = "pocket/x/application/types"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; +import "gogoproto/gogo.proto"; + import "pocket/shared/service.proto"; // Application defines the type used to store an on-chain definition and state for an application @@ -12,5 +14,5 @@ message Application { string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked repeated shared.ApplicationServiceConfig service_configs = 3; // The ID of the service this session is servicing + repeated string delegatee_gateway_addresses = 4 [(cosmos_proto.scalar) = "cosmos.AddressString", (gogoproto.nullable) = false]; // The Bech32 encoded addresses for all delegatee Gateways, in a non-nullable slice } - diff --git a/proto/pocket/application/tx.proto b/proto/pocket/application/tx.proto index 971d47b83..0b49ce706 100644 --- a/proto/pocket/application/tx.proto +++ b/proto/pocket/application/tx.proto @@ -34,7 +34,9 @@ message MsgUnstakeApplication { message MsgUnstakeApplicationResponse {} message MsgDelegateToGateway { - string address = 1; + option (cosmos.msg.v1.signer) = "app_address"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries + string app_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding + string gateway_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the gateway the application wants to delegate to using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding } message MsgDelegateToGatewayResponse {} diff --git a/testutil/keeper/application.go b/testutil/keeper/application.go index 08bf27bb7..71b06b446 100644 --- a/testutil/keeper/application.go +++ b/testutil/keeper/application.go @@ -18,8 +18,14 @@ import ( mocks "pocket/testutil/application/mocks" "pocket/x/application/keeper" "pocket/x/application/types" + gatewaytypes "pocket/x/gateway/types" ) +// StakedGatewayMap is used to mock whether a gateway is staked or not for use +// in the application's mocked gateway keeper. This enables the tester to +// control whether a gateway is "staked" or not and whether it can be delegated to +var StakedGatewayMap = make(map[string]struct{}) + func ApplicationKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { storeKey := sdk.NewKVStoreKey(types.StoreKey) memStoreKey := storetypes.NewMemoryStoreKey(types.MemStoreKey) @@ -38,6 +44,23 @@ func ApplicationKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { mockBankKeeper.EXPECT().DelegateCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).AnyTimes() mockBankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()).AnyTimes() + mockAccountKeeper := mocks.NewMockAccountKeeper(ctrl) + mockAccountKeeper.EXPECT().GetAccount(gomock.Any(), gomock.Any()).AnyTimes() + + mockGatewayKeeper := mocks.NewMockGatewayKeeper(ctrl) + mockGatewayKeeper.EXPECT().GetGateway(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ sdk.Context, addr string) (gatewaytypes.Gateway, bool) { + if _, ok := StakedGatewayMap[addr]; !ok { + return gatewaytypes.Gateway{}, false + } + stake := sdk.NewCoin("upokt", sdk.NewInt(10000)) + return gatewaytypes.Gateway{ + Address: addr, + Stake: &stake, + }, true + }, + ).AnyTimes() + paramsSubspace := typesparams.NewSubspace(cdc, types.Amino, storeKey, @@ -50,6 +73,8 @@ func ApplicationKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { memStoreKey, paramsSubspace, mockBankKeeper, + mockAccountKeeper, + mockGatewayKeeper, ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) diff --git a/testutil/keeper/gateway.go b/testutil/keeper/gateway.go index cd2e3a3eb..2a7f27ea9 100644 --- a/testutil/keeper/gateway.go +++ b/testutil/keeper/gateway.go @@ -51,7 +51,6 @@ func GatewayKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { memStoreKey, paramsSubspace, mockBankKeeper, - nil, ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) diff --git a/x/application/client/cli/tx_delegate_to_gateway.go b/x/application/client/cli/tx_delegate_to_gateway.go index 8291df1a5..f993739db 100644 --- a/x/application/client/cli/tx_delegate_to_gateway.go +++ b/x/application/client/cli/tx_delegate_to_gateway.go @@ -7,6 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" + "pocket/x/application/types" ) @@ -14,11 +15,17 @@ var _ = strconv.Itoa(0) func CmdDelegateToGateway() *cobra.Command { cmd := &cobra.Command{ - Use: "delegate-to-gateway", - Short: "Broadcast message delegate-to-gateway", - Args: cobra.ExactArgs(0), + Use: "delegate-to-gateway [gateway address]", + Short: "Delegate an application to a gateway", + Long: `Delegate an application to the gateway with the provided address. This is a broadcast operation +that delegates authority to the gateway specified to sign relays requests for the application, allowing the gateway +act on the behalf of the application during a session. + +Example: +$ pocketd --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { - + gatewayAddress := args[0] clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err @@ -26,10 +33,12 @@ func CmdDelegateToGateway() *cobra.Command { msg := types.NewMsgDelegateToGateway( clientCtx.GetFromAddress().String(), + gatewayAddress, ) if err := msg.ValidateBasic(); err != nil { return err } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } diff --git a/x/application/client/cli/tx_delegate_to_gateway_test.go b/x/application/client/cli/tx_delegate_to_gateway_test.go new file mode 100644 index 000000000..b70f4558b --- /dev/null +++ b/x/application/client/cli/tx_delegate_to_gateway_test.go @@ -0,0 +1,117 @@ +package cli_test + +import ( + "fmt" + "testing" + + sdkerrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/testutil" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/status" + + "pocket/testutil/network" + "pocket/x/application/client/cli" + "pocket/x/application/types" +) + +func TestCLI_DelegateToGateway(t *testing.T) { + net, _ := networkWithApplicationObjects(t, 2) + val := net.Validators[0] + ctx := val.ClientCtx + + // Create a keyring and add an account for the application to be delegated + // and the gateway to be delegated to + kr := ctx.Keyring + accounts := testutil.CreateKeyringAccounts(t, kr, 2) + appAccount := accounts[0] + gatewayAccount := accounts[1] + + // Update the context with the new keyring + ctx = ctx.WithKeyring(kr) + + // Common args used for all requests + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(net.Config.BondDenom, sdkmath.NewInt(10))).String()), + } + + tests := []struct { + desc string + appAddress string + gatewayAddress string + err *sdkerrors.Error + }{ + { + desc: "delegate to gateway: valid", + appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + }, + { + desc: "invalid - missing app address", + // appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - invalid app address", + appAddress: "invalid address", + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - missing gateway address", + appAddress: appAccount.Address.String(), + // gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidGatewayAddress, + }, + { + desc: "invalid - invalid gateway address", + appAddress: appAccount.Address.String(), + gatewayAddress: "invalid address", + err: types.ErrAppInvalidGatewayAddress, + }, + } + + // Initialize the App and Gateway Accounts by sending it some funds from the validator account that is part of genesis + network.InitAccount(t, net, appAccount.Address) + network.InitAccount(t, net, gatewayAccount.Address) + + // Run the tests + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + // Wait for a new block to be committed + require.NoError(t, net.WaitForNextBlock()) + + // Prepare the arguments for the CLI command + args := []string{ + tt.gatewayAddress, + fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.appAddress), + } + args = append(args, commonArgs...) + + // Execute the command + delegateOutput, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdDelegateToGateway(), args) + + // Validate the error if one is expected + if tt.err != nil { + stat, ok := status.FromError(tt.err) + require.True(t, ok) + require.Contains(t, stat.Message(), tt.err.Error()) + return + } + require.NoError(t, err) + + // Check the response + var resp sdk.TxResponse + require.NoError(t, net.Config.Codec.UnmarshalJSON(delegateOutput.Bytes(), &resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.TxHash) + require.Equal(t, uint32(0), resp.Code) + }) + } +} diff --git a/x/application/keeper/keeper.go b/x/application/keeper/keeper.go index d115dddd5..6d0f54b47 100644 --- a/x/application/keeper/keeper.go +++ b/x/application/keeper/keeper.go @@ -19,7 +19,9 @@ type ( memKey storetypes.StoreKey paramstore paramtypes.Subspace - bankKeeper types.BankKeeper + bankKeeper types.BankKeeper + accountKeeper types.AccountKeeper + gatewayKeeper types.GatewayKeeper } ) @@ -30,6 +32,8 @@ func NewKeeper( ps paramtypes.Subspace, bankKeeper types.BankKeeper, + accountKeeper types.AccountKeeper, + gatewayKeeper types.GatewayKeeper, ) *Keeper { // set KeyTable if it has not already been set if !ps.HasKeyTable() { @@ -42,7 +46,9 @@ func NewKeeper( memKey: memKey, paramstore: ps, - bankKeeper: bankKeeper, + bankKeeper: bankKeeper, + accountKeeper: accountKeeper, + gatewayKeeper: gatewayKeeper, } } diff --git a/x/application/keeper/msg_server_delegate_to_gateway.go b/x/application/keeper/msg_server_delegate_to_gateway.go index 732bd5a4e..41b96e1ef 100644 --- a/x/application/keeper/msg_server_delegate_to_gateway.go +++ b/x/application/keeper/msg_server_delegate_to_gateway.go @@ -3,20 +3,52 @@ package keeper import ( "context" - sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + + sdkerrors "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" ) func (k msgServer) DelegateToGateway(goCtx context.Context, msg *types.MsgDelegateToGateway) (*types.MsgDelegateToGatewayResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + logger := k.Logger(ctx).With("method", "DelegateToGateway") + logger.Info("About to delegate application to gateway with msg: %v", msg) + if err := msg.ValidateBasic(); err != nil { + logger.Error("Delegation Message failed basic validation: %v", err) return nil, err } - // TODO: Handling the message - _ = ctx + // Retrieve the application from the store + app, found := k.GetApplication(ctx, msg.AppAddress) + if !found { + logger.Info("Application not found with address [%s]", msg.AppAddress) + return nil, sdkerrors.Wrapf(types.ErrAppNotFound, "application not found with address: %s", msg.AppAddress) + } + logger.Info("Application found with address [%s]", msg.AppAddress) + + // Check if the gateway is staked + if _, found := k.gatewayKeeper.GetGateway(ctx, msg.GatewayAddress); !found { + logger.Info("Gateway not found with address [%s]", msg.GatewayAddress) + return nil, sdkerrors.Wrapf(types.ErrAppGatewayNotFound, "gateway not found with address: %s", msg.GatewayAddress) + } + + // Check if the application is already delegated to the gateway + for _, gatewayAddr := range app.DelegateeGatewayAddresses { + if gatewayAddr == msg.GatewayAddress { + logger.Info("Application already delegated to gateway with address [%s]", msg.GatewayAddress) + return nil, sdkerrors.Wrapf(types.ErrAppAlreadyDelegated, "application already delegated to gateway with address: %s", msg.GatewayAddress) + } + } + + // Update the application with the new delegatee public key + app.DelegateeGatewayAddresses = append(app.DelegateeGatewayAddresses, msg.GatewayAddress) + logger.Info("Successfully added delegatee public key to application") + + // Update the application store with the new delegation + k.SetApplication(ctx, app) + logger.Info("Successfully delegated application to gateway for app: %+v", app) return &types.MsgDelegateToGatewayResponse{}, nil } diff --git a/x/application/keeper/msg_server_delegate_to_gateway_test.go b/x/application/keeper/msg_server_delegate_to_gateway_test.go new file mode 100644 index 000000000..788b740b8 --- /dev/null +++ b/x/application/keeper/msg_server_delegate_to_gateway_test.go @@ -0,0 +1,184 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + keepertest "pocket/testutil/keeper" + "pocket/testutil/sample" + "pocket/x/application/keeper" + "pocket/x/application/types" + sharedtypes "pocket/x/shared/types" +) + +func TestMsgServer_DelegateToGateway_SuccessfullyDelegate(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateways + appAddr := sample.AccAddress() + gatewayAddr1 := sample.AccAddress() + gatewayAddr2 := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr1] = struct{}{} + keepertest.StakedGatewayMap[gatewayAddr2] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr1) + delete(keepertest.StakedGatewayMap, gatewayAddr2) + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr1, + } + + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Verify that the application exists + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr1, foundApp.DelegateeGatewayAddresses[0]) + + // Prepare a second delegation message + delegateMsg2 := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr2, + } + + // Delegate the application to the second gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg2) + require.NoError(t, err) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, 2, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr1, foundApp.DelegateeGatewayAddresses[0]) + require.Equal(t, gatewayAddr2, foundApp.DelegateeGatewayAddresses[1]) +} + +func TestMsgServer_DelegateToGateway_FailDuplicate(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateway + appAddr := sample.AccAddress() + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr) + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Verify that the application exists + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[0]) + + // Prepare a second delegation message + delegateMsg2 := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Attempt to delegate the application to the gateway again + _, err = srv.DelegateToGateway(wctx, delegateMsg2) + require.Error(t, err) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[0]) +} + +func TestMsgServer_DelegateToGateway_FailGatewayNotStaked(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateway + appAddr := sample.AccAddress() + gatewayAddr := sample.AccAddress() + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Attempt to delegate the application to the unstaked gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.Error(t, err) + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses)) +} diff --git a/x/application/keeper/msg_server_stake_application.go b/x/application/keeper/msg_server_stake_application.go index 6aa824e24..1d6fbcaf1 100644 --- a/x/application/keeper/msg_server_stake_application.go +++ b/x/application/keeper/msg_server_stake_application.go @@ -67,9 +67,10 @@ func (k msgServer) createApplication( msg *types.MsgStakeApplication, ) types.Application { return types.Application{ - Address: msg.Address, - Stake: msg.Stake, - ServiceConfigs: msg.Services, + Address: msg.Address, + Stake: msg.Stake, + ServiceConfigs: msg.Services, + DelegateeGatewayAddresses: make([]string, 0), } } diff --git a/x/application/simulation/delegate_to_gateway.go b/x/application/simulation/delegate_to_gateway.go index 35ea46bee..b9e337d58 100644 --- a/x/application/simulation/delegate_to_gateway.go +++ b/x/application/simulation/delegate_to_gateway.go @@ -3,11 +3,12 @@ package simulation import ( "math/rand" + "pocket/x/application/keeper" + "pocket/x/application/types" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/application/keeper" - "pocket/x/application/types" ) func SimulateMsgDelegateToGateway( @@ -17,9 +18,11 @@ func SimulateMsgDelegateToGateway( ) simtypes.Operation { return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - simAccount, _ := simtypes.RandomAcc(r, accs) + simAppAccount, _ := simtypes.RandomAcc(r, accs) + simGatewayAccount, _ := simtypes.RandomAcc(r, accs) msg := &types.MsgDelegateToGateway{ - Address: simAccount.Address.String(), + AppAddress: simAppAccount.Address.String(), + GatewayAddress: simGatewayAccount.Address.String(), } // TODO: Handling the DelegateToGateway simulation diff --git a/x/application/types/errors.go b/x/application/types/errors.go index 0a84948da..7ed7fc8e2 100644 --- a/x/application/types/errors.go +++ b/x/application/types/errors.go @@ -12,5 +12,8 @@ var ( ErrAppInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid application address") ErrAppUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized application signer") ErrAppNotFound = sdkerrors.Register(ModuleName, 4, "application not found") - ErrAppInvalidServiceConfigs = sdkerrors.Register(ModuleName, 5, "invalid service configs") + ErrAppInvalidServiceConfigs = sdkerrors.Register(ModuleName, 6, "invalid service configs") + ErrAppGatewayNotFound = sdkerrors.Register(ModuleName, 7, "gateway not found") + ErrAppInvalidGatewayAddress = sdkerrors.Register(ModuleName, 8, "invalid gateway address") + ErrAppAlreadyDelegated = sdkerrors.Register(ModuleName, 9, "application already delegated to gateway") ) diff --git a/x/application/types/expected_keepers.go b/x/application/types/expected_keepers.go index ff977bf18..ab3cd615d 100644 --- a/x/application/types/expected_keepers.go +++ b/x/application/types/expected_keepers.go @@ -1,10 +1,12 @@ package types -//go:generate mockgen -destination ../../../testutil/application/mocks/expected_keepers_mock.go -package mocks . AccountKeeper,BankKeeper +//go:generate mockgen -destination ../../../testutil/application/mocks/expected_keepers_mock.go -package mocks . AccountKeeper,BankKeeper,GatewayKeeper import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" + + gatewaytypes "pocket/x/gateway/types" ) // AccountKeeper defines the expected account keeper used for simulations (noalias) @@ -17,3 +19,8 @@ type BankKeeper interface { DelegateCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error } + +// GatewayKeeper defines the expected interface needed to retrieve gateway information. +type GatewayKeeper interface { + GetGateway(ctx sdk.Context, addr string) (gatewaytypes.Gateway, bool) +} diff --git a/x/application/types/genesis.go b/x/application/types/genesis.go index 1d8d5f892..38d2b25dd 100644 --- a/x/application/types/genesis.go +++ b/x/application/types/genesis.go @@ -34,7 +34,7 @@ func (gs GenesisState) Validate() error { applicationIndexMap[index] = struct{}{} } - // Check that the stake value for the apps is valid + // Check that the stake value for the apps is valid and that the delegatee pubkeys are valid for _, app := range gs.ApplicationList { // TODO_TECHDEBT: Consider creating shared helpers across the board for stake validation, // similar to how we have `AreValidAppServiceConfigs` below @@ -55,7 +55,13 @@ func (gs GenesisState) Validate() error { return sdkerrors.Wrapf(ErrAppInvalidStake, "invalid stake amount denom for application %v", app.Stake) } - // Valid the application service configs + // Check that the application's delegated gateway addresses are valid + for _, gatewayAddr := range app.DelegateeGatewayAddresses { + if _, err := sdk.AccAddressFromBech32(gatewayAddr); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidGatewayAddress, "invalid gateway address %s; (%v)", gatewayAddr, err) + } + } + // Validate the application service configs if reason, ok := servicehelpers.AreValidAppServiceConfigs(app.ServiceConfigs); !ok { return sdkerrors.Wrapf(ErrAppInvalidStake, reason) diff --git a/x/application/types/genesis_test.go b/x/application/types/genesis_test.go index f1eed28b3..90790e842 100644 --- a/x/application/types/genesis_test.go +++ b/x/application/types/genesis_test.go @@ -24,6 +24,10 @@ func TestGenesisState_Validate(t *testing.T) { ServiceId: &sharedtypes.ServiceId{Id: "svc2"}, } + emptyDelegatees := make([]string, 0) + gatewayAddr1 := sample.AccAddress() + gatewayAddr2 := sample.AccAddress() + tests := []struct { desc string genState *types.GenesisState @@ -37,17 +41,18 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "valid genesis state", genState: &types.GenesisState{ - ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: []string{gatewayAddr1, gatewayAddr2}, }, { - Address: addr2, - Stake: &stake2, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + Address: addr2, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: []string{gatewayAddr2, gatewayAddr1}, }, }, // this line is used by starport scaffolding # types/genesis/validField @@ -59,14 +64,16 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -77,14 +84,16 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -95,14 +104,16 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + Address: addr2, + Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -113,14 +124,16 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + Address: addr2, + Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -131,14 +144,16 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr1, - Stake: &stake2, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + Address: addr1, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -149,14 +164,16 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr2, - Stake: nil, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + Address: addr2, + Stake: nil, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -167,14 +184,56 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { Address: addr2, // Explicitly missing stake - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - due to invalid delegatee pub key", + genState: &types.GenesisState{ + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, + }, + { + Address: addr2, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: []string{"invalid address"}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - due to invalid delegatee pub keys", + genState: &types.GenesisState{ + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: []string{gatewayAddr1}, + }, + { + Address: addr2, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: []string{"invalid address", gatewayAddr2}, }, }, }, @@ -188,6 +247,7 @@ func TestGenesisState_Validate(t *testing.T) { Address: addr1, Stake: &stake1, // ServiceConfigs: omitted + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -198,9 +258,10 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, - ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{}, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -212,10 +273,11 @@ func TestGenesisState_Validate(t *testing.T) { ApplicationList: []types.Application{ { Address: addr1, - Stake: &stake1, + Stake: &stake1, ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ {ServiceId: &sharedtypes.ServiceId{Id: "12345678901"}}, }, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -234,6 +296,7 @@ func TestGenesisState_Validate(t *testing.T) { Name: "abcdefghijklmnopqrstuvwxyzab-abcdefghijklmnopqrstuvwxyzab", }}, }, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -249,6 +312,7 @@ func TestGenesisState_Validate(t *testing.T) { ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ {ServiceId: &sharedtypes.ServiceId{Id: "12 45 !"}}, }, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, diff --git a/x/application/types/message_delegate_to_gateway.go b/x/application/types/message_delegate_to_gateway.go index 232564310..652b5baa6 100644 --- a/x/application/types/message_delegate_to_gateway.go +++ b/x/application/types/message_delegate_to_gateway.go @@ -1,17 +1,18 @@ package types import ( + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) const TypeMsgDelegateToGateway = "delegate_to_gateway" var _ sdk.Msg = (*MsgDelegateToGateway)(nil) -func NewMsgDelegateToGateway(address string) *MsgDelegateToGateway { +func NewMsgDelegateToGateway(appAddress, gatewayAddress string) *MsgDelegateToGateway { return &MsgDelegateToGateway{ - Address: address, + AppAddress: appAddress, + GatewayAddress: gatewayAddress, } } @@ -24,7 +25,7 @@ func (msg *MsgDelegateToGateway) Type() string { } func (msg *MsgDelegateToGateway) GetSigners() []sdk.AccAddress { - address, err := sdk.AccAddressFromBech32(msg.Address) + address, err := sdk.AccAddressFromBech32(msg.AppAddress) if err != nil { panic(err) } @@ -37,9 +38,13 @@ func (msg *MsgDelegateToGateway) GetSignBytes() []byte { } func (msg *MsgDelegateToGateway) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(msg.Address) - if err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid address address (%s)", err) + // Validate the application address + if _, err := sdk.AccAddressFromBech32(msg.AppAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidAddress, "invalid application address %s; (%v)", msg.AppAddress, err) + } + // Validate the gateway address + if _, err := sdk.AccAddressFromBech32(msg.GatewayAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidGatewayAddress, "invalid gateway address %s; (%v)", msg.GatewayAddress, err) } return nil } diff --git a/x/application/types/message_delegate_to_gateway_test.go b/x/application/types/message_delegate_to_gateway_test.go index 770d801b2..bf1254a29 100644 --- a/x/application/types/message_delegate_to_gateway_test.go +++ b/x/application/types/message_delegate_to_gateway_test.go @@ -3,9 +3,9 @@ package types import ( "testing" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/stretchr/testify/require" "pocket/testutil/sample" + + "github.com/stretchr/testify/require" ) func TestMsgDelegateToGateway_ValidateBasic(t *testing.T) { @@ -15,15 +15,31 @@ func TestMsgDelegateToGateway_ValidateBasic(t *testing.T) { err error }{ { - name: "invalid address", + name: "invalid app address - no gateway address", + msg: MsgDelegateToGateway{ + AppAddress: "invalid_address", + // GatewayAddress: intentionally omitted, + }, + err: ErrAppInvalidAddress, + }, { + name: "valid app address - no gateway address", + msg: MsgDelegateToGateway{ + AppAddress: sample.AccAddress(), + // GatewayAddress: intentionally omitted, + }, + err: ErrAppInvalidGatewayAddress, + }, { + name: "valid app address - invalid gateway address", msg: MsgDelegateToGateway{ - Address: "invalid_address", + AppAddress: sample.AccAddress(), + GatewayAddress: "invalid_address", }, - err: sdkerrors.ErrInvalidAddress, + err: ErrAppInvalidGatewayAddress, }, { name: "valid address", msg: MsgDelegateToGateway{ - Address: sample.AccAddress(), + AppAddress: sample.AccAddress(), + GatewayAddress: sample.AccAddress(), }, }, } diff --git a/x/gateway/keeper/keeper.go b/x/gateway/keeper/keeper.go index c132d3a48..ea74f170b 100644 --- a/x/gateway/keeper/keeper.go +++ b/x/gateway/keeper/keeper.go @@ -19,8 +19,7 @@ type ( memKey storetypes.StoreKey paramstore paramtypes.Subspace - bankKeeper types.BankKeeper - accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper } ) @@ -31,7 +30,6 @@ func NewKeeper( ps paramtypes.Subspace, bankKeeper types.BankKeeper, - accountKeeper types.AccountKeeper, ) *Keeper { // set KeyTable if it has not already been set if !ps.HasKeyTable() { @@ -44,8 +42,7 @@ func NewKeeper( memKey: memKey, paramstore: ps, - bankKeeper: bankKeeper, - accountKeeper: accountKeeper, + bankKeeper: bankKeeper, } } From 0f139ee992de4cf1a2b31ec762fb4a45473d2a4e Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Tue, 31 Oct 2023 14:42:11 -0700 Subject: [PATCH 13/28] [Test] Temporarily skip a flaky test `TestEventsQueryClient_Subscribe_Succeeds` (#121) See #120 for details --- go.mod | 4 ++-- pkg/client/events_query/client_test.go | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index d2fd3985c..cb8df3174 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( cosmossdk.io/math v1.0.1 github.com/cometbft/cometbft v0.37.2 github.com/cometbft/cometbft-db v0.8.0 - github.com/cosmos/cosmos-proto v1.0.0-beta.2 github.com/cosmos/cosmos-sdk v0.47.3 github.com/cosmos/gogoproto v1.4.10 github.com/cosmos/ibc-go/v7 v7.1.0 @@ -26,7 +25,6 @@ require ( go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.12.0 golang.org/x/sync v0.3.0 - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 google.golang.org/grpc v1.56.1 gopkg.in/yaml.v2 v2.4.0 ) @@ -71,6 +69,7 @@ require ( github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.0 // indirect @@ -266,6 +265,7 @@ require ( gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/api v0.122.0 // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/client/events_query/client_test.go b/pkg/client/events_query/client_test.go index 393e68813..4d3b41b30 100644 --- a/pkg/client/events_query/client_test.go +++ b/pkg/client/events_query/client_test.go @@ -23,6 +23,8 @@ import ( ) func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { + t.Skip("TODO_BUG(@bryanchriswhite): See #120 for more details") + var ( readObserverEventsTimeout = time.Second queryCounter int From aef9615fd33fba22e99b8c69b96b5f4ac99ed152 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Tue, 31 Oct 2023 22:00:11 +0000 Subject: [PATCH 14/28] [AppGate] Add the MaxDelegatedGateways parameter (#109) --- config.yml | 2 + docs/static/openapi.yml | 19 +++++ proto/pocket/application/params.proto | 3 +- testutil/keeper/application.go | 1 + .../keeper/msg_server_delegate_to_gateway.go | 7 ++ .../msg_server_delegate_to_gateway_test.go | 72 ++++++++++++++++++- x/application/types/errors.go | 18 ++--- x/application/types/genesis_test.go | 54 ++++++++++++++ x/application/types/params.go | 9 ++- 9 files changed, 173 insertions(+), 12 deletions(-) diff --git a/config.yml b/config.yml index 4ad66fbc9..609860430 100644 --- a/config.yml +++ b/config.yml @@ -79,6 +79,8 @@ genesis: - amount: "10000" denom: upokt application: + params: + maxDelegatedGateways: 7 applicationList: - address: pokt1mrqt5f7qh8uxs27cjm9t7v9e74a9vvdnq5jva4 stake: diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index 3e0e46e33..d6f879b53 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46735,6 +46735,13 @@ paths: params: description: params holds all the parameters of this module. type: object + properties: + max_delegated_gateways: + type: integer + format: int64 + title: >- + The maximum number of gateways an application can delegate + trust to description: >- QueryParamsResponse is response type for the Query/Params RPC method. @@ -77831,6 +77838,11 @@ definitions: type: object pocket.application.Params: type: object + properties: + max_delegated_gateways: + type: integer + format: int64 + title: The maximum number of gateways an application can delegate trust to description: Params defines the parameters for the module. pocket.application.QueryAllApplicationResponse: type: object @@ -78004,6 +78016,13 @@ definitions: params: description: params holds all the parameters of this module. type: object + properties: + max_delegated_gateways: + type: integer + format: int64 + title: >- + The maximum number of gateways an application can delegate trust + to description: QueryParamsResponse is response type for the Query/Params RPC method. pocket.shared.ApplicationServiceConfig: type: object diff --git a/proto/pocket/application/params.proto b/proto/pocket/application/params.proto index 608ca124d..71d2f69ff 100644 --- a/proto/pocket/application/params.proto +++ b/proto/pocket/application/params.proto @@ -8,5 +8,6 @@ option go_package = "pocket/x/application/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + + int64 max_delegated_gateways = 1 [(gogoproto.jsontag) = "max_delegated_gateways"]; // The maximum number of gateways an application can delegate trust to } diff --git a/testutil/keeper/application.go b/testutil/keeper/application.go index 71b06b446..0546b27a7 100644 --- a/testutil/keeper/application.go +++ b/testutil/keeper/application.go @@ -24,6 +24,7 @@ import ( // StakedGatewayMap is used to mock whether a gateway is staked or not for use // in the application's mocked gateway keeper. This enables the tester to // control whether a gateway is "staked" or not and whether it can be delegated to +// WARNING: Using this map may cause issues if running multiple tests in parallel var StakedGatewayMap = make(map[string]struct{}) func ApplicationKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { diff --git a/x/application/keeper/msg_server_delegate_to_gateway.go b/x/application/keeper/msg_server_delegate_to_gateway.go index 41b96e1ef..1fe9bb786 100644 --- a/x/application/keeper/msg_server_delegate_to_gateway.go +++ b/x/application/keeper/msg_server_delegate_to_gateway.go @@ -34,6 +34,13 @@ func (k msgServer) DelegateToGateway(goCtx context.Context, msg *types.MsgDelega return nil, sdkerrors.Wrapf(types.ErrAppGatewayNotFound, "gateway not found with address: %s", msg.GatewayAddress) } + // Ensure the application is not already delegated to the maximum number of gateways + maxDelegatedParam := k.GetParams(ctx).MaxDelegatedGateways + if int64(len(app.DelegateeGatewayAddresses)) >= maxDelegatedParam { + logger.Info("Application already delegated to maximum number of gateways: %d", maxDelegatedParam) + return nil, sdkerrors.Wrapf(types.ErrAppMaxDelegatedGateways, "application already delegated to %d gateways", maxDelegatedParam) + } + // Check if the application is already delegated to the gateway for _, gatewayAddr := range app.DelegateeGatewayAddresses { if gatewayAddr == msg.GatewayAddress { diff --git a/x/application/keeper/msg_server_delegate_to_gateway_test.go b/x/application/keeper/msg_server_delegate_to_gateway_test.go index 788b740b8..470c8e572 100644 --- a/x/application/keeper/msg_server_delegate_to_gateway_test.go +++ b/x/application/keeper/msg_server_delegate_to_gateway_test.go @@ -136,7 +136,7 @@ func TestMsgServer_DelegateToGateway_FailDuplicate(t *testing.T) { // Attempt to delegate the application to the gateway again _, err = srv.DelegateToGateway(wctx, delegateMsg2) - require.Error(t, err) + require.ErrorIs(t, err, types.ErrAppAlreadyDelegated) foundApp, isAppFound = k.GetApplication(ctx, appAddr) require.True(t, isAppFound) require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) @@ -177,8 +177,76 @@ func TestMsgServer_DelegateToGateway_FailGatewayNotStaked(t *testing.T) { // Attempt to delegate the application to the unstaked gateway _, err = srv.DelegateToGateway(wctx, delegateMsg) - require.Error(t, err) + require.ErrorIs(t, err, types.ErrAppGatewayNotFound) foundApp, isAppFound := k.GetApplication(ctx, appAddr) require.True(t, isAppFound) require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses)) } + +func TestMsgServer_DelegateToGateway_FailMaxReached(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateway + appAddr := sample.AccAddress() + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr) + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Delegate the application to the max number of gateways + maxDelegatedParam := k.GetParams(ctx).MaxDelegatedGateways + for i := int64(0); i < k.GetParams(ctx).MaxDelegatedGateways; i++ { + // Prepare the delegation message + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr) + }) + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + // Check number of gateways delegated to is correct + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, int(i+1), len(foundApp.DelegateeGatewayAddresses)) + } + + // Attempt to delegate the application when the max is already reached + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.ErrorIs(t, err, types.ErrAppMaxDelegatedGateways) + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, maxDelegatedParam, int64(len(foundApp.DelegateeGatewayAddresses))) +} diff --git a/x/application/types/errors.go b/x/application/types/errors.go index 7ed7fc8e2..3445ec6f4 100644 --- a/x/application/types/errors.go +++ b/x/application/types/errors.go @@ -8,12 +8,14 @@ import ( // x/application module sentinel errors var ( - ErrAppInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid application stake") - ErrAppInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid application address") - ErrAppUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized application signer") - ErrAppNotFound = sdkerrors.Register(ModuleName, 4, "application not found") - ErrAppInvalidServiceConfigs = sdkerrors.Register(ModuleName, 6, "invalid service configs") - ErrAppGatewayNotFound = sdkerrors.Register(ModuleName, 7, "gateway not found") - ErrAppInvalidGatewayAddress = sdkerrors.Register(ModuleName, 8, "invalid gateway address") - ErrAppAlreadyDelegated = sdkerrors.Register(ModuleName, 9, "application already delegated to gateway") + ErrAppInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid application stake") + ErrAppInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid application address") + ErrAppUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized application signer") + ErrAppNotFound = sdkerrors.Register(ModuleName, 4, "application not found") + ErrAppInvalidServiceConfigs = sdkerrors.Register(ModuleName, 6, "invalid service configs") + ErrAppGatewayNotFound = sdkerrors.Register(ModuleName, 7, "gateway not found") + ErrAppInvalidGatewayAddress = sdkerrors.Register(ModuleName, 8, "invalid gateway address") + ErrAppAlreadyDelegated = sdkerrors.Register(ModuleName, 9, "application already delegated to gateway") + ErrAppMaxDelegatedGateways = sdkerrors.Register(ModuleName, 10, "maximum number of delegated gateways reached") + ErrAppInvalidMaxDelegatedGateways = sdkerrors.Register(ModuleName, 11, "invalid MaxDelegatedGateways parameter") ) diff --git a/x/application/types/genesis_test.go b/x/application/types/genesis_test.go index 90790e842..74c31c2ed 100644 --- a/x/application/types/genesis_test.go +++ b/x/application/types/genesis_test.go @@ -41,6 +41,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "valid genesis state", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -62,6 +65,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - zero app stake", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -82,6 +88,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - negative application stake", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -102,6 +111,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - wrong stake denom", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -122,6 +134,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - missing denom", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -142,6 +157,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - due to duplicated app address", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -162,6 +180,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - due to nil app stake", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -182,6 +203,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - due to missing app stake", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -202,6 +226,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - due to invalid delegatee pub key", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -222,6 +249,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - due to invalid delegatee pub keys", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -242,6 +272,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - service config not present", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -256,6 +289,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - empty service config", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -270,6 +306,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - service ID too long", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -286,6 +325,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - service name too long", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -305,6 +347,9 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - service ID with invalid characters", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, @@ -318,6 +363,15 @@ func TestGenesisState_Validate(t *testing.T) { }, valid: false, }, + { + desc: "invalid - MaxDelegatedGateways less than 1", + genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 0, + }, + }, + valid: false, + }, // this line is used by starport scaffolding # types/genesis/testcase } diff --git a/x/application/types/params.go b/x/application/types/params.go index 357196ad6..f5ec7cd0c 100644 --- a/x/application/types/params.go +++ b/x/application/types/params.go @@ -1,10 +1,14 @@ package types import ( + sdkerrors "cosmossdk.io/errors" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "gopkg.in/yaml.v2" ) +// TODO: Revisit default param values +const DefaultMaxDelegatedGateways int64 = 7 + var _ paramtypes.ParamSet = (*Params)(nil) // ParamKeyTable the param key table for launch module @@ -14,7 +18,7 @@ func ParamKeyTable() paramtypes.KeyTable { // NewParams creates a new Params instance func NewParams() Params { - return Params{} + return Params{MaxDelegatedGateways: DefaultMaxDelegatedGateways} } // DefaultParams returns a default set of parameters @@ -29,6 +33,9 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { // Validate validates the set of params func (p Params) Validate() error { + if p.MaxDelegatedGateways < 1 { + return sdkerrors.Wrapf(ErrAppInvalidMaxDelegatedGateways, "MaxDelegatedGateways param < 1: got %d", p.MaxDelegatedGateways) + } return nil } From c43fdbc712f601dafbeeec693f903c2e7ea1e4fc Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Tue, 31 Oct 2023 15:54:43 -0700 Subject: [PATCH 15/28] [Supplier] Add `ServiceConfigs` to `SupplierStaking` (#114) Update all related components (CLI, Tx, Message, etc...) so suppliers can stake for a service --- Co-authored-by: harry <53987565+h5law@users.noreply.github.com> Co-authored-by: Daniel Olshansky Co-authored-by: Bryan White --- Makefile | 24 +- Tiltfile | 103 ++++++-- proto/pocket/supplier/tx.proto | 5 +- testutil/keeper/session.go | 2 + testutil/network/network.go | 28 +- .../client/cli/tx_stake_application_test.go | 8 +- x/application/genesis_test.go | 11 + .../keeper/msg_server_stake_application.go | 4 +- .../msg_server_stake_application_test.go | 20 +- .../msg_server_unstake_application_test.go | 8 +- x/application/types/genesis.go | 8 +- .../types/message_stake_application.go | 6 +- .../types/message_stake_application_test.go | 24 +- x/shared/helpers/service.go | 25 +- x/shared/helpers/service_configs.go | 85 +++++- x/shared/helpers/service_test.go | 53 ++++ x/supplier/client/cli/tx_stake_supplier.go | 51 +++- .../client/cli/tx_stake_supplier_test.go | 134 ++++++++-- x/supplier/genesis_test.go | 29 +++ .../keeper/msg_server_stake_supplier.go | 17 +- .../keeper/msg_server_stake_supplier_test.go | 159 +++++++++++- .../msg_server_unstake_supplier_test.go | 16 ++ x/supplier/keeper/supplier.go | 26 +- x/supplier/keeper/supplier_test.go | 72 ++++-- x/supplier/simulation/stake_supplier.go | 1 + x/supplier/types/errors.go | 9 +- x/supplier/types/genesis.go | 9 + x/supplier/types/genesis_test.go | 200 ++++++++++++--- x/supplier/types/message_stake_supplier.go | 16 +- .../types/message_stake_supplier_test.go | 242 +++++++++++++++++- 30 files changed, 1184 insertions(+), 211 deletions(-) diff --git a/Makefile b/Makefile index ba08bdf84..be2e63282 100644 --- a/Makefile +++ b/Makefile @@ -256,15 +256,15 @@ app_stake: ## Stake tokens for the application specified (must specify the APP a .PHONY: app1_stake app1_stake: ## Stake app1 - SERVICES=svc1,svc2 APP=app1 make app_stake + APP=app1 SERVICES=anvil,svc1,svc2 make app_stake .PHONY: app2_stake app2_stake: ## Stake app2 - SERVICES=svc2,svc3 APP=app2 make app_stake + APP=app2 SERVICES=anvil,svc2,svc3 make app_stake .PHONY: app3_stake app3_stake: ## Stake app3 - SERVICES=svc3,svc4 APP=app3 make app_stake + APP=app3 SERVICES=anvil,svc3,svc4 make app_stake .PHONY: app_unstake app_unstake: ## Unstake an application (must specify the APP env var) @@ -306,21 +306,23 @@ app3_delegate_gateway3: ## Delegate trust to gateway3 supplier_list: ## List all the staked supplier pocketd --home=$(POCKETD_HOME) q supplier list-supplier --node $(POCKET_NODE) +# TODO(@Olshansk, @okdas): Add more services (in addition to anvil) for apps and suppliers to stake for. +# TODO_TECHDEBT: svc1, svc2 and svc3 below are only in place to make GetSession testable .PHONY: supplier_stake supplier_stake: ## Stake tokens for the supplier specified (must specify the APP env var) - pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) + pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt "$(SERVICES)" --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) .PHONY: supplier1_stake supplier1_stake: ## Stake supplier1 - SUPPLIER=supplier1 make supplier_stake + SUPPLIER=supplier1 SERVICES="anvil;http://anvil:8547,svc1;http://localhost:8081" make supplier_stake .PHONY: supplier2_stake supplier2_stake: ## Stake supplier2 - SUPPLIER=supplier2 make supplier_stake + SUPPLIER=supplier2 SERVICES="anvil;http://anvil:8547,svc2;http://localhost:8082" make supplier_stake .PHONY: supplier3_stake supplier3_stake: ## Stake supplier3 - SUPPLIER=supplier3 make supplier_stake + SUPPLIER=supplier3 SERVICES="anvil;http://anvil:8547,svc3;http://localhost:8083" make supplier_stake .PHONY: supplier_unstake supplier_unstake: ## Unstake an supplier (must specify the SUPPLIER env var) @@ -350,10 +352,14 @@ acc_balance_query: ## Query the balance of the account specified (make acc_balan @echo "Querying spendable balance for $(ACC)" pocketd --home=$(POCKETD_HOME) q bank spendable-balances $(ACC) --node $(POCKET_NODE) -.PHONY: acc_balance_query_app_module -acc_balance_query_app_module: ## Query the balance of the network level "application" module +.PHONY: acc_balance_query_module_app +acc_balance_query_module_app: ## Query the balance of the network level "application" module make acc_balance_query ACC=pokt1rl3gjgzexmplmds3tq3r3yk84zlwdl6djzgsvm +.PHONY: acc_balance_query_module_supplier +acc_balance_query_module_supplier: ## Query the balance of the network level "supplier" module + make acc_balance_query ACC=pokt1j40dzzmn6cn9kxku7a5tjnud6hv37vesr5ccaa + .PHONY: acc_balance_query_app1 acc_balance_query_app1: ## Query the balance of app1 make acc_balance_query ACC=pokt1mrqt5f7qh8uxs27cjm9t7v9e74a9vvdnq5jva4 diff --git a/Tiltfile b/Tiltfile index 34717c9d2..1fd0dd779 100644 --- a/Tiltfile +++ b/Tiltfile @@ -1,16 +1,16 @@ -load('ext://restart_process', 'docker_build_with_restart') -load('ext://helm_resource', "helm_resource", 'helm_repo') +load("ext://restart_process", "docker_build_with_restart") +load("ext://helm_resource", "helm_resource", "helm_repo") # A list of directories where changes trigger a hot-reload of the sequencer -hot_reload_dirs = ['app', 'cmd', 'tools', 'x'] +hot_reload_dirs = ["app", "cmd", "tools", "x"] # Create a localnet config file from defaults, and if a default configuration doesn't exist, populate it with default values localnet_config_path = "localnet_config.yaml" localnet_config_defaults = { "relayers": {"count": 1}, "gateways": {"count": 1}, - # By default, we use the `helm_repo` function below to point to the remote repository - # but can update it to the locally cloned repo for testing & development + # By default, we use the `helm_repo` function below to point to the remote repository + # but can update it to the locally cloned repo for testing & development "helm_chart_local_repo": {"enabled": False, "path": "../helm-charts"}, } localnet_config_file = read_yaml(localnet_config_path, default=localnet_config_defaults) @@ -35,6 +35,7 @@ if localnet_config["helm_chart_local_repo"]["enabled"]: sequencer_chart = helm_chart_local_repo + "/charts/poktroll-sequencer" poktroll_chart = helm_chart_local_repo + "/charts/poktroll" + # Import files into Kubernetes ConfigMap def read_files_from_directory(directory): files = listdir(directory) @@ -45,31 +46,58 @@ def read_files_from_directory(directory): config_map_data[filename] = content return config_map_data + def generate_config_map_yaml(name, data): config_map_object = { "apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": name}, - "data": data + "data": data, } return encode_yaml(config_map_object) + # Import keyring/keybase files into Kubernetes ConfigMap -k8s_yaml(generate_config_map_yaml("pocketd-keys", read_files_from_directory("localnet/pocketd/keyring-test/"))) # pocketd/keys +k8s_yaml( + generate_config_map_yaml( + "pocketd-keys", read_files_from_directory("localnet/pocketd/keyring-test/") + ) +) # pocketd/keys # Import configuration files into Kubernetes ConfigMap -k8s_yaml(generate_config_map_yaml("pocketd-configs", read_files_from_directory("localnet/pocketd/config/"))) # pocketd/configs +k8s_yaml( + generate_config_map_yaml( + "pocketd-configs", read_files_from_directory("localnet/pocketd/config/") + ) +) # pocketd/configs # Hot reload protobuf changes -local_resource('hot-reload: generate protobufs', 'ignite generate proto-go -y', deps=['proto'], labels=["hot-reloading"]) +local_resource( + "hot-reload: generate protobufs", + "ignite generate proto-go -y", + deps=["proto"], + labels=["hot-reloading"], +) # Hot reload the pocketd binary used by the k8s cluster -local_resource('hot-reload: pocketd', 'GOOS=linux ignite chain build --skip-proto --output=./bin --debug -v', deps=hot_reload_dirs, labels=["hot-reloading"], resource_deps=['hot-reload: generate protobufs']) +local_resource( + "hot-reload: pocketd", + "GOOS=linux ignite chain build --skip-proto --output=./bin --debug -v", + deps=hot_reload_dirs, + labels=["hot-reloading"], + resource_deps=["hot-reload: generate protobufs"], +) # Hot reload the local pocketd binary used by the CLI -local_resource('hot-reload: pocketd - local cli', 'ignite chain build --skip-proto --debug -v -o $(go env GOPATH)/bin', deps=hot_reload_dirs, labels=["hot-reloading"], resource_deps=['hot-reload: generate protobufs']) +local_resource( + "hot-reload: pocketd - local cli", + "ignite chain build --skip-proto --debug -v -o $(go env GOPATH)/bin", + deps=hot_reload_dirs, + labels=["hot-reloading"], + resource_deps=["hot-reload: generate protobufs"], +) # Build an image with a pocketd binary docker_build_with_restart( "pocketd", - '.', + ".", dockerfile_contents="""FROM golang:1.20.8 RUN apt-get -q update && apt-get install -qyy curl jq RUN go install github.com/go-delve/delve/cmd/dlv@latest @@ -77,21 +105,50 @@ COPY bin/pocketd /usr/local/bin/pocketd WORKDIR / """, only=["./bin/pocketd"], - entrypoint=[ - "/bin/sh", "/scripts/pocket.sh" - ], + entrypoint=["/bin/sh", "/scripts/pocket.sh"], live_update=[sync("bin/pocketd", "/usr/local/bin/pocketd")], ) # Run celestia and anvil nodes -k8s_yaml(['localnet/kubernetes/celestia-rollkit.yaml', 'localnet/kubernetes/anvil.yaml']) +k8s_yaml( + ["localnet/kubernetes/celestia-rollkit.yaml", "localnet/kubernetes/anvil.yaml"] +) # Run pocket-specific nodes (sequencer, relayers, etc...) -helm_resource("sequencer", sequencer_chart, flags=['--values=./localnet/kubernetes/values-common.yaml'], image_deps=["pocketd"], image_keys=[('image.repository', 'image.tag')]) -helm_resource("relayers", poktroll_chart, flags=['--values=./localnet/kubernetes/values-common.yaml', '--set=replicaCount=' + str(localnet_config["relayers"]["count"])], image_deps=["pocketd"], image_keys=[('image.repository', 'image.tag')]) +helm_resource( + "sequencer", + sequencer_chart, + flags=["--values=./localnet/kubernetes/values-common.yaml"], + image_deps=["pocketd"], + image_keys=[("image.repository", "image.tag")], +) +helm_resource( + "relayers", + poktroll_chart, + flags=[ + "--values=./localnet/kubernetes/values-common.yaml", + "--set=replicaCount=" + str(localnet_config["relayers"]["count"]), + ], + image_deps=["pocketd"], + image_keys=[("image.repository", "image.tag")], +) -# Configure tilt resources (tilt labels and port forawards) for all of the nodes above -k8s_resource('celestia-rollkit', labels=["blockchains"], port_forwards=['26657', '26658', '26659']) -k8s_resource('sequencer', labels=["blockchains"], resource_deps=['celestia-rollkit'], port_forwards=['36657', '40004']) -k8s_resource('relayers', labels=["blockchains"], resource_deps=['sequencer'], port_forwards=['8545', '8546', '40005']) -k8s_resource('anvil', labels=["blockchains"], port_forwards=['8547']) +# Configure tilt resources (tilt labels and port forwards) for all of the nodes above +k8s_resource( + "celestia-rollkit", + labels=["blockchains"], + port_forwards=["26657", "26658", "26659"], +) +k8s_resource( + "sequencer", + labels=["blockchains"], + resource_deps=["celestia-rollkit"], + port_forwards=["36657", "40004"], +) +k8s_resource( + "relayers", + labels=["blockchains"], + resource_deps=["sequencer"], + port_forwards=["8545", "8546", "40005"], +) +k8s_resource("anvil", labels=["blockchains"], port_forwards=["8547"]) diff --git a/proto/pocket/supplier/tx.proto b/proto/pocket/supplier/tx.proto index d994b03aa..e334cdd53 100644 --- a/proto/pocket/supplier/tx.proto +++ b/proto/pocket/supplier/tx.proto @@ -5,7 +5,9 @@ package pocket.supplier; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; import "cosmos/msg/v1/msg.proto"; + import "pocket/session/session.proto"; +import "pocket/shared/service.proto"; option go_package = "pocket/x/supplier/types"; @@ -22,8 +24,7 @@ message MsgStakeSupplier { string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the supplier using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the supplier has staked. Must be ≥ to the current amount that the supplier has staked (if any) - // TODO(@Olshansk): Update the tx flow to add support for `services` - // repeated service.SupplierServiceConfig services = 3; // The list of services this supplier is staked to provide service for + repeated shared.SupplierServiceConfig services = 3; // The list of services this supplier is staked to provide service for } message MsgStakeSupplierResponse {} diff --git a/testutil/keeper/session.go b/testutil/keeper/session.go index ef18152f6..c25a188a9 100644 --- a/testutil/keeper/session.go +++ b/testutil/keeper/session.go @@ -70,6 +70,7 @@ var ( { Url: TestSupplierUrl, RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), }, }, }, @@ -79,6 +80,7 @@ var ( { Url: TestSupplierUrl, RpcType: sharedtypes.RPCType_GRPC, + Configs: make([]*sharedtypes.ConfigOption, 0), }, }, }, diff --git a/testutil/network/network.go b/testutil/network/network.go index dcd44a5a1..f0c0fb92b 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -2,7 +2,6 @@ package network import ( "fmt" - "strconv" "testing" "time" @@ -23,7 +22,6 @@ import ( "github.com/stretchr/testify/require" "pocket/app" - "pocket/testutil/nullify" "pocket/testutil/sample" apptypes "pocket/x/application/types" gatewaytypes "pocket/x/gateway/types" @@ -117,6 +115,7 @@ func DefaultApplicationModuleGenesisState(t *testing.T, n int) *apptypes.Genesis }, }, } + // TODO_CONSIDERATION: Evaluate whether we need `nullify.Fill` or if we should enforce `(gogoproto.nullable) = false` everywhere // nullify.Fill(&application) state.ApplicationList = append(state.ApplicationList, application) } @@ -131,10 +130,11 @@ func DefaultGatewayModuleGenesisState(t *testing.T, n int) *gatewaytypes.Genesis for i := 0; i < n; i++ { stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i))) gateway := gatewaytypes.Gateway{ - Address: strconv.Itoa(i), + Address: sample.AccAddress(), Stake: &stake, } - nullify.Fill(&gateway) + // TODO_CONSIDERATION: Evaluate whether we need `nullify.Fill` or if we should enforce `(gogoproto.nullable) = false` everywhere + // nullify.Fill(&gateway) state.GatewayList = append(state.GatewayList, gateway) } return state @@ -147,12 +147,24 @@ func DefaultSupplierModuleGenesisState(t *testing.T, n int) *suppliertypes.Genes state := suppliertypes.DefaultGenesis() for i := 0; i < n; i++ { stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i))) - gateway := sharedtypes.Supplier{ - Address: strconv.Itoa(i), + supplier := sharedtypes.Supplier{ + Address: sample.AccAddress(), Stake: &stake, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: fmt.Sprintf("svc%d", i)}, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: fmt.Sprintf("http://localhost:%d", i), + RpcType: sharedtypes.RPCType_JSON_RPC, + }, + }, + }, + }, } - nullify.Fill(&gateway) - state.SupplierList = append(state.SupplierList, gateway) + // TODO_CONSIDERATION: Evaluate whether we need `nullify.Fill` or if we should enforce `(gogoproto.nullable) = false` everywhere + // nullify.Fill(&supplier) + state.SupplierList = append(state.SupplierList, supplier) } return state } diff --git a/x/application/client/cli/tx_stake_application_test.go b/x/application/client/cli/tx_stake_application_test.go index 3721e8ed4..12f9f75d8 100644 --- a/x/application/client/cli/tx_stake_application_test.go +++ b/x/application/client/cli/tx_stake_application_test.go @@ -106,28 +106,28 @@ func TestCLI_StakeApplication(t *testing.T) { address: appAccount.Address.String(), stakeString: "1000upokt", serviceIdsString: "", - err: types.ErrAppInvalidStake, + err: types.ErrAppInvalidServiceConfigs, }, { desc: "services_test: single invalid service contains spaces", address: appAccount.Address.String(), stakeString: "1000upokt", serviceIdsString: "svc1 svc1_part2 svc1_part3", - err: types.ErrAppInvalidStake, + err: types.ErrAppInvalidServiceConfigs, }, { desc: "services_test: one of two services is invalid because it contains spaces", address: appAccount.Address.String(), stakeString: "1000upokt", serviceIdsString: "svc1 svc1_part2,svc2", - err: types.ErrAppInvalidStake, + err: types.ErrAppInvalidServiceConfigs, }, { desc: "services_test: service ID is too long (8 chars is the max)", address: appAccount.Address.String(), stakeString: "1000upokt", serviceIdsString: "svc1,abcdefghi", - err: types.ErrAppInvalidStake, + err: types.ErrAppInvalidServiceConfigs, }, } diff --git a/x/application/genesis_test.go b/x/application/genesis_test.go index 50b2d432c..1c8e883ad 100644 --- a/x/application/genesis_test.go +++ b/x/application/genesis_test.go @@ -11,6 +11,7 @@ import ( "pocket/testutil/sample" "pocket/x/application" "pocket/x/application/types" + sharedtypes "pocket/x/shared/types" ) // Please see `x/application/types/genesis_test.go` for extensive tests related to the validity of the genesis state. @@ -21,10 +22,20 @@ func TestGenesis(t *testing.T) { { Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, }, { Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc2"}, + }, + }, }, }, // this line is used by starport scaffolding # genesis/test/state diff --git a/x/application/keeper/msg_server_stake_application.go b/x/application/keeper/msg_server_stake_application.go index 1d6fbcaf1..6f4a73f64 100644 --- a/x/application/keeper/msg_server_stake_application.go +++ b/x/application/keeper/msg_server_stake_application.go @@ -93,8 +93,8 @@ func (k msgServer) updateApplication( } app.Stake = msg.Stake - // Validate that the service configs maintain at least one service. Additional validation is done in - // `msg.ValidateBasic` above. + // Validate that the service configs maintain at least one service. + // Additional validation is done in `msg.ValidateBasic` above. if len(msg.Services) == 0 { return sdkerrors.Wrapf(types.ErrAppInvalidServiceConfigs, "must have at least one service") } diff --git a/x/application/keeper/msg_server_stake_application_test.go b/x/application/keeper/msg_server_stake_application_test.go index 98d3069ea..ad8edffb6 100644 --- a/x/application/keeper/msg_server_stake_application_test.go +++ b/x/application/keeper/msg_server_stake_application_test.go @@ -41,12 +41,12 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { require.NoError(t, err) // Verify that the application exists - foundApp, isAppFound := k.GetApplication(ctx, addr) + appFound, isAppFound := k.GetApplication(ctx, addr) require.True(t, isAppFound) - require.Equal(t, addr, foundApp.Address) - require.Equal(t, int64(100), foundApp.Stake.Amount.Int64()) - require.Len(t, foundApp.ServiceConfigs, 1) - require.Equal(t, "svc1", foundApp.ServiceConfigs[0].ServiceId.Id) + require.Equal(t, addr, appFound.Address) + require.Equal(t, int64(100), appFound.Stake.Amount.Int64()) + require.Len(t, appFound.ServiceConfigs, 1) + require.Equal(t, "svc1", appFound.ServiceConfigs[0].ServiceId.Id) // Prepare an updated application with a higher stake and another service updateStakeMsg := &types.MsgStakeApplication{ @@ -65,12 +65,12 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { // Update the staked application _, err = srv.StakeApplication(wctx, updateStakeMsg) require.NoError(t, err) - foundApp, isAppFound = k.GetApplication(ctx, addr) + appFound, isAppFound = k.GetApplication(ctx, addr) require.True(t, isAppFound) - require.Equal(t, int64(200), foundApp.Stake.Amount.Int64()) - require.Len(t, foundApp.ServiceConfigs, 2) - require.Equal(t, "svc1", foundApp.ServiceConfigs[0].ServiceId.Id) - require.Equal(t, "svc2", foundApp.ServiceConfigs[1].ServiceId.Id) + require.Equal(t, int64(200), appFound.Stake.Amount.Int64()) + require.Len(t, appFound.ServiceConfigs, 2) + require.Equal(t, "svc1", appFound.ServiceConfigs[0].ServiceId.Id) + require.Equal(t, "svc2", appFound.ServiceConfigs[1].ServiceId.Id) } func TestMsgServer_StakeApplication_FailRestakingDueToInvalidServices(t *testing.T) { diff --git a/x/application/keeper/msg_server_unstake_application_test.go b/x/application/keeper/msg_server_unstake_application_test.go index 9f45647f0..370f29905 100644 --- a/x/application/keeper/msg_server_unstake_application_test.go +++ b/x/application/keeper/msg_server_unstake_application_test.go @@ -42,11 +42,11 @@ func TestMsgServer_UnstakeApplication_Success(t *testing.T) { require.NoError(t, err) // Verify that the application exists - foundApp, isAppFound := k.GetApplication(ctx, addr) + appFound, isAppFound := k.GetApplication(ctx, addr) require.True(t, isAppFound) - require.Equal(t, addr, foundApp.Address) - require.Equal(t, initialStake.Amount, foundApp.Stake.Amount) - require.Len(t, foundApp.ServiceConfigs, 1) + require.Equal(t, addr, appFound.Address) + require.Equal(t, initialStake.Amount, appFound.Stake.Amount) + require.Len(t, appFound.ServiceConfigs, 1) // Unstake the application unstakeMsg := &types.MsgUnstakeApplication{Address: addr} diff --git a/x/application/types/genesis.go b/x/application/types/genesis.go index 38d2b25dd..b9fa03931 100644 --- a/x/application/types/genesis.go +++ b/x/application/types/genesis.go @@ -34,10 +34,10 @@ func (gs GenesisState) Validate() error { applicationIndexMap[index] = struct{}{} } - // Check that the stake value for the apps is valid and that the delegatee pubkeys are valid + // Check that the stake value for the apps is valid and that the delegatee addresses are valid for _, app := range gs.ApplicationList { // TODO_TECHDEBT: Consider creating shared helpers across the board for stake validation, - // similar to how we have `AreValidAppServiceConfigs` below + // similar to how we have `ValidateAppServiceConfigs` below if app.Stake == nil { return sdkerrors.Wrapf(ErrAppInvalidStake, "nil stake amount for application") } @@ -63,8 +63,8 @@ func (gs GenesisState) Validate() error { } // Validate the application service configs - if reason, ok := servicehelpers.AreValidAppServiceConfigs(app.ServiceConfigs); !ok { - return sdkerrors.Wrapf(ErrAppInvalidStake, reason) + if err := servicehelpers.ValidateAppServiceConfigs(app.ServiceConfigs); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidServiceConfigs, err.Error()) } } diff --git a/x/application/types/message_stake_application.go b/x/application/types/message_stake_application.go index f813cb809..678c49cac 100644 --- a/x/application/types/message_stake_application.go +++ b/x/application/types/message_stake_application.go @@ -13,6 +13,7 @@ const TypeMsgStakeApplication = "stake_application" var _ sdk.Msg = (*MsgStakeApplication)(nil) +// TODO_TECHDEBT: See `NewMsgStakeSupplier` and follow the same pattern for the `Services` parameter func NewMsgStakeApplication( address string, stake types.Coin, @@ -63,6 +64,7 @@ func (msg *MsgStakeApplication) ValidateBasic() error { return sdkerrors.Wrapf(ErrAppInvalidAddress, "invalid application address %s; (%v)", msg.Address, err) } + // TODO_TECHDEBT: Centralize stake related verification and share across different parts of the source code // Validate the stake amount if msg.Stake == nil { return sdkerrors.Wrapf(ErrAppInvalidStake, "nil application stake; (%v)", err) @@ -82,8 +84,8 @@ func (msg *MsgStakeApplication) ValidateBasic() error { } // Validate the application service configs - if reason, ok := servicehelpers.AreValidAppServiceConfigs(msg.Services); !ok { - return sdkerrors.Wrapf(ErrAppInvalidServiceConfigs, reason) + if err := servicehelpers.ValidateAppServiceConfigs(msg.Services); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidServiceConfigs, err.Error()) } return nil diff --git a/x/application/types/message_stake_application_test.go b/x/application/types/message_stake_application_test.go index 6b5ceec7a..90ec6969c 100644 --- a/x/application/types/message_stake_application_test.go +++ b/x/application/types/message_stake_application_test.go @@ -16,7 +16,7 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg MsgStakeApplication err error }{ - // address tests + // address related tests { name: "invalid address - nil stake", msg: MsgStakeApplication{ @@ -92,6 +92,17 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { }, // service related tests + { + name: "valid service configs - multiple services", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + {ServiceId: &sharedtypes.ServiceId{Id: "svc2"}}, + }, + }, + }, { name: "invalid service configs - not present", msg: MsgStakeApplication{ @@ -146,17 +157,6 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { }, err: ErrAppInvalidServiceConfigs, }, - { - name: "valid service configs - multiple services", - msg: MsgStakeApplication{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, - Services: []*sharedtypes.ApplicationServiceConfig{ - {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, - {ServiceId: &sharedtypes.ServiceId{Id: "svc2"}}, - }, - }, - }, } for _, tt := range tests { diff --git a/x/shared/helpers/service.go b/x/shared/helpers/service.go index 1eb302958..3a4e56e5f 100644 --- a/x/shared/helpers/service.go +++ b/x/shared/helpers/service.go @@ -1,6 +1,9 @@ package helpers -import "regexp" +import ( + "net/url" + "regexp" +) const ( maxServiceIdLength = 8 // Limiting all serviceIds to 8 characters @@ -51,3 +54,23 @@ func IsValidServiceName(serviceName string) bool { // Use the regex to match against the input string return regexExprServiceName.MatchString(serviceName) } + +// IsValidEndpointUrl checks if the provided string is a valid URL. +func IsValidEndpointUrl(endpoint string) bool { + u, err := url.Parse(endpoint) + if err != nil { + return false + } + + // Check if scheme is http or https + if u.Scheme != "http" && u.Scheme != "https" { + return false + } + + // Ensure the URL has a host + if u.Host == "" { + return false + } + + return true +} diff --git a/x/shared/helpers/service_configs.go b/x/shared/helpers/service_configs.go index 7a7baaf33..9955a4d5f 100644 --- a/x/shared/helpers/service_configs.go +++ b/x/shared/helpers/service_configs.go @@ -6,28 +6,93 @@ import ( sharedtypes "pocket/x/shared/types" ) -// AreValidAppServiceConfigs returns an error if the provided service configs are invalid -// by wrapping the provided around with additional details -func AreValidAppServiceConfigs(services []*sharedtypes.ApplicationServiceConfig) (string, bool) { +// ValidateAppServiceConfigs returns an error if any of the application service configs are invalid +func ValidateAppServiceConfigs(services []*sharedtypes.ApplicationServiceConfig) error { if len(services) == 0 { - return fmt.Sprintf("no services configs provided for application: %v", services), false + return fmt.Errorf("no services configs provided for application: %v", services) } for _, serviceConfig := range services { if serviceConfig == nil { - return fmt.Sprintf("serviceConfig cannot be nil: %v", services), false + return fmt.Errorf("serviceConfig cannot be nil: %v", services) } if serviceConfig.ServiceId == nil { - return fmt.Sprintf("serviceId cannot be nil: %v", serviceConfig), false + return fmt.Errorf("serviceId cannot be nil: %v", serviceConfig) } if serviceConfig.ServiceId.Id == "" { - return fmt.Sprintf("serviceId.Id cannot be empty: %v", serviceConfig), false + return fmt.Errorf("serviceId.Id cannot be empty: %v", serviceConfig) } if !IsValidServiceId(serviceConfig.ServiceId.Id) { - return fmt.Sprintf("invalid serviceId.Id: %v", serviceConfig), false + return fmt.Errorf("invalid serviceId.Id: %v", serviceConfig) } if !IsValidServiceName(serviceConfig.ServiceId.Name) { - return fmt.Sprintf("invalid serviceId.Name: %v", serviceConfig), false + return fmt.Errorf("invalid serviceId.Name: %v", serviceConfig) } } - return "", true + return nil +} + +// ValidateSupplierServiceConfigs returns an error if any of the supplier service configs are invalid +func ValidateSupplierServiceConfigs(services []*sharedtypes.SupplierServiceConfig) error { + if len(services) == 0 { + return fmt.Errorf("no services provided for supplier: %v", services) + } + for _, serviceConfig := range services { + if serviceConfig == nil { + return fmt.Errorf("serviceConfig cannot be nil: %v", services) + } + + // Check the ServiceId + if serviceConfig.ServiceId == nil { + return fmt.Errorf("serviceId cannot be nil: %v", serviceConfig) + } + if serviceConfig.ServiceId.Id == "" { + return fmt.Errorf("serviceId.Id cannot be empty: %v", serviceConfig) + } + if !IsValidServiceId(serviceConfig.ServiceId.Id) { + return fmt.Errorf("invalid serviceId.Id: %v", serviceConfig) + } + if !IsValidServiceName(serviceConfig.ServiceId.Name) { + return fmt.Errorf("invalid serviceId.Name: %v", serviceConfig) + } + + // Check the Endpoints + if serviceConfig.Endpoints == nil { + return fmt.Errorf("endpoints cannot be nil: %v", serviceConfig) + } + if len(serviceConfig.Endpoints) == 0 { + return fmt.Errorf("endpoints must have at least one entry: %v", serviceConfig) + } + + // Check each endpoint + for _, endpoint := range serviceConfig.Endpoints { + if endpoint == nil { + return fmt.Errorf("endpoint cannot be nil: %v", serviceConfig) + } + + // Validate the URL + if endpoint.Url == "" { + return fmt.Errorf("endpoint.Url cannot be empty: %v", serviceConfig) + } + if !IsValidEndpointUrl(endpoint.Url) { + return fmt.Errorf("invalid endpoint.Url: %v", serviceConfig) + } + + // Validate the RPC type + if endpoint.RpcType == sharedtypes.RPCType_UNKNOWN_RPC { + return fmt.Errorf("endpoint.RpcType cannot be UNKNOWN_RPC: %v", serviceConfig) + } + if _, ok := sharedtypes.RPCType_name[int32(endpoint.RpcType)]; !ok { + return fmt.Errorf("endpoint.RpcType is not a valid RPCType: %v", serviceConfig) + } + + // TODO: Validate configs once they are being used + // if endpoint.Configs == nil { + // return fmt.Errorf("endpoint.Configs cannot be nil: %v", serviceConfig) + // } + // if len(endpoint.Configs) == 0 { + // return fmt.Errorf("endpoint.Configs must have at least one entry: %v", serviceConfig) + // } + } + } + return nil } diff --git a/x/shared/helpers/service_test.go b/x/shared/helpers/service_test.go index 1d344b095..335cbcd51 100644 --- a/x/shared/helpers/service_test.go +++ b/x/shared/helpers/service_test.go @@ -27,3 +27,56 @@ func TestIsValidServiceId(t *testing.T) { }) } } + +func TestIsValidEndpointUrl(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "valid http URL", + input: "http://example.com", + expected: true, + }, + { + name: "valid https URL", + input: "https://example.com/path?query=value#fragment", + expected: true, + }, + { + name: "valid localhost URL with scheme", + input: "https://localhost:8081", + expected: true, + }, + { + name: "valid loopback URL with scheme", + input: "http://127.0.0.1:8081", + expected: true, + }, + { + name: "invalid scheme", + input: "ftp://example.com", + expected: false, + }, + { + name: "missing scheme", + input: "example.com", + expected: false, + }, + { + name: "invalid URL", + input: "not-a-valid-url", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsValidEndpointUrl(tt.input) + if got != tt.expected { + t.Errorf("IsValidEndpointUrl(%q) = %v, want %v", tt.input, got, tt.expected) + } + }) + } +} diff --git a/x/supplier/client/cli/tx_stake_supplier.go b/x/supplier/client/cli/tx_stake_supplier.go index a372f9772..7ea78e077 100644 --- a/x/supplier/client/cli/tx_stake_supplier.go +++ b/x/supplier/client/cli/tx_stake_supplier.go @@ -1,7 +1,9 @@ package cli import ( + "fmt" "strconv" + "strings" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -9,6 +11,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/cobra" + sharedtypes "pocket/x/shared/types" "pocket/x/supplier/types" ) @@ -17,15 +20,23 @@ var _ = strconv.Itoa(0) func CmdStakeSupplier() *cobra.Command { // fromAddress & signature is retrieved via `flags.FlagFrom` in the `clientCtx` cmd := &cobra.Command{ - Use: "stake-supplier [amount]", + // TODO_HACK: For now we are only specifying the service IDs as a list of of strings separated by commas. + // This needs to be expand to specify the full SupplierServiceConfig. Furthermore, providing a flag to + // a file where SupplierServiceConfig specifying full service configurations in the CLI by providing a flag that accepts a JSON string + Use: "stake-supplier [amount] [svcId1;url1,svcId2;url2,...,svcIdN;urlN]", Short: "Stake a supplier", Long: `Stake an supplier with the provided parameters. This is a broadcast operation that will stake the tokens and associate them with the supplier specified by the 'from' address. +TODO_HACK: Until proper service configuration files are supported, suppliers must specify the services as a single string +of comma separated values of the form 'service;url' where 'service' is the service ID and 'url' is the service URL. +For example, an application that stakes for 'anvil' could be matched with a supplier staking for 'anvil;http://anvil:8547'. + Example: -$ pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, - Args: cobra.ExactArgs(1), +$ pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt anvil;http://anvil:8547 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { stakeString := args[0] + servicesArg := args[1] clientCtx, err := client.GetClientTxContext(cmd) if err != nil { @@ -37,9 +48,15 @@ $ pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt --keyring- return err } + services, err := hackStringToServices(servicesArg) + if err != nil { + return err + } + msg := types.NewMsgStakeSupplier( clientCtx.GetFromAddress().String(), stake, + services, ) if err := msg.ValidateBasic(); err != nil { @@ -54,3 +71,31 @@ $ pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt --keyring- return cmd } + +// TODO_BLOCKER, TODO_HACK: The supplier stake command should take an argument +// or flag that points to a file containing all the services configurations & specifications. +// As a quick workaround, we just need the service & url to get things working for now. +func hackStringToServices(servicesArg string) ([]*sharedtypes.SupplierServiceConfig, error) { + supplierServiceConfig := make([]*sharedtypes.SupplierServiceConfig, 0) + serviceStrings := strings.Split(servicesArg, ",") + for _, serviceString := range serviceStrings { + serviceParts := strings.Split(serviceString, ";") + if len(serviceParts) != 2 { + return nil, fmt.Errorf("invalid service string: %s. Expected it to be of the form 'service;url'", serviceString) + } + service := &sharedtypes.SupplierServiceConfig{ + ServiceId: &sharedtypes.ServiceId{ + Id: serviceParts[0], + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: serviceParts[1], + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + } + supplierServiceConfig = append(supplierServiceConfig, service) + } + return supplierServiceConfig, nil +} diff --git a/x/supplier/client/cli/tx_stake_supplier_test.go b/x/supplier/client/cli/tx_stake_supplier_test.go index 3997a8a5a..f6de5641e 100644 --- a/x/supplier/client/cli/tx_stake_supplier_test.go +++ b/x/supplier/client/cli/tx_stake_supplier_test.go @@ -39,51 +39,134 @@ func TestCLI_StakeSupplier(t *testing.T) { } tests := []struct { - desc string - address string - stakeString string - err *sdkerrors.Error + desc string + address string + stakeString string + servicesString string + err *sdkerrors.Error }{ + // Happy Paths { - desc: "stake supplier: valid", - address: supplierAccount.Address.String(), - stakeString: "1000upokt", + desc: "stake supplier: valid", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "svc1;http://pokt.network:8081", }, + + // Error Paths - Address Related { desc: "stake supplier: missing address", // address: "explicitly missing", - stakeString: "1000upokt", - err: types.ErrSupplierInvalidAddress, + stakeString: "1000upokt", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidAddress, }, { - desc: "stake supplier: invalid address", - address: "invalid", - stakeString: "1000upokt", - err: types.ErrSupplierInvalidAddress, + desc: "stake supplier: invalid address", + address: "invalid", + stakeString: "1000upokt", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidAddress, }, + + // Error Paths - Stake Related { desc: "stake supplier: missing stake", address: supplierAccount.Address.String(), // stakeString: "explicitly missing", - err: types.ErrSupplierInvalidStake, + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidStake, }, { - desc: "stake supplier: invalid stake denom", - address: supplierAccount.Address.String(), - stakeString: "1000invalid", - err: types.ErrSupplierInvalidStake, + desc: "stake supplier: invalid stake denom", + address: supplierAccount.Address.String(), + stakeString: "1000invalid", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidStake, }, { - desc: "stake supplier: invalid stake amount (zero)", - address: supplierAccount.Address.String(), - stakeString: "0upokt", - err: types.ErrSupplierInvalidStake, + desc: "stake supplier: invalid stake amount (zero)", + address: supplierAccount.Address.String(), + stakeString: "0upokt", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidStake, }, { - desc: "stake supplier: invalid stake amount (negative)", + desc: "stake supplier: invalid stake amount (negative)", + address: supplierAccount.Address.String(), + stakeString: "-1000upokt", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidStake, + }, + + // Happy Paths - Service Related + { + desc: "services_test: valid multiple services", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "svc1;http://pokt.network:8081,svc2;http://pokt.network:8082", + }, + { + desc: "services_test: valid localhost", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "scv1;http://127.0.0.1:8082", + }, + { + desc: "services_test: valid loopback", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "scv1;http://localhost:8082", + }, + { + desc: "services_test: valid without a pork", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "scv1;http://pokt.network", + }, + + // Error Paths - Service Related + { + desc: "services_test: invalid services (missing argument)", address: supplierAccount.Address.String(), - stakeString: "-1000upokt", - err: types.ErrSupplierInvalidStake, + stakeString: "1000upokt", + // servicesString: "explicitly omitted", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: invalid services (empty string)", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: invalid because contains a space", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "scv1 http://127.0.0.1:8082", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: invalid URL", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "svc1;bad_url", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: missing URLs", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "svc1,svc2;", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: missing service IDs", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "localhost:8081,;localhost:8082", + err: types.ErrSupplierInvalidServiceConfig, }, } @@ -99,6 +182,7 @@ func TestCLI_StakeSupplier(t *testing.T) { // Prepare the arguments for the CLI command args := []string{ tt.stakeString, + tt.servicesString, fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.address), } args = append(args, commonArgs...) diff --git a/x/supplier/genesis_test.go b/x/supplier/genesis_test.go index a0d3ccfcb..9bae65111 100644 --- a/x/supplier/genesis_test.go +++ b/x/supplier/genesis_test.go @@ -14,6 +14,7 @@ import ( "pocket/x/supplier/types" ) +// Please see `x/supplier/types/genesis_test.go` for extensive tests related to the validity of the genesis state. func TestGenesis(t *testing.T) { genesisState := types.GenesisState{ Params: types.DefaultParams(), @@ -21,10 +22,38 @@ func TestGenesis(t *testing.T) { { Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, }, { Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId2", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8082", + RpcType: sharedtypes.RPCType_GRPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, }, }, // this line is used by starport scaffolding # genesis/test/state diff --git a/x/supplier/keeper/msg_server_stake_supplier.go b/x/supplier/keeper/msg_server_stake_supplier.go index 3359b7d77..978bd71b9 100644 --- a/x/supplier/keeper/msg_server_stake_supplier.go +++ b/x/supplier/keeper/msg_server_stake_supplier.go @@ -20,6 +20,7 @@ func (k msgServer) StakeSupplier( logger.Info("About to stake supplier with msg: %v", msg) if err := msg.ValidateBasic(); err != nil { + logger.Error("invalid MsgStakeSupplier: %v", msg) return nil, err } @@ -47,6 +48,7 @@ func (k msgServer) StakeSupplier( return nil, err } + // TODO_IMPROVE: Should we avoid making this call if `coinsToDelegate` = 0? // Send the coins from the supplier to the staked supplier pool err = k.bankKeeper.DelegateCoinsFromAccountToModule(ctx, supplierAddress, types.ModuleName, []sdk.Coin{coinsToDelegate}) if err != nil { @@ -66,8 +68,9 @@ func (k msgServer) createSupplier( msg *types.MsgStakeSupplier, ) sharedtypes.Supplier { return sharedtypes.Supplier{ - Address: msg.Address, - Stake: msg.Stake, + Address: msg.Address, + Stake: msg.Stake, + Services: msg.Services, } } @@ -81,16 +84,22 @@ func (k msgServer) updateSupplier( return sdkerrors.Wrapf(types.ErrSupplierUnauthorized, "msg Address (%s) != supplier address (%s)", msg.Address, supplier.Address) } + // Validate that the stake is not being lowered if msg.Stake == nil { return sdkerrors.Wrapf(types.ErrSupplierInvalidStake, "stake amount cannot be nil") } - if msg.Stake.IsLTE(*supplier.Stake) { return sdkerrors.Wrapf(types.ErrSupplierInvalidStake, "stake amount %v must be higher than previous stake amount %v", msg.Stake, supplier.Stake) } - supplier.Stake = msg.Stake + // Validate that the service configs maintain at least one service. + // Additional validation is done in `msg.ValidateBasic` above. + if len(msg.Services) == 0 { + return sdkerrors.Wrapf(types.ErrSupplierInvalidServiceConfig, "must have at least one service") + } + supplier.Services = msg.Services + return nil } diff --git a/x/supplier/keeper/msg_server_stake_supplier_test.go b/x/supplier/keeper/msg_server_stake_supplier_test.go index dfc6b3880..5b5949fe5 100644 --- a/x/supplier/keeper/msg_server_stake_supplier_test.go +++ b/x/supplier/keeper/msg_server_stake_supplier_test.go @@ -8,6 +8,7 @@ import ( keepertest "pocket/testutil/keeper" "pocket/testutil/sample" + sharedtypes "pocket/x/shared/types" "pocket/x/supplier/keeper" "pocket/x/supplier/types" ) @@ -28,6 +29,20 @@ func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) { stakeMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Stake the supplier @@ -35,23 +50,126 @@ func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) { require.NoError(t, err) // Verify that the supplier exists - foundSupplier, isSupplierFound := k.GetSupplier(ctx, addr) + supplierFound, isSupplierFound := k.GetSupplier(ctx, addr) require.True(t, isSupplierFound) - require.Equal(t, addr, foundSupplier.Address) - require.Equal(t, int64(100), foundSupplier.Stake.Amount.Int64()) + require.Equal(t, addr, supplierFound.Address) + require.Equal(t, int64(100), supplierFound.Stake.Amount.Int64()) + require.Len(t, supplierFound.Services, 1) + require.Equal(t, "svcId", supplierFound.Services[0].ServiceId.Id) + require.Len(t, supplierFound.Services[0].Endpoints, 1) + require.Equal(t, "http://localhost:8080", supplierFound.Services[0].Endpoints[0].Url) - // Prepare an updated supplier with a higher stake + // Prepare an updated supplier with a higher stake and a different URL for the service updateMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(200)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId2", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8082", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Update the staked supplier _, err = srv.StakeSupplier(wctx, updateMsg) require.NoError(t, err) - foundSupplier, isSupplierFound = k.GetSupplier(ctx, addr) + supplierFound, isSupplierFound = k.GetSupplier(ctx, addr) + require.True(t, isSupplierFound) + require.Equal(t, int64(200), supplierFound.Stake.Amount.Int64()) + require.Len(t, supplierFound.Services, 1) + require.Equal(t, "svcId2", supplierFound.Services[0].ServiceId.Id) + require.Len(t, supplierFound.Services[0].Endpoints, 1) + require.Equal(t, "http://localhost:8082", supplierFound.Services[0].Endpoints[0].Url) +} + +func TestMsgServer_StakeSupplier_FailRestakingDueToInvalidServices(t *testing.T) { + k, ctx := keepertest.SupplierKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + supplierAddr := sample.AccAddress() + + // Prepare the supplier stake message + stakeMsg := &types.MsgStakeSupplier{ + Address: supplierAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + } + + // Stake the supplier + _, err := srv.StakeSupplier(wctx, stakeMsg) + require.NoError(t, err) + + // Prepare the supplier stake message without any service endpoints + updateStakeMsg := &types.MsgStakeSupplier{ + Address: supplierAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svcId"}, + Endpoints: []*sharedtypes.SupplierEndpoint{}, + }, + }, + } + + // Fail updating the supplier when the list of service endpoints is empty + _, err = srv.StakeSupplier(wctx, updateStakeMsg) + require.Error(t, err) + + // Verify the supplierFound still exists and is staked for svc1 + supplierFound, isSupplierFound := k.GetSupplier(ctx, supplierAddr) + require.True(t, isSupplierFound) + require.Equal(t, supplierAddr, supplierFound.Address) + require.Len(t, supplierFound.Services, 1) + require.Equal(t, "svcId", supplierFound.Services[0].ServiceId.Id) + require.Len(t, supplierFound.Services[0].Endpoints, 1) + require.Equal(t, "http://localhost:8080", supplierFound.Services[0].Endpoints[0].Url) + + // Prepare the supplier stake message with an invalid service ID + updateStakeMsg = &types.MsgStakeSupplier{ + Address: supplierAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1 INVALID ! & *"}, + }, + }, + } + + // Fail updating the supplier when the list of services is empty + _, err = srv.StakeSupplier(wctx, updateStakeMsg) + require.Error(t, err) + + // Verify the supplier still exists and is staked for svc1 + supplierFound, isSupplierFound = k.GetSupplier(ctx, supplierAddr) require.True(t, isSupplierFound) - require.Equal(t, int64(200), foundSupplier.Stake.Amount.Int64()) + require.Equal(t, supplierAddr, supplierFound.Address) + require.Len(t, supplierFound.Services, 1) + require.Equal(t, "svcId", supplierFound.Services[0].ServiceId.Id) + require.Len(t, supplierFound.Services[0].Endpoints, 1) + require.Equal(t, "http://localhost:8080", supplierFound.Services[0].Endpoints[0].Url) } func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) { @@ -64,6 +182,20 @@ func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) { stakeMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Stake the supplier & verify that the supplier exists @@ -76,6 +208,20 @@ func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) { updateMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(50)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Verify that it fails @@ -86,4 +232,5 @@ func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) { supplierFound, isSupplierFound := k.GetSupplier(ctx, addr) require.True(t, isSupplierFound) require.Equal(t, int64(100), supplierFound.Stake.Amount.Int64()) + require.Len(t, supplierFound.Services, 1) } diff --git a/x/supplier/keeper/msg_server_unstake_supplier_test.go b/x/supplier/keeper/msg_server_unstake_supplier_test.go index b083aac2b..57e4dcdc0 100644 --- a/x/supplier/keeper/msg_server_unstake_supplier_test.go +++ b/x/supplier/keeper/msg_server_unstake_supplier_test.go @@ -8,6 +8,7 @@ import ( keepertest "pocket/testutil/keeper" "pocket/testutil/sample" + sharedtypes "pocket/x/shared/types" "pocket/x/supplier/keeper" "pocket/x/supplier/types" ) @@ -29,6 +30,20 @@ func TestMsgServer_UnstakeSupplier_Success(t *testing.T) { stakeMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &initialStake, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Stake the supplier @@ -40,6 +55,7 @@ func TestMsgServer_UnstakeSupplier_Success(t *testing.T) { require.True(t, isSupplierFound) require.Equal(t, addr, foundSupplier.Address) require.Equal(t, initialStake.Amount, foundSupplier.Stake.Amount) + require.Len(t, foundSupplier.Services, 1) // Unstake the supplier unstakeMsg := &types.MsgUnstakeSupplier{Address: addr} diff --git a/x/supplier/keeper/supplier.go b/x/supplier/keeper/supplier.go index f3ae6a310..68c800c24 100644 --- a/x/supplier/keeper/supplier.go +++ b/x/supplier/keeper/supplier.go @@ -20,49 +20,49 @@ func (k Keeper) SetSupplier(ctx sdk.Context, supplier sharedtypes.Supplier) { // GetSupplier returns a supplier from its index func (k Keeper) GetSupplier( ctx sdk.Context, - address string, + supplierAddr string, -) (val sharedtypes.Supplier, found bool) { +) (supplier sharedtypes.Supplier, found bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.SupplierKeyPrefix)) b := store.Get(types.SupplierKey( - address, + supplierAddr, )) if b == nil { - return val, false + return supplier, false } - k.cdc.MustUnmarshal(b, &val) - return val, true + k.cdc.MustUnmarshal(b, &supplier) + return supplier, true } // RemoveSupplier removes a supplier from the store func (k Keeper) RemoveSupplier( ctx sdk.Context, - address string, + supplierAddr string, ) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.SupplierKeyPrefix)) store.Delete(types.SupplierKey( - address, + supplierAddr, )) } // GetAllSupplier returns all supplier -func (k Keeper) GetAllSupplier(ctx sdk.Context) (list []sharedtypes.Supplier) { +func (k Keeper) GetAllSupplier(ctx sdk.Context) (suppliers []sharedtypes.Supplier) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.SupplierKeyPrefix)) iterator := sdk.KVStorePrefixIterator(store, []byte{}) defer iterator.Close() for ; iterator.Valid(); iterator.Next() { - var val sharedtypes.Supplier - k.cdc.MustUnmarshal(iterator.Value(), &val) - list = append(list, val) + var supplier sharedtypes.Supplier + k.cdc.MustUnmarshal(iterator.Value(), &supplier) + suppliers = append(suppliers, supplier) } return } // TODO_OPTIMIZE: Index suppliers by serviceId so we can easily query `k.GetAllSupplier(ctx, ServiceId)` -// func (k Keeper) GetAllSupplier(ctx, sdkContext, serviceId string) (list []sharedtypes.Supplier) {} +// func (k Keeper) GetAllSupplier(ctx, sdkContext, serviceId string) (suppliers []sharedtypes.Supplier) {} diff --git a/x/supplier/keeper/supplier_test.go b/x/supplier/keeper/supplier_test.go index a2d072f79..fef1f8645 100644 --- a/x/supplier/keeper/supplier_test.go +++ b/x/supplier/keeper/supplier_test.go @@ -1,64 +1,94 @@ package keeper_test import ( + "fmt" "strconv" "testing" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/require" + "pocket/cmd/pocketd/cmd" keepertest "pocket/testutil/keeper" "pocket/testutil/nullify" + "pocket/testutil/sample" sharedtypes "pocket/x/shared/types" "pocket/x/supplier/keeper" + "pocket/x/supplier/types" ) // Prevent strconv unused error var _ = strconv.IntSize -func createNSupplier(keeper *keeper.Keeper, ctx sdk.Context, n int) []sharedtypes.Supplier { - items := make([]sharedtypes.Supplier, n) - for i := range items { - items[i].Address = strconv.Itoa(i) +func init() { + cmd.InitSDKConfig() +} - keeper.SetSupplier(ctx, items[i]) +func createNSupplier(keeper *keeper.Keeper, ctx sdk.Context, n int) []sharedtypes.Supplier { + suppliers := make([]sharedtypes.Supplier, n) + for i := range suppliers { + supplier := &suppliers[i] + supplier.Address = sample.AccAddress() + supplier.Stake = &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(int64(i))} + supplier.Services = []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: fmt.Sprintf("svc%d", i)}, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: fmt.Sprintf("http://localhost:%d", i), + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + } + keeper.SetSupplier(ctx, *supplier) } - return items + + return suppliers } func TestSupplierGet(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) - items := createNSupplier(keeper, ctx, 10) - for _, item := range items { - rst, found := keeper.GetSupplier(ctx, - item.Address, + suppliers := createNSupplier(keeper, ctx, 10) + for _, supplier := range suppliers { + supplierFound, isSupplierFound := keeper.GetSupplier(ctx, + supplier.Address, ) - require.True(t, found) + require.True(t, isSupplierFound) require.Equal(t, - nullify.Fill(&item), - nullify.Fill(&rst), + nullify.Fill(&supplier), + nullify.Fill(&supplierFound), ) } } func TestSupplierRemove(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) - items := createNSupplier(keeper, ctx, 10) - for _, item := range items { + suppliers := createNSupplier(keeper, ctx, 10) + for _, supplier := range suppliers { keeper.RemoveSupplier(ctx, - item.Address, + supplier.Address, ) - _, found := keeper.GetSupplier(ctx, - item.Address, + _, isSupplierFound := keeper.GetSupplier(ctx, + supplier.Address, ) - require.False(t, found) + require.False(t, isSupplierFound) } } func TestSupplierGetAll(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) - items := createNSupplier(keeper, ctx, 10) + suppliers := createNSupplier(keeper, ctx, 10) require.ElementsMatch(t, - nullify.Fill(items), + nullify.Fill(suppliers), nullify.Fill(keeper.GetAllSupplier(ctx)), ) } + +// The application module address is derived off of its semantic name. +// This test is a helper for us to easily identify the underlying address. +func TestApplicationModuleAddress(t *testing.T) { + moduleAddress := authtypes.NewModuleAddress(types.ModuleName) + require.Equal(t, "pokt1j40dzzmn6cn9kxku7a5tjnud6hv37vesr5ccaa", moduleAddress.String()) +} diff --git a/x/supplier/simulation/stake_supplier.go b/x/supplier/simulation/stake_supplier.go index 159035ee3..103e321bd 100644 --- a/x/supplier/simulation/stake_supplier.go +++ b/x/supplier/simulation/stake_supplier.go @@ -21,6 +21,7 @@ func SimulateMsgStakeSupplier( simAccount, _ := simtypes.RandomAcc(r, accs) stakeMsg := &types.MsgStakeSupplier{ Address: simAccount.Address.String(), + // TODO: Update all stake message fields } // TODO: Handling the StakeSupplier simulation diff --git a/x/supplier/types/errors.go b/x/supplier/types/errors.go index a345e67cc..02a76a573 100644 --- a/x/supplier/types/errors.go +++ b/x/supplier/types/errors.go @@ -8,8 +8,9 @@ import ( // x/supplier module sentinel errors var ( - ErrSupplierInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid supplier stake") - ErrSupplierInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid supplier address") - ErrSupplierUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized supplier signer") - ErrSupplierNotFound = sdkerrors.Register(ModuleName, 4, "supplier not found") + ErrSupplierInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid supplier stake") + ErrSupplierInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid supplier address") + ErrSupplierUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized supplier signer") + ErrSupplierNotFound = sdkerrors.Register(ModuleName, 4, "supplier not found") + ErrSupplierInvalidServiceConfig = sdkerrors.Register(ModuleName, 5, "invalid service config") ) diff --git a/x/supplier/types/genesis.go b/x/supplier/types/genesis.go index 1573a1269..d4ae794c6 100644 --- a/x/supplier/types/genesis.go +++ b/x/supplier/types/genesis.go @@ -6,6 +6,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + servicehelpers "pocket/x/shared/helpers" sharedtypes "pocket/x/shared/types" ) @@ -36,6 +37,8 @@ func (gs GenesisState) Validate() error { // Check that the stake value for the suppliers is valid for _, supplier := range gs.SupplierList { + // TODO_TECHDEBT: Consider creating shared helpers across the board for stake validation, + // similar to how we have `ValidateAppServiceConfigs` below if supplier.Stake == nil { return sdkerrors.Wrapf(ErrSupplierInvalidStake, "nil stake amount for supplier") } @@ -52,6 +55,12 @@ func (gs GenesisState) Validate() error { if stake.Denom != "upokt" { return sdkerrors.Wrapf(ErrSupplierInvalidStake, "invalid stake amount denom for supplier %v", supplier.Stake) } + + // Valid the application service configs + // Validate the application service configs + if err := servicehelpers.ValidateSupplierServiceConfigs(supplier.Services); err != nil { + return sdkerrors.Wrapf(ErrSupplierInvalidServiceConfig, err.Error()) + } } // this line is used by starport scaffolding # genesis/types/validate diff --git a/x/supplier/types/genesis_test.go b/x/supplier/types/genesis_test.go index 2f33966e5..be0988fa6 100644 --- a/x/supplier/types/genesis_test.go +++ b/x/supplier/types/genesis_test.go @@ -14,9 +14,35 @@ import ( func TestGenesisState_Validate(t *testing.T) { addr1 := sample.AccAddress() stake1 := sdk.NewCoin("upokt", sdk.NewInt(100)) + serviceConfig1 := &sharedtypes.SupplierServiceConfig{ + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + } + serviceList1 := []*sharedtypes.SupplierServiceConfig{serviceConfig1} addr2 := sample.AccAddress() stake2 := sdk.NewCoin("upokt", sdk.NewInt(100)) + serviceConfig2 := &sharedtypes.SupplierServiceConfig{ + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId2", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8082", + RpcType: sharedtypes.RPCType_GRPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + } + serviceList2 := []*sharedtypes.SupplierServiceConfig{serviceConfig2} tests := []struct { desc string @@ -34,12 +60,14 @@ func TestGenesisState_Validate(t *testing.T) { SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &stake2, + Address: addr2, + Stake: &stake2, + Services: serviceList2, }, }, // this line is used by starport scaffolding # types/genesis/validField @@ -51,12 +79,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Services: serviceList2, }, }, }, @@ -67,12 +97,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Services: serviceList2, }, }, }, @@ -83,12 +115,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Services: serviceList2, }, }, }, @@ -99,12 +133,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Services: serviceList2, }, }, }, @@ -115,12 +151,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr1, - Stake: &stake2, + Address: addr1, + Stake: &stake2, + Services: serviceList2, }, }, }, @@ -131,12 +169,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: nil, + Address: addr2, + Stake: nil, + Services: serviceList2, }, }, }, @@ -147,12 +187,112 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { Address: addr2, // Explicitly missing stake + Services: serviceList2, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - missing services list", + genState: &types.GenesisState{ + SupplierList: []sharedtypes.Supplier{ + { + Address: addr1, + Stake: &stake1, + Services: serviceList1, + }, + { + Address: addr2, + Stake: &stake2, + // Services: intentionally omitted + }, + }, + }, + valid: false, + }, + { + desc: "invalid - empty services list", + genState: &types.GenesisState{ + SupplierList: []sharedtypes.Supplier{ + { + Address: addr1, + Stake: &stake1, + Services: serviceList1, + }, + { + Address: addr2, + Stake: &stake2, + Services: []*sharedtypes.SupplierServiceConfig{}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - invalid URL", + genState: &types.GenesisState{ + SupplierList: []sharedtypes.Supplier{ + { + Address: addr1, + Stake: &stake1, + Services: serviceList1, + }, + { + Address: addr2, + Stake: &stake2, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "invalid URL", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - invalid RPC Type", + genState: &types.GenesisState{ + SupplierList: []sharedtypes.Supplier{ + { + Address: addr1, + Stake: &stake1, + Services: serviceList1, + }, + { + Address: addr2, + Stake: &stake2, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_UNKNOWN_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, }, }, }, diff --git a/x/supplier/types/message_stake_supplier.go b/x/supplier/types/message_stake_supplier.go index 9d4ce5cdc..02c6dd164 100644 --- a/x/supplier/types/message_stake_supplier.go +++ b/x/supplier/types/message_stake_supplier.go @@ -4,6 +4,9 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/types" + + servicehelpers "pocket/x/shared/helpers" + sharedtypes "pocket/x/shared/types" ) const TypeMsgStakeSupplier = "stake_supplier" @@ -13,11 +16,12 @@ var _ sdk.Msg = (*MsgStakeSupplier)(nil) func NewMsgStakeSupplier( address string, stake types.Coin, - + services []*sharedtypes.SupplierServiceConfig, ) *MsgStakeSupplier { return &MsgStakeSupplier{ - Address: address, - Stake: &stake, + Address: address, + Stake: &stake, + Services: services, } } @@ -49,6 +53,7 @@ func (msg *MsgStakeSupplier) ValidateBasic() error { return sdkerrors.Wrapf(ErrSupplierInvalidAddress, "invalid supplier address %s; (%v)", msg.Address, err) } + // TODO_TECHDEBT: Centralize stake related verification and share across different parts of the source code // Validate the stake amount if msg.Stake == nil { return sdkerrors.Wrapf(ErrSupplierInvalidStake, "nil supplier stake; (%v)", err) @@ -67,5 +72,10 @@ func (msg *MsgStakeSupplier) ValidateBasic() error { return sdkerrors.Wrapf(ErrSupplierInvalidStake, "invalid stake amount denom for supplier %v", msg.Stake) } + // Validate the supplier service configs + if err := servicehelpers.ValidateSupplierServiceConfigs(msg.Services); err != nil { + return sdkerrors.Wrapf(ErrSupplierInvalidServiceConfig, err.Error()) + } + return nil } diff --git a/x/supplier/types/message_stake_supplier_test.go b/x/supplier/types/message_stake_supplier_test.go index 3aa725c0b..76d8ba6d4 100644 --- a/x/supplier/types/message_stake_supplier_test.go +++ b/x/supplier/types/message_stake_supplier_test.go @@ -7,63 +7,283 @@ import ( "github.com/stretchr/testify/require" "pocket/testutil/sample" + sharedtypes "pocket/x/shared/types" ) +// TODO_CLEANUP: This test has a lot of copy-pasted code from test to test. +// It can be simplified by splitting it into smaller tests where the common +// fields don't need to be explicitly specified from test to test. func TestMsgStakeSupplier_ValidateBasic(t *testing.T) { + + defaultServicesList := []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }} + tests := []struct { name string msg MsgStakeSupplier err error }{ + // address related tests { name: "invalid address - nil stake", msg: MsgStakeSupplier{ Address: "invalid_address", // Stake explicitly nil + Services: defaultServicesList, }, err: ErrSupplierInvalidAddress, - }, { + }, + + // stake related tests + { name: "valid address - nil stake", msg: MsgStakeSupplier{ Address: sample.AccAddress(), // Stake explicitly nil + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, { name: "valid address - valid stake", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: defaultServicesList, }, }, { name: "valid address - zero stake", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, { name: "valid address - negative stake", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, { name: "valid address - invalid stake denom", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, { name: "valid address - invalid stake missing denom", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, + + // service related tests + { + name: "valid service configs - multiple services", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId2", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8082", + RpcType: sharedtypes.RPCType_GRPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + }, + { + name: "invalid service configs - omitted", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + // Services: intentionally omitted + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - empty", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{}, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - invalid service ID that's too long", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "123456790", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - invalid service Name that's too long", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "123", + Name: "abcdefghijklmnopqrstuvwxyzab-abcdefghijklmnopqrstuvwxyzab", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - invalid service ID that contains invalid characters", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "12 45 !", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - missing url", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + Name: "name", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + // Url: intentionally omitted + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - invalid url", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + Name: "name", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "I am not a valid URL", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - missing rpc type", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + Name: "name", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + // RpcType: intentionally omitted, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + // TODO_TEST: Need to add more tests around config types } for _, tt := range tests { From e85cc8a28a6aae25b20aac2b55120a040588c815 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Wed, 1 Nov 2023 08:06:00 +0100 Subject: [PATCH 16/28] [Miner] feat: add block client (#65) * feat: add the map channel observable operator (cherry picked from commit 22371aa550eb0060b528f4573ba6908bbdfa0c1c) * feat: add replay observable (cherry picked from commit ab21790164ab544ae5f1508d3237a3faab33e71e) * chore: add query client interface * chore: add query client errors * test: fix false positive, prevent regression, & add comments * chore: add godoc comment * feat: add query client implementation * chore: add connection & dialer wrapper implementations * test: query client & add testquery helper pkg * chore: add go_test_integration make target * chore: add internal mocks pkg * test: query client integration test * docs: add event query client docs * chore: update go.mod * chore: re-order `eventsQueryClient` methods to improve readability * chore: add godoc comments to testclient helpers * fix: comment formatting * chore: improve comment & naming in evt query client test * test: tune events query client parameters * chore: improve godoc comments * chore: review improvements * refactor: `replayObservable` as its own interface type * refactor: `replayObservable#Next() V` to `ReplayObservable#Last(ctx, n) []V` * chore: add constructor func for `ReplayObservable` * test: reorder to improve readibility * refactor: rename and add godoc comments * chore: improve naming & comments * chore: add warning log and improve comments * test: improve and add tests * fix: interface assertion * fix: comment typo * chore: review improvements * fix: race * chore: add block client interface * chore: add `MapReplay` operator * feat: add block client * test: block client integration * test: block client * docs: fix install instructions * fix: race on eventsBytesAndConns map * fix: interface assertions Co-authored-by: Redouane Lakrache * fix: race * Small updates to the README * refactor: add observableInternals interface (cherry picked from commit 5d149e5297ce7d11dad77983f53be53efd8dae15) * chore: update last; only block for 1 value min (cherry picked from commit b24a5e586e9c776a962008043d065a2294fd921c) * chore: review improvements * refactor: move add `channelObservableInternals` & migrate its relevant methods & state from channelObservable * refactor: simplify, cleanup, & improve comments * chore: review improvements (cherry picked from commit 31555cdc68211964358c43842e0581f565d1afff) * refactor: eliminate `EventsQueryClient#requestId` field (cherry picked from commit ccb1d6981f67ab860cb65dde4da15d89bcf57875) * refactor: review improvements * refactor: eliminate `EventsQueryClient#requestId` field * refactor: move websocket dialer and connection to own pkg * chore: add comment * fix: notify `retryOnError()` of async error propagating through `#EventsBytes()` observable * chore: review improvements * chore: move `EventsBytesObservable type above interfaces * chore: review improvements * fix: bug & improve naming & comments * chore: review improvements * fix: bug in `accumulateReplayValues()` * refactor: promote `retryOnError` to its own pkg: `retry.OnError` * chore: improve comments * test: inline wip test helpers * test: skip retry.OnError tests & comment * chore: review feedback improvements Co-authored-by: Daniel Olshansky * chore: review feedback improvements Co-authored-by: Daniel Olshansky * fix: format placeholder error --------- Co-authored-by: Redouane Lakrache Co-authored-by: Daniel Olshansky --- go.mod | 2 +- internal/testclient/testblock/client.go | 27 ++ pkg/client/block/block.go | 44 +++ pkg/client/block/client.go | 209 ++++++++++++ pkg/client/block/client_integration_test.go | 77 +++++ pkg/client/block/client_test.go | 138 ++++++++ pkg/client/block/errors.go | 8 + pkg/client/interface.go | 24 +- pkg/observable/channel/map.go | 34 +- pkg/retry/retry.go | 72 +++++ pkg/retry/retry_test.go | 337 ++++++++++++++++++++ 11 files changed, 969 insertions(+), 3 deletions(-) create mode 100644 internal/testclient/testblock/client.go create mode 100644 pkg/client/block/block.go create mode 100644 pkg/client/block/client.go create mode 100644 pkg/client/block/client_integration_test.go create mode 100644 pkg/client/block/client_test.go create mode 100644 pkg/client/block/errors.go create mode 100644 pkg/retry/retry.go create mode 100644 pkg/retry/retry_test.go diff --git a/go.mod b/go.mod index cb8df3174..7e42ae4a4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( cosmossdk.io/api v0.3.1 + cosmossdk.io/depinject v1.0.0-alpha.3 cosmossdk.io/errors v1.0.0-beta.7 cosmossdk.io/math v1.0.1 github.com/cometbft/cometbft v0.37.2 @@ -36,7 +37,6 @@ require ( cloud.google.com/go/iam v0.13.0 // indirect cloud.google.com/go/storage v1.29.0 // indirect cosmossdk.io/core v0.5.1 // indirect - cosmossdk.io/depinject v1.0.0-alpha.3 // indirect cosmossdk.io/log v1.1.1-0.20230704160919-88f2c830b0ca // indirect cosmossdk.io/tools/rosetta v0.2.1 // indirect filippo.io/edwards25519 v1.0.0 // indirect diff --git a/internal/testclient/testblock/client.go b/internal/testclient/testblock/client.go new file mode 100644 index 000000000..0d0f1b78b --- /dev/null +++ b/internal/testclient/testblock/client.go @@ -0,0 +1,27 @@ +package testblock + +import ( + "context" + "testing" + + "cosmossdk.io/depinject" + "github.com/stretchr/testify/require" + + "pocket/internal/testclient" + "pocket/internal/testclient/testeventsquery" + "pocket/pkg/client" + "pocket/pkg/client/block" +) + +func NewLocalnetClient(ctx context.Context, t *testing.T) client.BlockClient { + t.Helper() + + queryClient := testeventsquery.NewLocalnetClient(t) + require.NotNil(t, queryClient) + + deps := depinject.Supply(queryClient) + bClient, err := block.NewBlockClient(ctx, deps, testclient.CometLocalWebsocketURL) + require.NoError(t, err) + + return bClient +} diff --git a/pkg/client/block/block.go b/pkg/client/block/block.go new file mode 100644 index 000000000..5fe9a2e1e --- /dev/null +++ b/pkg/client/block/block.go @@ -0,0 +1,44 @@ +package block + +import ( + "encoding/json" + + "github.com/cometbft/cometbft/types" + + "pocket/pkg/client" +) + +// cometBlockEvent is used to deserialize incoming committed block event messages +// from the respective events query subscription. It implements the client.Block +// interface by loosely wrapping cometbft's block type, into which messages are +// deserialized. +type cometBlockEvent struct { + Block types.Block `json:"block"` +} + +// Height returns the block's height. +func (blockEvent *cometBlockEvent) Height() int64 { + return blockEvent.Block.Height +} + +// Hash returns the binary representation of the block's hash as a byte slice. +func (blockEvent *cometBlockEvent) Hash() []byte { + return blockEvent.Block.LastBlockID.Hash.Bytes() +} + +// newCometBlockEvent attempts to deserialize the given bytes into a comet block. +// if the resulting block has a height of zero, assume the event was not a block +// event and return an ErrUnmarshalBlockEvent error. +func newCometBlockEvent(blockMsgBz []byte) (client.Block, error) { + blockMsg := new(cometBlockEvent) + if err := json.Unmarshal(blockMsgBz, blockMsg); err != nil { + return nil, err + } + + // If msg does not match the expected format then the block's height has a zero value. + if blockMsg.Block.Header.Height == 0 { + return nil, ErrUnmarshalBlockEvent.Wrap(string(blockMsgBz)) + } + + return blockMsg, nil +} diff --git a/pkg/client/block/client.go b/pkg/client/block/client.go new file mode 100644 index 000000000..387f5f16b --- /dev/null +++ b/pkg/client/block/client.go @@ -0,0 +1,209 @@ +package block + +import ( + "context" + "fmt" + "time" + + "cosmossdk.io/depinject" + + "pocket/pkg/client" + "pocket/pkg/either" + "pocket/pkg/observable" + "pocket/pkg/observable/channel" + "pocket/pkg/retry" +) + +const ( + // eventsBytesRetryDelay is the delay between retry attempts when the events + // bytes observable returns an error. + eventsBytesRetryDelay = time.Second + // eventsBytesRetryLimit is the maximum number of times to attempt to + // re-establish the events query bytes subscription when the events bytes + // observable returns an error. + eventsBytesRetryLimit = 10 + eventsBytesRetryResetTimeout = 10 * time.Second + // NB: cometbft event subscription query for newly committed blocks. + // (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) + committedBlocksQuery = "tm.event='NewBlock'" + // latestBlockObsvblsReplayBufferSize is the replay buffer size of the + // latestBlockObsvbls replay observable which is used to cache the latest block observable. + // It is updated with a new "active" observable when a new + // events query subscription is created, for example, after a non-persistent + // connection error. + latestBlockObsvblsReplayBufferSize = 1 + // latestBlockReplayBufferSize is the replay buffer size of the latest block + // replay observable which is notified when block commit events are received + // by the events query client subscription created in goPublishBlocks. + latestBlockReplayBufferSize = 1 +) + +var ( + _ client.BlockClient = (*blockClient)(nil) + _ client.Block = (*cometBlockEvent)(nil) +) + +// blockClient implements the BlockClient interface. +type blockClient struct { + // endpointURL is the URL of RPC endpoint which eventsClient subscription + // requests will be sent. + endpointURL string + // eventsClient is the events query client which is used to subscribe to + // newly committed block events. It emits an either value which may contain + // an error, at most, once and closes immediately after if it does. + eventsClient client.EventsQueryClient + // latestBlockObsvbls is a replay observable with replay buffer size 1, + // which holds the "active latest block observable" which is notified when + // block commit events are received by the events query client subscription + // created in goPublishBlocks. This observable (and the one it emits) closes + // when the events bytes observable returns an error and is updated with a + // new "active" observable after a new events query subscription is created. + latestBlockObsvbls observable.ReplayObservable[client.BlocksObservable] + // latestBlockObsvblsReplayPublishCh is the publish channel for latestBlockObsvbls. + // It's used to set blockObsvbl initially and subsequently update it, for + // example, when the connection is re-established after erroring. + latestBlockObsvblsReplayPublishCh chan<- client.BlocksObservable +} + +// eventsBytesToBlockMapFn is a convenience type to represent the type of a +// function which maps event subscription message bytes into block event objects. +// This is used as a transformFn in a channel.Map() call and is the type returned +// by the newEventsBytesToBlockMapFn factory function. +type eventBytesToBlockMapFn func(either.Either[[]byte]) (client.Block, bool) + +// NewBlockClient creates a new block client from the given dependencies and cometWebsocketURL. +func NewBlockClient( + ctx context.Context, + deps depinject.Config, + cometWebsocketURL string, +) (client.BlockClient, error) { + // Initialize block client + bClient := &blockClient{endpointURL: cometWebsocketURL} + bClient.latestBlockObsvbls, bClient.latestBlockObsvblsReplayPublishCh = + channel.NewReplayObservable[client.BlocksObservable](ctx, latestBlockObsvblsReplayBufferSize) + + // Inject dependencies + if err := depinject.Inject(deps, &bClient.eventsClient); err != nil { + return nil, err + } + + // Concurrently publish blocks to the observable emitted by latestBlockObsvbls. + go bClient.goPublishBlocks(ctx) + + return bClient, nil +} + +// CommittedBlocksSequence returns a ReplayObservable, with a replay buffer size +// of 1, which is notified when block commit events are received by the events +// query subscription. +func (bClient *blockClient) CommittedBlocksSequence(ctx context.Context) client.BlocksObservable { + // Get the latest block observable from the replay observable. We only ever + // want the last 1 as any prior latest block observable values are closed. + // Directly accessing the zeroth index here is safe because the call to Last + // is guaranteed to return a slice with at least 1 element. + return bClient.latestBlockObsvbls.Last(ctx, 1)[0] +} + +// LatestBlock returns the latest committed block that's been received by the +// corresponding events query subscription. +// It blocks until at least one block event has been received. +func (bClient *blockClient) LatestBlock(ctx context.Context) client.Block { + return bClient.CommittedBlocksSequence(ctx).Last(ctx, 1)[0] +} + +// Close unsubscribes all observers of the committed blocks sequence observable +// and closes the events query client. +func (bClient *blockClient) Close() { + // Closing eventsClient will cascade unsubscribe and close downstream observers. + bClient.eventsClient.Close() +} + +// goPublishBlocks runs the work function returned by retryPublishBlocksFactory, +// re-invoking it according to the arguments to retry.OnError when the events bytes +// observable returns an asynchronous error. +// This function is intended to be called in a goroutine. +func (bClient *blockClient) goPublishBlocks(ctx context.Context) { + // React to errors by getting a new events bytes observable, re-mapping it, + // and send it to latestBlockObsvblsReplayPublishCh such that + // latestBlockObsvbls.Last(ctx, 1) will return it. + publishErr := retry.OnError( + ctx, + eventsBytesRetryLimit, + eventsBytesRetryDelay, + eventsBytesRetryResetTimeout, + "goPublishBlocks", + bClient.retryPublishBlocksFactory(ctx), + ) + + // If we get here, the retry limit was reached and the retry loop exited. + // Since this function runs in a goroutine, we can't return the error to the + // caller. Instead, we panic. + panic(fmt.Errorf("BlockClient.goPublishBlocks shold never reach this spot: %w", publishErr)) +} + +// retryPublishBlocksFactory returns a function which is intended to be passed to +// retry.OnError. The returned function pipes event bytes from the events query +// client, maps them to block events, and publishes them to the latestBlockObsvbls +// replay observable. +func (bClient *blockClient) retryPublishBlocksFactory(ctx context.Context) func() chan error { + return func() chan error { + errCh := make(chan error, 1) + eventsBzObsvbl, err := bClient.eventsClient.EventsBytes(ctx, committedBlocksQuery) + if err != nil { + errCh <- err + return errCh + } + + // NB: must cast back to generic observable type to use with Map. + // client.BlocksObservable is only used to workaround gomock's lack of + // support for generic types. + eventsBz := observable.Observable[either.Either[[]byte]](eventsBzObsvbl) + blockEventFromEventBz := newEventsBytesToBlockMapFn(errCh) + blocksObsvbl := channel.MapReplay(ctx, latestBlockReplayBufferSize, eventsBz, blockEventFromEventBz) + + // Initially set latestBlockObsvbls and update if after retrying on error. + bClient.latestBlockObsvblsReplayPublishCh <- blocksObsvbl + + return errCh + } +} + +// newEventsBytesToBlockMapFn is a factory for a function which is intended +// to be used as a transformFn in a channel.Map() call. Since the map function +// is called asynchronously, this factory creates a closure around an error channel +// which can be used for asynchronous error signaling from within the map function, +// and handling from the Map call context. +// +// The map function itself attempts to deserialize the given byte slice as a +// committed block event. If the events bytes observable contained an error, this value is not emitted +// (skipped) on the destination observable of the map operation. +// If deserialization failed because the event bytes were for a different event type, +// this value is also skipped. +// If deserialization failed for some other reason, this function panics. +func newEventsBytesToBlockMapFn(errCh chan<- error) eventBytesToBlockMapFn { + return func(eitherEventBz either.Either[[]byte]) (_ client.Block, skip bool) { + eventBz, err := eitherEventBz.ValueOrError() + if err != nil { + errCh <- err + // Don't publish (skip) if eitherEventBz contained an error. + // eitherEventBz should automatically close itself in this case. + // (i.e. no more values should be mapped to this transformFn's respective + // dstObservable). + return nil, true + } + + block, err := newCometBlockEvent(eventBz) + if err != nil { + if ErrUnmarshalBlockEvent.Is(err) { + // Don't publish (skip) if the message was not a block event. + return nil, true + } + + panic(fmt.Sprintf( + "unexpected error deserializing block event: %s; eventBz: %s", + err, string(eventBz), + )) + } + return block, false + } +} diff --git a/pkg/client/block/client_integration_test.go b/pkg/client/block/client_integration_test.go new file mode 100644 index 000000000..4f51d7873 --- /dev/null +++ b/pkg/client/block/client_integration_test.go @@ -0,0 +1,77 @@ +//go:build integration + +package block_test + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "pocket/internal/testclient/testblock" + "pocket/pkg/client" +) + +const blockIntegrationSubTimeout = 5 * time.Second + +func TestBlockClient_LatestBlock(t *testing.T) { + ctx := context.Background() + + blockClient := testblock.NewLocalnetClient(ctx, t) + require.NotNil(t, blockClient) + + block := blockClient.LatestBlock(ctx) + require.NotEmpty(t, block) +} + +func TestBlockClient_BlocksObservable(t *testing.T) { + ctx := context.Background() + + blockClient := testblock.NewLocalnetClient(ctx, t) + require.NotNil(t, blockClient) + + blockSub := blockClient.CommittedBlocksSequence(ctx).Subscribe(ctx) + + var ( + blockMu sync.Mutex + blockCounter int + blocksToRecv = 2 + errCh = make(chan error, 1) + ) + go func() { + var previousBlock client.Block + for block := range blockSub.Ch() { + if previousBlock != nil { + if !assert.Equal(t, previousBlock.Height()+1, block.Height()) { + errCh <- fmt.Errorf("expected block height %d, got %d", previousBlock.Height()+1, block.Height()) + return + } + } + previousBlock = block + + require.NotEmpty(t, block) + blockMu.Lock() + blockCounter++ + if blockCounter >= blocksToRecv { + errCh <- nil + return + } + blockMu.Unlock() + } + }() + + select { + case err := <-errCh: + require.NoError(t, err) + require.Equal(t, blocksToRecv, blockCounter) + case <-time.After(blockIntegrationSubTimeout): + t.Fatalf( + "timed out waiting for block subscription; expected %d blocks, got %d", + blocksToRecv, blockCounter, + ) + } +} diff --git a/pkg/client/block/client_test.go b/pkg/client/block/client_test.go new file mode 100644 index 000000000..c787f5ad2 --- /dev/null +++ b/pkg/client/block/client_test.go @@ -0,0 +1,138 @@ +package block_test + +import ( + "context" + "encoding/json" + "testing" + "time" + + "cosmossdk.io/depinject" + comettypes "github.com/cometbft/cometbft/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "pocket/internal/testclient" + "pocket/internal/testclient/testeventsquery" + "pocket/pkg/client" + "pocket/pkg/client/block" + eventsquery "pocket/pkg/client/events_query" +) + +const blockAssertionLoopTimeout = 500 * time.Millisecond + +func TestBlockClient(t *testing.T) { + var ( + expectedHeight = int64(1) + expectedHash = []byte("test_hash") + expectedBlockEvent = &testBlockEvent{ + Block: comettypes.Block{ + Header: comettypes.Header{ + Height: 1, + Time: time.Now(), + LastBlockID: comettypes.BlockID{ + Hash: expectedHash, + }, + }, + }, + } + ctx = context.Background() + ) + + // Set up a mock connection and dialer which are expected to be used once. + connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) + connMock.EXPECT().Send(gomock.Any()).Return(nil).Times(1) + // Mock the Receive method to return the expected block event. + connMock.EXPECT().Receive().DoAndReturn(func() ([]byte, error) { + blockEventJson, err := json.Marshal(expectedBlockEvent) + require.NoError(t, err) + return blockEventJson, nil + }).AnyTimes() + + // Set up events query client dependency. + dialerOpt := eventsquery.WithDialer(dialerMock) + eventsQueryClient := testeventsquery.NewLocalnetClient(t, dialerOpt) + deps := depinject.Supply(eventsQueryClient) + + // Set up block client. + blockClient, err := block.NewBlockClient(ctx, deps, testclient.CometLocalWebsocketURL) + require.NoError(t, err) + require.NotNil(t, blockClient) + + // Run LatestBlock and CommittedBlockSequence concurrently because they can + // block, leading to an unresponsive test. This function sends multiple values + // on the actualBlockCh which are all asserted against in blockAssertionLoop. + // If any of the methods under test hang, the test will time out. + var ( + actualBlockCh = make(chan client.Block, 1) + done = make(chan struct{}, 1) + ) + go func() { + // Test LatestBlock method. + actualBlock := blockClient.LatestBlock(ctx) + require.Equal(t, expectedHeight, actualBlock.Height()) + require.Equal(t, expectedHash, actualBlock.Hash()) + + // Test CommittedBlockSequence method. + blockObservable := blockClient.CommittedBlocksSequence(ctx) + require.NotNil(t, blockObservable) + + // Ensure that the observable is replayable via Last. + actualBlockCh <- blockObservable.Last(ctx, 1)[0] + + // Ensure that the observable is replayable via Subscribe. + blockObserver := blockObservable.Subscribe(ctx) + for block := range blockObserver.Ch() { + actualBlockCh <- block + break + } + + // Signal test completion + done <- struct{}{} + }() + + // blockAssertionLoop ensures that the blocks retrieved from both LatestBlock + // method and CommittedBlocksSequence method match the expected block height + // and hash. This loop waits for blocks to be sent on the actualBlockCh channel + // by the methods being tested. Once the methods are done, they send a signal on + // the "done" channel. If the blockAssertionLoop doesn't receive any block or + // the done signal within a specific timeout, it assumes something has gone wrong + // and fails the test. +blockAssertionLoop: + for { + select { + case actualBlock := <-actualBlockCh: + require.Equal(t, expectedHeight, actualBlock.Height()) + require.Equal(t, expectedHash, actualBlock.Hash()) + case <-done: + break blockAssertionLoop + case <-time.After(blockAssertionLoopTimeout): + t.Fatal("timed out waiting for block event") + } + } + + // Wait a tick for the observables to be set up. + time.Sleep(time.Millisecond) + + blockClient.Close() +} + +/* +TODO_TECHDEBT/TODO_CONSIDERATION(#XXX): this duplicates the unexported block event + +type from pkg/client/block/block.go. We seem to have some conflicting preferences +which result in the need for this duplication until a preferred direction is +identified: + + - We should prefer tests being in their own pkgs (e.g. block_test) + - this would resolve if this test were in the block package instead. + - We should prefer to not export types which don't require exporting for API + consumption. + - This test is the only external (to the block pkg) dependency of cometBlockEvent. + - We could use the //go:build test constraint on a new file which exports it + for testing purposes. + - This would imply that we also add -tags=test to all applicable tooling + and add a test which fails if the tag is absent. +*/ +type testBlockEvent struct { + Block comettypes.Block `json:"block"` +} diff --git a/pkg/client/block/errors.go b/pkg/client/block/errors.go new file mode 100644 index 000000000..0a0cc28c9 --- /dev/null +++ b/pkg/client/block/errors.go @@ -0,0 +1,8 @@ +package block + +import errorsmod "cosmossdk.io/errors" + +var ( + ErrUnmarshalBlockEvent = errorsmod.Register(codespace, 1, "failed to unmarshal committed block event") + codespace = "block_client" +) diff --git a/pkg/client/interface.go b/pkg/client/interface.go index bd811a153..1007656a5 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -1,4 +1,4 @@ -//go:generate mockgen -destination=../../internal/mocks/mockclient/query_client_mock.go -package=mockclient . Dialer,Connection +//go:generate mockgen -destination=../../internal/mocks/mockclient/events_query_client_mock.go -package=mockclient . Dialer,Connection,EventsQueryClient package client @@ -9,6 +9,28 @@ import ( "pocket/pkg/observable" ) +// BlocksObservable is an observable which is notified with an either +// value which contains either an error or the event message bytes. +// TODO_HACK: The purpose of this type is to work around gomock's lack of +// support for generic types. For the same reason, this type cannot be an +// alias (i.e. EventsBytesObservable = observable.Observable[either.Either[[]byte]]). +type BlocksObservable observable.ReplayObservable[Block] + +type BlockClient interface { + // Blocks returns an observable which emits newly committed blocks. + CommittedBlocksSequence(context.Context) BlocksObservable + // LatestBlock returns the latest block that has been committed. + LatestBlock(context.Context) Block + // Close unsubscribes all observers of the committed blocks sequence observable + // and closes the events query client. + Close() +} + +type Block interface { + Height() int64 + Hash() []byte +} + // TODO_CONSIDERATION: the cosmos-sdk CLI code seems to use a cometbft RPC client // which includes a `#Subscribe()` method for a similar purpose. Perhaps we could // replace this custom websocket client with that. diff --git a/pkg/observable/channel/map.go b/pkg/observable/channel/map.go index 942859e50..912043ca9 100644 --- a/pkg/observable/channel/map.go +++ b/pkg/observable/channel/map.go @@ -6,6 +6,8 @@ import ( "pocket/pkg/observable" ) +type MapFn[S, D any] func(src S) (dst D, skip bool) + // Map transforms the given observable by applying the given transformFn to each // notification received from the observable. If the transformFn returns a skip // bool of true, the notification is skipped and not emitted to the resulting @@ -14,7 +16,7 @@ func Map[S, D any]( ctx context.Context, srcObservable observable.Observable[S], // TODO_CONSIDERATION: if this were variadic, it could simplify serial transformations. - transformFn func(src S) (dst D, skip bool), + transformFn MapFn[S, D], ) observable.Observable[D] { dstObservable, dstProducer := NewObservable[D]() srcObserver := srcObservable.Subscribe(ctx) @@ -32,3 +34,33 @@ func Map[S, D any]( return dstObservable } + +// MapReplay transforms the given observable by applying the given transformFn to +// each notification received from the observable. If the transformFn returns a +// skip bool of true, the notification is skipped and not emitted to the resulting +// observable. +// The resulting observable will receive the last replayBufferSize +// number of values published to the source observable before receiving new values. +func MapReplay[S, D any]( + ctx context.Context, + replayBufferSize int, + srcObservable observable.Observable[S], + // TODO_CONSIDERATION: if this were variadic, it could simplify serial transformations. + transformFn func(src S) (dst D, skip bool), +) observable.ReplayObservable[D] { + dstObservable, dstProducer := NewReplayObservable[D](ctx, replayBufferSize) + srcObserver := srcObservable.Subscribe(ctx) + + go func() { + for srcNotification := range srcObserver.Ch() { + dstNotification, skip := transformFn(srcNotification) + if skip { + continue + } + + dstProducer <- dstNotification + } + }() + + return dstObservable +} diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go new file mode 100644 index 000000000..f76a7dcf5 --- /dev/null +++ b/pkg/retry/retry.go @@ -0,0 +1,72 @@ +package retry + +import ( + "context" + "log" + "time" +) + +type RetryFunc func() chan error + +// OnError continuously invokes the provided work function (workFn) until either the context (ctx) +// is canceled or the error channel returned by workFn is closed. If workFn encounters an error, +// OnError will retry invoking workFn based on the provided retry parameters. +// +// Parameters: +// - ctx: the context to monitor for cancellation. If canceled, OnError will exit without error. +// - retryLimit: the maximum number of retries for workFn upon encountering an error. +// - retryDelay: the duration to wait before retrying workFn after an error. +// - retryResetCount: Specifies the duration of continuous error-free operation required +// before the retry count is reset. If the work function operates without +// errors for this duration, any subsequent error will restart the retry +// count from the beginning. +// - workName: a name or descriptor for the work function, used for logging purposes. +// - workFn: a function that performs some work and returns an error channel. +// This channel emits errors encountered during the work. +// +// Returns: +// - If the context is canceled, the function returns nil. +// - If the error channel is closed, a warning is logged, and the function returns nil. +// - If the retry limit is reached, the function returns the error from the channel. +// +// Note: After each error, a delay specified by retryDelay is introduced before retrying workFn.func OnError( +func OnError( + ctx context.Context, + retryLimit int, + retryDelay time.Duration, + retryResetTimeout time.Duration, + workName string, + workFn RetryFunc, +) error { + var retryCount int + errCh := workFn() + for { + select { + case <-ctx.Done(): + return nil + case <-time.After(retryResetTimeout): + retryCount = 0 + case err, ok := <-errCh: + // Exit the retry loop if the error channel is closed. + if !ok { + log.Printf( + "WARN: error channel for %s closed, will no longer retry on error\n", + workName, + ) + return nil + } + + if retryCount >= retryLimit { + return err + } + + // Wait retryDelay before retrying. + time.Sleep(retryDelay) + + // Increment retryCount and retry workFn. + retryCount++ + errCh = workFn() + log.Printf("ERROR: retrying %s after error: %s\n", workName, err) + } + } +} diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go new file mode 100644 index 000000000..8a1154c30 --- /dev/null +++ b/pkg/retry/retry_test.go @@ -0,0 +1,337 @@ +package retry_test + +/* TODO_TECHDEBT: improve this test: +- fix race condition around the logOutput buffer +- factor our common setup and assertion code +- drive out flakiness +- improve comments +*/ + +import ( + "bytes" + "context" + "fmt" + "log" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "pocket/pkg/retry" +) + +var testErr = fmt.Errorf("test error") + +// TestOnError verifies the behavior of the OnError function in the retry package. +// It ensures that the function correctly retries a failing operation for a specified +// number of times with the expected delay between retries. +func TestOnError(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test should pass but contains a race condition around the logOutput buffer") + + // Setting up the test variables. + var ( + // logOutput captures the log output for verification of logged messages. + logOutput bytes.Buffer + // expectedRetryDelay is the duration we expect between retries. + expectedRetryDelay = time.Millisecond + // expectedRetryLimit is the maximum number of retries the test expects. + expectedRetryLimit = 5 + // retryResetTimeout is the duration after which the retry count should reset. + retryResetTimeout = time.Second + // testFnCallCount keeps track of how many times the test function is called. + testFnCallCount int32 + // testFnCallTimeCh is a channel receives a time.Time each when the test + // function is called. + testFnCallTimeCh = make(chan time.Time, expectedRetryLimit) + ctx = context.Background() + ) + + // Redirect the standard logger's output to our custom buffer for later verification. + log.SetOutput(&logOutput) + + // Define testFn, a function that simulates a failing operation and logs its invocation times. + testFn := func() chan error { + // Record the current time to track the delay between retries. + testFnCallTimeCh <- time.Now() + + // Create a channel to return an error, simulating a failing operation. + errCh := make(chan error, 1) + errCh <- testErr + + // Increment the call count safely across goroutine boundaries. + atomic.AddInt32(&testFnCallCount, 1) + + return errCh + } + + // Create a channel to receive the error result from the OnError function. + retryOnErrorErrCh := make(chan error, 1) + + // Start the OnError function in a separate goroutine, simulating concurrent operation. + go func() { + // Call the OnError function with the test parameters and function. + retryOnErrorErrCh <- retry.OnError( + ctx, + expectedRetryLimit, + expectedRetryDelay, + retryResetTimeout, + "TestOnError", + testFn, + ) + }() + + // Calculate the total expected time for all retries to complete. + totalExpectedDelay := expectedRetryDelay * time.Duration(expectedRetryLimit) + // Wait for the OnError function to execute and retry the expected number of times. + time.Sleep(totalExpectedDelay + 100*time.Millisecond) + + // Verify that the test function was called the expected number of times. + require.Equal(t, expectedRetryLimit, int(testFnCallCount), "Test function was not called the expected number of times") + + // Verify the delay between retries of the test function. + var prevCallTime time.Time + for i := 0; i < expectedRetryLimit; i++ { + // Retrieve the next function call time from the channel. + nextCallTime, ok := <-testFnCallTimeCh + if !ok { + t.Fatalf("expected %d calls to testFn, but channel closed after %d", expectedRetryLimit, i) + } + + // For all calls after the first, check that the delay since the previous call meets expectations. + if i != 0 { + actualRetryDelay := nextCallTime.Sub(prevCallTime) + require.GreaterOrEqual(t, actualRetryDelay, expectedRetryDelay, "Retry delay was less than expected") + } + + // Update prevCallTime for the next iteration. + prevCallTime = nextCallTime + } + + // Verify that the OnError function returned the expected error. + select { + case err := <-retryOnErrorErrCh: + require.ErrorIs(t, err, testErr, "OnError did not return the expected error") + case <-time.After(100 * time.Millisecond): + t.Fatal("expected error from OnError, but none received") + } + + // Verify the error messages logged during the retries. + expectedErrorLine := "ERROR: retrying TestOnError after error: test error" + trimmedLogOutput := strings.Trim(logOutput.String(), "\n") + logOutputLines := strings.Split(trimmedLogOutput, "\n") + require.Lenf(t, logOutputLines, expectedRetryLimit, "unexpected number of log lines") + for _, line := range logOutputLines { + require.Contains(t, line, expectedErrorLine, "log line does not contain the expected prefix") + } +} + +// TODO_TECHDEBT: assert that the retry loop exits when the context is closed. +func TestOnError_ExitsWhenCtxCloses(t *testing.T) { + t.SkipNow() +} + +func TestOnError_ExitsWhenErrChCloses(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test should pass but contains a race condition around the logOutput buffer") + + // Setup test variables and log capture + var ( + logOutput bytes.Buffer + testFnCallCount int32 + expectedRetryDelay = time.Millisecond + expectedRetryLimit = 3 + retryLimit = 5 + retryResetTimeout = time.Second + testFnCallTimeCh = make(chan time.Time, expectedRetryLimit) + ctx = context.Background() + ) + + // Redirect the log output for verification later + log.SetOutput(&logOutput) + + // Define the test function that simulates an error and counts its invocations + testFn := func() chan error { + atomic.AddInt32(&testFnCallCount, 1) // Increment the invocation count atomically + testFnCallTimeCh <- time.Now() // Track the invocation time + + errCh := make(chan error, 1) + if atomic.LoadInt32(&testFnCallCount) >= int32(expectedRetryLimit) { + close(errCh) + return errCh + } + + errCh <- testErr + return errCh + } + + retryOnErrorErrCh := make(chan error, 1) + // Spawn a goroutine to test the OnError function + go func() { + retryOnErrorErrCh <- retry.OnError( + ctx, + retryLimit, + expectedRetryDelay, + retryResetTimeout, + "TestOnError_ExitsWhenErrChCloses", + testFn, + ) + }() + + // Wait for the OnError function to execute and retry the expected number of times + totalExpectedDelay := expectedRetryDelay * time.Duration(expectedRetryLimit) + time.Sleep(totalExpectedDelay + 100*time.Millisecond) + + // Assert that the test function was called the expected number of times + require.Equal(t, expectedRetryLimit, int(testFnCallCount)) + + // Assert that the retry delay between function calls matches the expected delay + var prevCallTime = new(time.Time) + for i := 0; i < expectedRetryLimit; i++ { + select { + case nextCallTime := <-testFnCallTimeCh: + if i != 0 { + actualRetryDelay := nextCallTime.Sub(*prevCallTime) + require.GreaterOrEqual(t, actualRetryDelay, expectedRetryDelay) + } + + *prevCallTime = nextCallTime + default: + t.Fatalf( + "expected %d calls to testFn, but only received %d", + expectedRetryLimit, i+1, + ) + } + } + + select { + case err := <-retryOnErrorErrCh: + require.NoError(t, err) + case <-time.After(100 * time.Millisecond): + t.Fatalf("expected error from OnError, but none received") + } + + // Verify the logged error messages + var ( + logOutputLines = strings.Split(strings.Trim(logOutput.String(), "\n"), "\n") + errorLines = logOutputLines[:len(logOutputLines)-1] + warnLine = logOutputLines[len(logOutputLines)-1] + expectedWarnMsg = "WARN: error channel for TestOnError_ExitsWhenErrChCloses closed, will no longer retry on error" + expectedErrorMsg = "ERROR: retrying TestOnError_ExitsWhenErrChCloses after error: test error" + ) + + require.Lenf( + t, logOutputLines, + expectedRetryLimit, + "expected %d log lines, got %d", + expectedRetryLimit, len(logOutputLines), + ) + for _, line := range errorLines { + require.Contains(t, line, expectedErrorMsg) + } + require.Contains(t, warnLine, expectedWarnMsg) +} + +// assert that retryCount resets on success +func TestOnError_RetryCountResetTimeout(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test should pass but contains a race condition around the logOutput buffer") + + // Setup test variables and log capture + var ( + logOutput bytes.Buffer + testFnCallCount int32 + expectedRetryDelay = time.Millisecond + expectedRetryLimit = 9 + retryLimit = 5 + retryResetTimeout = 3 * time.Millisecond + testFnCallTimeCh = make(chan time.Time, expectedRetryLimit) + ctx = context.Background() + ) + + // Redirect the log output for verification later + log.SetOutput(&logOutput) + + // Define the test function that simulates an error and counts its invocations + testFn := func() chan error { + // Track the invocation time + testFnCallTimeCh <- time.Now() + + errCh := make(chan error, 1) + + count := atomic.LoadInt32(&testFnCallCount) + if count == int32(retryLimit) { + go func() { + time.Sleep(retryResetTimeout) + errCh <- testErr + }() + } else { + errCh <- testErr + } + + // Increment the invocation count atomically + atomic.AddInt32(&testFnCallCount, 1) + return errCh + } + + retryOnErrorErrCh := make(chan error, 1) + // Spawn a goroutine to test the OnError function + go func() { + retryOnErrorErrCh <- retry.OnError( + ctx, + retryLimit, + expectedRetryDelay, + retryResetTimeout, + "TestOnError", + testFn, + ) + }() + + // Wait for the OnError function to execute and retry the expected number of times + totalExpectedDelay := expectedRetryDelay * time.Duration(expectedRetryLimit) + time.Sleep(totalExpectedDelay + 100*time.Millisecond) + + // Assert that the test function was called the expected number of times + require.Equal(t, expectedRetryLimit, int(testFnCallCount)) + + // Assert that the retry delay between function calls matches the expected delay + var prevCallTime = new(time.Time) + for i := 0; i < expectedRetryLimit; i++ { + select { + case nextCallTime := <-testFnCallTimeCh: + if i != 0 { + actualRetryDelay := nextCallTime.Sub(*prevCallTime) + require.GreaterOrEqual(t, actualRetryDelay, expectedRetryDelay) + } + + *prevCallTime = nextCallTime + default: + t.Fatalf( + "expected %d calls to testFn, but only received %d", + expectedRetryLimit, i+1, + ) + } + } + + // Verify the logged error messages + var ( + logOutputLines = strings.Split(strings.Trim(logOutput.String(), "\n"), "\n") + expectedPrefix = "ERROR: retrying TestOnError after error: test error" + ) + + select { + case err := <-retryOnErrorErrCh: + require.ErrorIs(t, err, testErr) + case <-time.After(100 * time.Millisecond): + t.Fatalf("expected error from OnError, but none received") + } + + require.Lenf( + t, logOutputLines, + expectedRetryLimit-1, + "expected %d log lines, got %d", + expectedRetryLimit-1, len(logOutputLines), + ) + for _, line := range logOutputLines { + require.Contains(t, line, expectedPrefix) + } +} From 7399b46aa2f8b86488587ab0634435791521dd3b Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Wed, 1 Nov 2023 16:10:06 +0000 Subject: [PATCH 17/28] [AppGate] Implement UndelegateFromGateway with Extensive Tests (#125) --- Makefile | 16 + docs/static/openapi.yml | 507 +----------------- go.mod | 4 +- proto/pocket/application/tx.proto | 4 +- .../client/cli/tx_undelegate_from_gateway.go | 19 +- .../cli/tx_undelegate_from_gateway_test.go | 117 ++++ .../msg_server_undelegate_from_gateway.go | 33 +- ...msg_server_undelegate_from_gateway_test.go | 220 ++++++++ .../simulation/undelegate_from_gateway.go | 11 +- x/application/types/errors.go | 1 + .../types/message_undelegate_from_gateway.go | 17 +- .../message_undelegate_from_gateway_test.go | 28 +- 12 files changed, 447 insertions(+), 530 deletions(-) create mode 100644 x/application/client/cli/tx_undelegate_from_gateway_test.go create mode 100644 x/application/keeper/msg_server_undelegate_from_gateway_test.go diff --git a/Makefile b/Makefile index be2e63282..3b68fff2a 100644 --- a/Makefile +++ b/Makefile @@ -298,6 +298,22 @@ app2_delegate_gateway2: ## Delegate trust to gateway2 app3_delegate_gateway3: ## Delegate trust to gateway3 APP=app3 GATEWAY_ADDR=pokt1zhmkkd0rh788mc9prfq0m2h88t9ge0j83gnxya make app_delegate +.PHONY: app_undelegate +app_undelegate: ## Undelegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked + pocketd --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + +.PHONY: app1_undelegate_gateway1 +app1_undelegate_gateway1: ## Undelegate trust to gateway1 + APP=app1 GATEWAY_ADDR=pokt15vzxjqklzjtlz7lahe8z2dfe9nm5vxwwmscne4 make app_undelegate + +.PHONY: app2_undelegate_gateway2 +app2_undelegate_gateway2: ## Undelegate trust to gateway2 + APP=app2 GATEWAY_ADDR=pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz make app_undelegate + +.PHONY: app3_undelegate_gateway3 +app3_undelegate_gateway3: ## Undelegate trust to gateway3 + APP=app3 GATEWAY_ADDR=pokt1zhmkkd0rh788mc9prfq0m2h88t9ge0j83gnxya make app_undelegate + ################# ### Suppliers ### ################# diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index d6f879b53..0e4c63907 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46737,7 +46737,7 @@ paths: type: object properties: max_delegated_gateways: - type: integer + type: string format: int64 title: >- The maximum number of gateways an application can delegate @@ -47973,174 +47973,7 @@ paths: properties: '@type': type: string - description: >- - A URL/resource name that uniquely identifies the type of - the serialized - - protocol buffer message. This string must contain at - least - - one "/" character. The last segment of the URL's path - must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in - a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary - all types that they - - expect it to use in the context of Any. However, for - URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally - set up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based - on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in - the official - - protobuf release, and it is not used for type URLs - beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer - message along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values - in the form - - of utility functions or additional generated methods of the - Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by - default use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the - last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield - type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with - an - - additional field `@type` which contains the type URL. - Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom - JSON - - representation, that representation will be embedded adding - a field - - `value` which holds the custom JSON in addition to the - `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } tags: - Query /pocket/supplier/supplier: @@ -48316,174 +48149,7 @@ paths: properties: '@type': type: string - description: >- - A URL/resource name that uniquely identifies the type of - the serialized - - protocol buffer message. This string must contain at - least - - one "/" character. The last segment of the URL's path - must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in - a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary - all types that they - - expect it to use in the context of Any. However, for - URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally - set up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based - on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in - the official - - protobuf release, and it is not used for type URLs - beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer - message along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values - in the form - - of utility functions or additional generated methods of the - Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by - default use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the - last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield - type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with - an - - additional field `@type` which contains the type URL. - Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom - JSON - - representation, that representation will be embedded adding - a field - - `value` which holds the custom JSON in addition to the - `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } parameters: - name: pagination.key description: |- @@ -48687,174 +48353,7 @@ paths: properties: '@type': type: string - description: >- - A URL/resource name that uniquely identifies the type of - the serialized - - protocol buffer message. This string must contain at - least - - one "/" character. The last segment of the URL's path - must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in - a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary - all types that they - - expect it to use in the context of Any. However, for - URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally - set up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based - on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in - the official - - protobuf release, and it is not used for type URLs - beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer - message along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values - in the form - - of utility functions or additional generated methods of the - Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by - default use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the - last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield - type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with - an - - additional field `@type` which contains the type URL. - Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom - JSON - - representation, that representation will be embedded adding - a field - - `value` which holds the custom JSON in addition to the - `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } parameters: - name: address in: path @@ -77840,7 +77339,7 @@ definitions: type: object properties: max_delegated_gateways: - type: integer + type: string format: int64 title: The maximum number of gateways an application can delegate trust to description: Params defines the parameters for the module. @@ -78018,7 +77517,7 @@ definitions: type: object properties: max_delegated_gateways: - type: integer + type: string format: int64 title: >- The maximum number of gateways an application can delegate trust diff --git a/go.mod b/go.mod index 7e42ae4a4..12d4e67a9 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( cosmossdk.io/math v1.0.1 github.com/cometbft/cometbft v0.37.2 github.com/cometbft/cometbft-db v0.8.0 + github.com/cosmos/cosmos-proto v1.0.0-beta.2 github.com/cosmos/cosmos-sdk v0.47.3 github.com/cosmos/gogoproto v1.4.10 github.com/cosmos/ibc-go/v7 v7.1.0 @@ -26,6 +27,7 @@ require ( go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.12.0 golang.org/x/sync v0.3.0 + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 google.golang.org/grpc v1.56.1 gopkg.in/yaml.v2 v2.4.0 ) @@ -69,7 +71,6 @@ require ( github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect - github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.0 // indirect @@ -265,7 +266,6 @@ require ( gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/api v0.122.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/proto/pocket/application/tx.proto b/proto/pocket/application/tx.proto index 0b49ce706..e9f50a048 100644 --- a/proto/pocket/application/tx.proto +++ b/proto/pocket/application/tx.proto @@ -42,7 +42,9 @@ message MsgDelegateToGateway { message MsgDelegateToGatewayResponse {} message MsgUndelegateFromGateway { - string address = 1; + option (cosmos.msg.v1.signer) = "appAddress"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries + string appAddress = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding + string gatewayAddress = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the gateway the application wants to undelegate from using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding } message MsgUndelegateFromGatewayResponse {} diff --git a/x/application/client/cli/tx_undelegate_from_gateway.go b/x/application/client/cli/tx_undelegate_from_gateway.go index 6c6bdf2a1..0b31de3d2 100644 --- a/x/application/client/cli/tx_undelegate_from_gateway.go +++ b/x/application/client/cli/tx_undelegate_from_gateway.go @@ -3,22 +3,29 @@ package cli import ( "strconv" + "pocket/x/application/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/application/types" ) var _ = strconv.Itoa(0) func CmdUndelegateFromGateway() *cobra.Command { cmd := &cobra.Command{ - Use: "undelegate-from-gateway", - Short: "Broadcast message undelegate-from-gateway", - Args: cobra.ExactArgs(0), + Use: "undelegate-from-gateway [gateway address]", + Short: "Undelegate an application from a gateway", + Long: `Undelegate an application from the gateway with the provided address. This is a broadcast operation +that removes the authority from the gateway specified to sign relays requests for the application, disallowing the gateway +act on the behalf of the application during a session. + +Example: +$ pocketd --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { - + gatewayAddress := args[0] clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err @@ -26,10 +33,12 @@ func CmdUndelegateFromGateway() *cobra.Command { msg := types.NewMsgUndelegateFromGateway( clientCtx.GetFromAddress().String(), + gatewayAddress, ) if err := msg.ValidateBasic(); err != nil { return err } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } diff --git a/x/application/client/cli/tx_undelegate_from_gateway_test.go b/x/application/client/cli/tx_undelegate_from_gateway_test.go new file mode 100644 index 000000000..93e9382ff --- /dev/null +++ b/x/application/client/cli/tx_undelegate_from_gateway_test.go @@ -0,0 +1,117 @@ +package cli_test + +import ( + "fmt" + "testing" + + sdkerrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/testutil" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/status" + + "pocket/testutil/network" + "pocket/x/application/client/cli" + "pocket/x/application/types" +) + +func TestCLI_UndelegateFromGateway(t *testing.T) { + net, _ := networkWithApplicationObjects(t, 2) + val := net.Validators[0] + ctx := val.ClientCtx + + // Create a keyring and add an account for the application to be delegated + // and the gateway to be delegated to + kr := ctx.Keyring + accounts := testutil.CreateKeyringAccounts(t, kr, 2) + appAccount := accounts[0] + gatewayAccount := accounts[1] + + // Update the context with the new keyring + ctx = ctx.WithKeyring(kr) + + // Common args used for all requests + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(net.Config.BondDenom, sdkmath.NewInt(10))).String()), + } + + tests := []struct { + desc string + appAddress string + gatewayAddress string + err *sdkerrors.Error + }{ + { + desc: "undelegate from gateway: valid", + appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + }, + { + desc: "invalid - missing app address", + // appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - invalid app address", + appAddress: "invalid address", + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - missing gateway address", + appAddress: appAccount.Address.String(), + // gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidGatewayAddress, + }, + { + desc: "invalid - invalid gateway address", + appAddress: appAccount.Address.String(), + gatewayAddress: "invalid address", + err: types.ErrAppInvalidGatewayAddress, + }, + } + + // Initialize the App and Gateway Accounts by sending it some funds from the validator account that is part of genesis + network.InitAccount(t, net, appAccount.Address) + network.InitAccount(t, net, gatewayAccount.Address) + + // Run the tests + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + // Wait for a new block to be committed + require.NoError(t, net.WaitForNextBlock()) + + // Prepare the arguments for the CLI command + args := []string{ + tt.gatewayAddress, + fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.appAddress), + } + args = append(args, commonArgs...) + + // Execute the command + undelegateOutput, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdUndelegateFromGateway(), args) + + // Validate the error if one is expected + if tt.err != nil { + stat, ok := status.FromError(tt.err) + require.True(t, ok) + require.Contains(t, stat.Message(), tt.err.Error()) + return + } + require.NoError(t, err) + + // Check the response + var resp sdk.TxResponse + require.NoError(t, net.Config.Codec.UnmarshalJSON(undelegateOutput.Bytes(), &resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.TxHash) + require.Equal(t, uint32(0), resp.Code) + }) + } +} diff --git a/x/application/keeper/msg_server_undelegate_from_gateway.go b/x/application/keeper/msg_server_undelegate_from_gateway.go index f4b1d45d9..39889063a 100644 --- a/x/application/keeper/msg_server_undelegate_from_gateway.go +++ b/x/application/keeper/msg_server_undelegate_from_gateway.go @@ -3,6 +3,7 @@ package keeper import ( "context" + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "pocket/x/application/types" @@ -11,12 +12,40 @@ import ( func (k msgServer) UndelegateFromGateway(goCtx context.Context, msg *types.MsgUndelegateFromGateway) (*types.MsgUndelegateFromGatewayResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + logger := k.Logger(ctx).With("method", "UndelegateFromGateway") + logger.Info("About to undelegate application from gateway with msg: %v", msg) + if err := msg.ValidateBasic(); err != nil { + logger.Error("Undelegation Message failed basic validation: %v", err) return nil, err } - // TODO: Handling the message - _ = ctx + // Retrieve the application from the store + app, found := k.GetApplication(ctx, msg.AppAddress) + if !found { + logger.Info("Application not found with address [%s]", msg.AppAddress) + return nil, sdkerrors.Wrapf(types.ErrAppNotFound, "application not found with address: %s", msg.AppAddress) + } + logger.Info("Application found with address [%s]", msg.AppAddress) + + // Check if the application is already delegated to the gateway + foundIdx := -1 + for i, gatewayAddr := range app.DelegateeGatewayAddresses { + if gatewayAddr == msg.GatewayAddress { + foundIdx = i + } + } + if foundIdx == -1 { + logger.Info("Application not delegated to gateway with address [%s]", msg.GatewayAddress) + return nil, sdkerrors.Wrapf(types.ErrAppNotDelegated, "application not delegated to gateway with address: %s", msg.GatewayAddress) + } + + // Remove the gateway from the application's delegatee gateway public keys + app.DelegateeGatewayAddresses = append(app.DelegateeGatewayAddresses[:foundIdx], app.DelegateeGatewayAddresses[foundIdx+1:]...) + + // Update the application store with the new delegation + k.SetApplication(ctx, app) + logger.Info("Successfully undelegated application from gateway for app: %+v", app) return &types.MsgUndelegateFromGatewayResponse{}, nil } diff --git a/x/application/keeper/msg_server_undelegate_from_gateway_test.go b/x/application/keeper/msg_server_undelegate_from_gateway_test.go new file mode 100644 index 000000000..e9c9bf70c --- /dev/null +++ b/x/application/keeper/msg_server_undelegate_from_gateway_test.go @@ -0,0 +1,220 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + keepertest "pocket/testutil/keeper" + "pocket/testutil/sample" + "pocket/x/application/keeper" + "pocket/x/application/types" + sharedtypes "pocket/x/shared/types" +) + +func TestMsgServer_UndelegateFromGateway_SuccessfullyUndelegate(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateways + appAddr := sample.AccAddress() + gatewayAddresses := make([]string, int(k.GetParams(ctx).MaxDelegatedGateways)) + for i := 0; i < len(gatewayAddresses); i++ { + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + gatewayAddresses[i] = gatewayAddr + } + t.Cleanup(func() { + for _, gatewayAddr := range gatewayAddresses { + delete(keepertest.StakedGatewayMap, gatewayAddr) + } + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation messages and delegate the application to the gateways + for _, gatewayAddr := range gatewayAddresses { + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + } + + // Verify that the application exists + maxDelegatedGateways := k.GetParams(ctx).MaxDelegatedGateways + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, maxDelegatedGateways, int64(len(foundApp.DelegateeGatewayAddresses))) + for i, gatewayAddr := range gatewayAddresses { + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[i]) + } + + // Prepare an undelegation message + undelegateMsg := &types.MsgUndelegateFromGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddresses[3], + } + + // Undelegate the application from the gateway + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.NoError(t, err) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, maxDelegatedGateways-1, int64(len(foundApp.DelegateeGatewayAddresses))) + gatewayAddresses = append(gatewayAddresses[:3], gatewayAddresses[4:]...) + for i, gatewayAddr := range gatewayAddresses { + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[i]) + } +} + +func TestMsgServer_UndelegateFromGateway_FailNotDelegated(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateway + appAddr := sample.AccAddress() + gatewayAddr1 := sample.AccAddress() + gatewayAddr2 := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr1] = struct{}{} + keepertest.StakedGatewayMap[gatewayAddr2] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr1) + delete(keepertest.StakedGatewayMap, gatewayAddr2) + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the undelegation message + undelegateMsg := &types.MsgUndelegateFromGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr1, + } + + // Attempt to undelgate the application from the gateway + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.ErrorIs(t, err, types.ErrAppNotDelegated) + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses)) + + // Prepare a delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr2, + } + + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Ensure the failed undelegation did not affect the application + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.ErrorIs(t, err, types.ErrAppNotDelegated) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr2, foundApp.DelegateeGatewayAddresses[0]) +} + +func TestMsgServer_UndelegateFromGateway_SuccessfullyUndelegateFromUnstakedGateway(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateways + appAddr := sample.AccAddress() + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message and delegate the application to the gateway + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Verify that the application exists + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[0]) + + // Mock unstaking the gateway + delete(keepertest.StakedGatewayMap, gatewayAddr) + + // Prepare an undelegation message + undelegateMsg := &types.MsgUndelegateFromGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Undelegate the application from the gateway + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.NoError(t, err) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses)) +} diff --git a/x/application/simulation/undelegate_from_gateway.go b/x/application/simulation/undelegate_from_gateway.go index ae03b5927..c5702c0d5 100644 --- a/x/application/simulation/undelegate_from_gateway.go +++ b/x/application/simulation/undelegate_from_gateway.go @@ -3,11 +3,12 @@ package simulation import ( "math/rand" + "pocket/x/application/keeper" + "pocket/x/application/types" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/application/keeper" - "pocket/x/application/types" ) func SimulateMsgUndelegateFromGateway( @@ -17,9 +18,11 @@ func SimulateMsgUndelegateFromGateway( ) simtypes.Operation { return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - simAccount, _ := simtypes.RandomAcc(r, accs) + simAppAccount, _ := simtypes.RandomAcc(r, accs) + simGatewayAccount, _ := simtypes.RandomAcc(r, accs) msg := &types.MsgUndelegateFromGateway{ - Address: simAccount.Address.String(), + AppAddress: simAppAccount.Address.String(), + GatewayAddress: simGatewayAccount.Address.String(), } // TODO: Handling the UndelegateFromGateway simulation diff --git a/x/application/types/errors.go b/x/application/types/errors.go index 3445ec6f4..ea89e77e1 100644 --- a/x/application/types/errors.go +++ b/x/application/types/errors.go @@ -18,4 +18,5 @@ var ( ErrAppAlreadyDelegated = sdkerrors.Register(ModuleName, 9, "application already delegated to gateway") ErrAppMaxDelegatedGateways = sdkerrors.Register(ModuleName, 10, "maximum number of delegated gateways reached") ErrAppInvalidMaxDelegatedGateways = sdkerrors.Register(ModuleName, 11, "invalid MaxDelegatedGateways parameter") + ErrAppNotDelegated = sdkerrors.Register(ModuleName, 12, "application not delegated to gateway") ) diff --git a/x/application/types/message_undelegate_from_gateway.go b/x/application/types/message_undelegate_from_gateway.go index 240605383..4d74748c1 100644 --- a/x/application/types/message_undelegate_from_gateway.go +++ b/x/application/types/message_undelegate_from_gateway.go @@ -9,9 +9,10 @@ const TypeMsgUndelegateFromGateway = "undelegate_from_gateway" var _ sdk.Msg = (*MsgUndelegateFromGateway)(nil) -func NewMsgUndelegateFromGateway(address string) *MsgUndelegateFromGateway { +func NewMsgUndelegateFromGateway(appAddress, gatewayAddress string) *MsgUndelegateFromGateway { return &MsgUndelegateFromGateway{ - Address: address, + AppAddress: appAddress, + GatewayAddress: gatewayAddress, } } @@ -24,7 +25,7 @@ func (msg *MsgUndelegateFromGateway) Type() string { } func (msg *MsgUndelegateFromGateway) GetSigners() []sdk.AccAddress { - address, err := sdk.AccAddressFromBech32(msg.Address) + address, err := sdk.AccAddressFromBech32(msg.AppAddress) if err != nil { panic(err) } @@ -37,9 +38,13 @@ func (msg *MsgUndelegateFromGateway) GetSignBytes() []byte { } func (msg *MsgUndelegateFromGateway) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(msg.Address) - if err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid address address (%s)", err) + // Validate the application address + if _, err := sdk.AccAddressFromBech32(msg.AppAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidAddress, "invalid application address %s; (%v)", msg.AppAddress, err) + } + // Validate the gateway address + if _, err := sdk.AccAddressFromBech32(msg.GatewayAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidGatewayAddress, "invalid gateway address %s; (%v)", msg.GatewayAddress, err) } return nil } diff --git a/x/application/types/message_undelegate_from_gateway_test.go b/x/application/types/message_undelegate_from_gateway_test.go index 1781a887a..72919eefb 100644 --- a/x/application/types/message_undelegate_from_gateway_test.go +++ b/x/application/types/message_undelegate_from_gateway_test.go @@ -3,9 +3,9 @@ package types import ( "testing" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/stretchr/testify/require" "pocket/testutil/sample" + + "github.com/stretchr/testify/require" ) func TestMsgUndelegateFromGateway_ValidateBasic(t *testing.T) { @@ -15,15 +15,31 @@ func TestMsgUndelegateFromGateway_ValidateBasic(t *testing.T) { err error }{ { - name: "invalid address", + name: "invalid app address - no gateway address", + msg: MsgUndelegateFromGateway{ + AppAddress: "invalid_address", + // GatewayAddress: sample.AccAddress(), + }, + err: ErrAppInvalidAddress, + }, { + name: "valid app address - no gateway address", + msg: MsgUndelegateFromGateway{ + AppAddress: sample.AccAddress(), + // GatewayAddress: sample.AccAddress(), + }, + err: ErrAppInvalidGatewayAddress, + }, { + name: "valid app address - invalid gateway address", msg: MsgUndelegateFromGateway{ - Address: "invalid_address", + AppAddress: sample.AccAddress(), + GatewayAddress: "invalid_address", }, - err: sdkerrors.ErrInvalidAddress, + err: ErrAppInvalidGatewayAddress, }, { name: "valid address", msg: MsgUndelegateFromGateway{ - Address: sample.AccAddress(), + AppAddress: sample.AccAddress(), + GatewayAddress: sample.AccAddress(), }, }, } From f7a527408350858c5735481b166be587b3d19807 Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Wed, 1 Nov 2023 14:27:35 -0700 Subject: [PATCH 18/28] Added first roadmap change --- docs/roadmap_changelog.md | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/roadmap_changelog.md diff --git a/docs/roadmap_changelog.md b/docs/roadmap_changelog.md new file mode 100644 index 000000000..e93fb5948 --- /dev/null +++ b/docs/roadmap_changelog.md @@ -0,0 +1,40 @@ +# Roadmap Changelog + +The purpose of this doc is to keep track of the changes made to the [Shannon roadmap](https://github.com/orgs/pokt-network/projects/144). + +- [Relevant links](#relevant-links) +- [11/01/2023](#11012023) + - [Changes](#changes) + - [After](#after) + - [Before](#before) + +## Relevant links + +- [Shannon Project](https://github.com/orgs/pokt-network/projects/144?query=is%3Aopen+sort%3Aupdated-desc) - GitHub dashboard +- [Shannon Roadmap](https://github.com/orgs/pokt-network/projects/144/views/4?query=is%3Aopen+sort%3Aupdated-desc) - GitHub Roadmap +- [PoktRoll Repo](https://github.com/pokt-network/poktroll) - Source Code +- [PoktRoll Issues](https://github.com/pokt-network/poktroll/issues) - GitHub Issues +- [PoktRoll Milestones](https://github.com/pokt-network/poktroll/milestones) - GitHub Milestones + +## 11/01/2023 + +### Changes + +1. We're adding a 1 week `E2E Relay` iteration to focus solely on finishing off `Foundation` & `Integration` related work needed to enable automating end-to-end relays. +2. We've delayed the `Govern` iteration to next year because: + - It is not a blocker for TestNetT + - here are still open-ended questions from PNF that need to be addressed first. +3. We've introduced `TECHDEBT` iterations to tackle `TODOs` left throughout the code. + - The first iteration will be focused on `TODO_BLOCKER` in the source code + - Details to other iterations will be ironed out closer to the iteration. +4. We have decided to have multiple `Test` iterations, each of which will be focused on testing different components. + - The first iteration will be focused on load testing relays to de-risk permissionless applications and verify the Claim & Proof lifecycle. + - Details to each iteration will be ironed out closer to the iteration. + +### After + +![Screenshot 2023-11-01 at 2 15 09 PM](https://github.com/pokt-network/poktroll/assets/1892194/e8ef99e6-aecc-433b-8a32-5fb42c05cb86) + +### Before + +![Screenshot 2023-11-01 at 11 05 21 AM](https://github.com/pokt-network/poktroll/assets/1892194/0826d4af-d0e1-4edc-a173-362425672c64) From 94e26650d8b4d20fefa3593b0664a8540aa6dc55 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Thu, 2 Nov 2023 07:12:37 +0100 Subject: [PATCH 19/28] [Testing] fix: flaky tests in observable & client pkgs (#124) * reactor: rename `event` to `eventsBz` for consistency * fix: increase delay in `Map` test to prevent flakiness * chore: improve test assertions * fix: remove redundant unsubscription * refactor: rename `publisher` var to `publishCh` * fix: close `publishCh` instead of relying on context cancellation to prevent test flakiness * fix: tune delay & timeout in observable test to prevent flakiness * chore: itest.sh prints total tests run when exiting early * chore: simplify * fixup: delay & timeout --- pkg/client/events_query/client.go | 4 +-- pkg/client/events_query/client_test.go | 28 +++++++++++---- pkg/observable/channel/map_test.go | 2 +- pkg/observable/channel/observable_test.go | 44 +++++++++-------------- pkg/observable/channel/observer_test.go | 5 ++- pkg/observable/channel/replay_test.go | 22 +++++++++--- tools/scripts/itest.sh | 2 +- 7 files changed, 63 insertions(+), 44 deletions(-) diff --git a/pkg/client/events_query/client.go b/pkg/client/events_query/client.go index f41e3e536..bd11e57fb 100644 --- a/pkg/client/events_query/client.go +++ b/pkg/client/events_query/client.go @@ -203,7 +203,7 @@ func (eqc *eventsQueryClient) goPublishEventsBz( // Read and handle messages from the websocket. This loop will exit when the // websocket connection is isClosed and/or returns an error. for { - event, err := conn.Receive() + eventBz, err := conn.Receive() if err != nil { // TODO_CONSIDERATION: should we close the publish channel here too? @@ -226,7 +226,7 @@ func (eqc *eventsQueryClient) goPublishEventsBz( } // Populate the []byte side (right) of the either and publish it. - eventsBzPublishCh <- either.Success(event) + eventsBzPublishCh <- either.Success(eventBz) } } diff --git a/pkg/client/events_query/client_test.go b/pkg/client/events_query/client_test.go index 4d3b41b30..a96516f0e 100644 --- a/pkg/client/events_query/client_test.go +++ b/pkg/client/events_query/client_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sync" "sync/atomic" "testing" "time" @@ -23,8 +24,6 @@ import ( ) func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { - t.Skip("TODO_BUG(@bryanchriswhite): See #120 for more details") - var ( readObserverEventsTimeout = time.Second queryCounter int @@ -60,7 +59,12 @@ func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { readEventCounter int // HandleEventsLimit is the total number of eventsBytesAndConns to send and // receive through the query client's eventsBytes for this subtest. - handleEventsLimit = 250 + handleEventsLimit = 250 + // delayFirstEvent runs once (per test case) to delay the first event + // published by the mocked connection's Receive method to give the test + // ample time to subscribe to the events bytes observable before it + // starts receiving events, otherwise they will be dropped. + delayFirstEvent sync.Once connClosed atomic.Bool queryCtx, cancelQuery = context.WithCancel(rootCtx) ) @@ -84,6 +88,8 @@ func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { // last message. connMock.EXPECT().Receive(). DoAndReturn(func() (any, error) { + delayFirstEvent.Do(func() { time.Sleep(50 * time.Millisecond) }) + // Simulate ErrConnClosed if connection is isClosed. if connClosed.Load() { return nil, eventsquery.ErrConnClosed @@ -132,11 +138,17 @@ func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { func TestEventsQueryClient_Subscribe_Close(t *testing.T) { var ( - readAllEventsTimeout = 50 * time.Millisecond + firstEventDelay = 50 * time.Millisecond + readAllEventsTimeout = 50*time.Millisecond + firstEventDelay handleEventsLimit = 10 readEventCounter int - connClosed atomic.Bool - ctx = context.Background() + // delayFirstEvent runs once (per test case) to delay the first event + // published by the mocked connection's Receive method to give the test + // ample time to subscribe to the events bytes observable before it + // starts receiving events, otherwise they will be dropped. + delayFirstEvent sync.Once + connClosed atomic.Bool + ctx = context.Background() ) connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) @@ -144,6 +156,8 @@ func TestEventsQueryClient_Subscribe_Close(t *testing.T) { Times(1) connMock.EXPECT().Receive(). DoAndReturn(func() (any, error) { + delayFirstEvent.Do(func() { time.Sleep(firstEventDelay) }) + if connClosed.Load() { return nil, eventsquery.ErrConnClosed } @@ -289,6 +303,8 @@ func behavesLikeEitherObserver[V any]( timeout time.Duration, onLimit func(), ) { + t.Helper() + var ( // eventsCounter is the number of events which have been received from the // eventsBytes since this function was called. diff --git a/pkg/observable/channel/map_test.go b/pkg/observable/channel/map_test.go index 37d7f5744..01014619f 100644 --- a/pkg/observable/channel/map_test.go +++ b/pkg/observable/channel/map_test.go @@ -69,7 +69,7 @@ func TestMap_Word_BytesToPalindrome(t *testing.T) { }() // wait a tick for the observer to receive the word - time.Sleep(time.Millisecond) + time.Sleep(10 * time.Millisecond) // ensure that the observer received the word require.Equal(t, int32(1), atomic.LoadInt32(&wordCounter)) diff --git a/pkg/observable/channel/observable_test.go b/pkg/observable/channel/observable_test.go index e94679630..918370cb0 100644 --- a/pkg/observable/channel/observable_test.go +++ b/pkg/observable/channel/observable_test.go @@ -17,8 +17,8 @@ import ( ) const ( - publishDelay = 100 * time.Microsecond - notifyTimeout = publishDelay * 20 + publishDelay = time.Millisecond + notifyTimeout = 50 * time.Millisecond cancelUnsubscribeDelay = publishDelay * 2 ) @@ -101,11 +101,11 @@ func TestChannelObservable_NotifyObservers(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - obsvbl, publisher := channel.NewObservable[int]( + obsvbl, publishCh := channel.NewObservable[int]( channel.WithPublisher(tt.publishCh), ) require.NotNil(t, obsvbl) - require.NotNil(t, publisher) + require.NotNil(t, publishCh) // construct 3 distinct observers, each with its own channel observers := make([]observable.Observer[int], 1) @@ -132,9 +132,8 @@ func TestChannelObservable_NotifyObservers(t *testing.T) { // onDone is called when the observer channel closes onDone := func(outputs []int) error { - if !assert.Equalf( - t, len(tt.expectedOutputs), - len(outputs), + if !assert.ElementsMatch( + t, tt.expectedOutputs, outputs, "obsvr addr: %p", obsvr, ) { return testerrors.ErrAsync @@ -148,24 +147,23 @@ func TestChannelObservable_NotifyObservers(t *testing.T) { } // notify with test input - publish := delayedPublishFactory(publisher, publishDelay) + publish := delayedPublishFactory(publishCh, publishDelay) for _, input := range tt.inputs { - inputPtr := new(int) - *inputPtr = input - // simulating IO delay in sequential message publishing publish(input) } - cancel() + + // Finished sending values, close publishCh to unsubscribe all observers + // and close all fan-out channels. + close(publishCh) // wait for obsvbl to be notified or timeout err := group.Wait() require.NoError(t, err) - // unsubscribing should close observer channel(s) + // closing publishCh should unsubscribe all observers, causing them + // to close their channels. for _, observer := range observers { - observer.Unsubscribe() - // must drain the channel first to ensure it is isClosed err := testchannel.DrainChannel(observer.Ch()) require.NoError(t, err) @@ -317,20 +315,10 @@ func TestChannelObservable_SequentialPublishAndUnsubscription(t *testing.T) { obsrvn.Lock() defer obsrvn.Unlock() - require.Equalf( - t, len(expectedNotifications[obsnIdx]), - len(obsrvn.Notifications), - "observation index: %d, expected: %+v, actual: %+v", - obsnIdx, expectedNotifications[obsnIdx], obsrvn.Notifications, + require.EqualValuesf( + t, expectedNotifications[obsnIdx], obsrvn.Notifications, + "observation index: %d", obsnIdx, ) - for notificationIdx, expected := range expectedNotifications[obsnIdx] { - require.Equalf( - t, expected, - (obsrvn.Notifications)[notificationIdx], - "allExpected: %+v, allActual: %+v", - expectedNotifications[obsnIdx], obsrvn.Notifications, - ) - } }) } } diff --git a/pkg/observable/channel/observer_test.go b/pkg/observable/channel/observer_test.go index ccda5c66c..fe7c865a9 100644 --- a/pkg/observable/channel/observer_test.go +++ b/pkg/observable/channel/observer_test.go @@ -70,13 +70,16 @@ func TestObserver_ConcurrentUnsubscribe(t *testing.T) { // publish a value obsvr.notify(idx) + + // Slow this loop to prevent bogging the test down. + time.Sleep(10 * time.Microsecond) } }() // send on done when the test cleans up t.Cleanup(func() { done <- struct{}{} }) // it should still be open after a bit of inactivity - time.Sleep(10 * time.Millisecond) + time.Sleep(time.Millisecond) require.Equal(t, false, obsvr.isClosed) obsvr.Unsubscribe() diff --git a/pkg/observable/channel/replay_test.go b/pkg/observable/channel/replay_test.go index a34fb0f92..b04857196 100644 --- a/pkg/observable/channel/replay_test.go +++ b/pkg/observable/channel/replay_test.go @@ -50,7 +50,9 @@ func TestReplayObservable(t *testing.T) { // send all values to the observable's publish channel for _, value := range values { + time.Sleep(10 * time.Microsecond) publishCh <- value + time.Sleep(10 * time.Microsecond) } // allow some time for values to be buffered by the replay observable @@ -59,27 +61,37 @@ func TestReplayObservable(t *testing.T) { // replay observer, should receive the last lastN values published prior to // subscribing followed by subsequently published values replayObserver := replayObsvbl.Subscribe(ctx) + + // Collect values from replayObserver. + var actualValues []int for _, expected := range expectedValues { select { case v := <-replayObserver.Ch(): - require.Equal(t, expected, v) + actualValues = append(actualValues, v) case <-time.After(1 * time.Second): t.Fatalf("Did not receive expected value %d in time", expected) } } - // second replay observer, should receive the same values as the first + require.EqualValues(t, expectedValues, actualValues) + + // Second replay observer, should receive the same values as the first // even though it subscribed after all values were published and the // values were already replayed by the first. replayObserver2 := replayObsvbl.Subscribe(ctx) + + // Collect values from replayObserver2. + var actualValues2 []int for _, expected := range expectedValues { select { case v := <-replayObserver2.Ch(): - require.Equal(t, expected, v) + actualValues2 = append(actualValues2, v) case <-time.After(1 * time.Second): t.Fatalf("Did not receive expected value %d in time", expected) } } + + require.EqualValues(t, expectedValues, actualValues) } func TestReplayObservable_Last_Full_ReplayBuffer(t *testing.T) { @@ -168,7 +180,7 @@ func TestReplayObservable_Last_Blocks_And_Times_Out(t *testing.T) { "Last should block until at lest 1 value has been published; actualValues: %v", actualValues, ) - case <-time.After(200 * time.Millisecond): + case <-time.After(10 * time.Millisecond): } // Publish some values (up to splitIdx). @@ -215,7 +227,7 @@ func TestReplayObservable_Last_Blocks_And_Times_Out(t *testing.T) { case actualValues := <-getLastValues(): require.Len(t, actualValues, lastN) require.ElementsMatch(t, values, actualValues) - case <-time.After(10 * time.Millisecond): + case <-time.After(50 * time.Millisecond): t.Fatal("timed out waiting for Last to return") } diff --git a/tools/scripts/itest.sh b/tools/scripts/itest.sh index 642cdbfe8..323db3d71 100755 --- a/tools/scripts/itest.sh +++ b/tools/scripts/itest.sh @@ -44,7 +44,7 @@ itest() { # If go test fails, exit the loop. if [[ $test_exit_status -ne 0 ]]; then - echo "go test failed on iteration $i. Exiting early." + echo "go test failed on iteration $i; exiting early. Total tests run: $total_tests_run" return 1 fi done From b8b75721a03c23f23df40900a139ce9bd3abeab8 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Thu, 2 Nov 2023 12:38:07 +0100 Subject: [PATCH 20/28] [Miner] feat: add `TxContext` (#118) * chore: add `TxContext` interface * feat: add `cosmosTxContext` implementation * test: add `TxContext` test helpers * chore: move godoc comment above `AsyncError` * fixup: goimports * chore: add godoc comments to tx context test helpers * chore: cleanup * chore: add godoc comments to common testclient helpers * chore: review feedback improvements (cherry picked from commit 78184e523e62450fe5fc7fe1d23906213e8c81bc) * chore: update comments * refactor: simplify tx context teset helpers (cherry picked from commit e2303a5b776830815457f12f5e371e916e3bee93) * chore: add godoc comments --- internal/testclient/common.go | 3 - internal/testclient/keyring.go | 28 +++ internal/testclient/localnet.go | 69 ++++++ internal/testclient/testtx/context.go | 340 ++++++++++++++++++++++++++ pkg/client/interface.go | 53 ++++ pkg/client/tx/context.go | 94 +++++++ pkg/either/types.go | 6 +- 7 files changed, 587 insertions(+), 6 deletions(-) delete mode 100644 internal/testclient/common.go create mode 100644 internal/testclient/keyring.go create mode 100644 internal/testclient/localnet.go create mode 100644 internal/testclient/testtx/context.go create mode 100644 pkg/client/tx/context.go diff --git a/internal/testclient/common.go b/internal/testclient/common.go deleted file mode 100644 index 41248916e..000000000 --- a/internal/testclient/common.go +++ /dev/null @@ -1,3 +0,0 @@ -package testclient - -const CometLocalWebsocketURL = "ws://localhost:36657/websocket" diff --git a/internal/testclient/keyring.go b/internal/testclient/keyring.go new file mode 100644 index 000000000..49a6b1c67 --- /dev/null +++ b/internal/testclient/keyring.go @@ -0,0 +1,28 @@ +package testclient + +import ( + cosmoshd "github.com/cosmos/cosmos-sdk/crypto/hd" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/stretchr/testify/require" + "testing" +) + +func NewKey( + t *testing.T, + name string, + keyring cosmoskeyring.Keyring, +) (key *cosmoskeyring.Record, mnemonic string) { + t.Helper() + + key, mnemonic, err := keyring.NewMnemonic( + name, + cosmoskeyring.English, + "m/44'/118'/0'/0/0", + cosmoskeyring.DefaultBIP39Passphrase, + cosmoshd.Secp256k1, + ) + require.NoError(t, err) + require.NotNil(t, key) + + return key, mnemonic +} diff --git a/internal/testclient/localnet.go b/internal/testclient/localnet.go new file mode 100644 index 000000000..a73352e12 --- /dev/null +++ b/internal/testclient/localnet.go @@ -0,0 +1,69 @@ +package testclient + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" + + "pocket/app" + "pocket/cmd/pocketd/cmd" +) + +// CometLocalWebsocketURL provides a default URL pointing to the localnet websocket endpoint. +const CometLocalWebsocketURL = "ws://localhost:36657/websocket" + +// EncodingConfig encapsulates encoding configurations for the Pocket application. +var EncodingConfig = app.MakeEncodingConfig() + +// init initializes the SDK configuration upon package import. +func init() { + cmd.InitSDKConfig() +} + +// NewLocalnetClientCtx creates a client context specifically tailored for localnet +// environments. The returned client context is initialized with encoding +// configurations, a default home directory, a default account retriever, and +// command flags. +// +// Parameters: +// - t: The testing.T instance used for the current test. +// - flagSet: The set of flags to be read for initializing the client context. +// +// Returns: +// - A pointer to a populated client.Context instance suitable for localnet usage. +func NewLocalnetClientCtx(t *testing.T, flagSet *pflag.FlagSet) *client.Context { + homedir := app.DefaultNodeHome + clientCtx := client.Context{}. + WithCodec(EncodingConfig.Marshaler). + WithTxConfig(EncodingConfig.TxConfig). + WithHomeDir(homedir). + WithAccountRetriever(authtypes.AccountRetriever{}). + WithInterfaceRegistry(EncodingConfig.InterfaceRegistry) + + clientCtx, err := client.ReadPersistentCommandFlags(clientCtx, flagSet) + require.NoError(t, err) + return &clientCtx +} + +// NewLocalnetFlagSet creates a set of predefined flags suitable for a localnet +// testing environment. +// +// Parameters: +// - t: The testing.T instance used for the current test. +// +// Returns: +// - A flag set populated with flags tailored for localnet environments. +func NewLocalnetFlagSet(t *testing.T) *pflag.FlagSet { + mockFlagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + mockFlagSet.String(flags.FlagNode, "tcp://127.0.0.1:36657", "use localnet poktrolld node") + mockFlagSet.String(flags.FlagHome, "", "use localnet poktrolld node") + mockFlagSet.String(flags.FlagKeyringBackend, "test", "use test keyring") + err := mockFlagSet.Parse([]string{}) + require.NoError(t, err) + + return mockFlagSet +} diff --git a/internal/testclient/testtx/context.go b/internal/testclient/testtx/context.go new file mode 100644 index 000000000..b87a97f08 --- /dev/null +++ b/internal/testclient/testtx/context.go @@ -0,0 +1,340 @@ +package testtx + +import ( + "context" + "fmt" + "testing" + + "cosmossdk.io/depinject" + abci "github.com/cometbft/cometbft/abci/types" + cometbytes "github.com/cometbft/cometbft/libs/bytes" + cometrpctypes "github.com/cometbft/cometbft/rpc/core/types" + comettypes "github.com/cometbft/cometbft/types" + cosmosclient "github.com/cosmos/cosmos-sdk/client" + cosmostx "github.com/cosmos/cosmos-sdk/client/tx" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "pocket/internal/mocks/mockclient" + "pocket/internal/testclient" + "pocket/pkg/client" + "pocket/pkg/client/tx" +) + +// TODO_IMPROVE: these mock constructor helpers could include parameters for the +// "times" (e.g. exact, min, max) values which are passed to their respective +// gomock.EXPECT() method calls (i.e. Times(), MinTimes(), MaxTimes()). +// When implementing such a pattern, be careful about making assumptions about +// correlations between these "times" values and the contexts in which the expected +// methods may be called. + +// NewOneTimeErrTxTimeoutTxContext creates a mock transaction context designed to simulate a specific +// timeout error scenario during transaction broadcasting. +// +// Parameters: +// - t: The testing.T instance for the current test. +// - keyring: The Cosmos SDK keyring containing the signer's cryptographic keys. +// - signingKeyName: The name of the key within the keyring to use for signing. +// - expectedTx: A pointer whose value will be set to the expected transaction +// bytes (in hexadecimal format). +// - expectedErrMsg: A pointer whose value will be set to the expected error +// message string. +// +// The function performs the following actions: +// 1. It retrieves the signer's cryptographic key from the provided keyring using the signingKeyName. +// 2. It computes the corresponding address of the signer's key. +// 3. It then formats an error message indicating that the fee payer's address does not exist. +// 4. It creates a base mock transaction context using NewBaseTxContext. +// 5. It sets up the mock behavior for the BroadcastTxSync method to return a specific preset response. +// 6. It also sets up the mock behavior for the QueryTx method to return a specific error response. +func NewOneTimeErrTxTimeoutTxContext( + t *testing.T, + keyring cosmoskeyring.Keyring, + signingKeyName string, + expectedErrMsg *string, +) *mockclient.MockTxContext { + t.Helper() + + signerKey, err := keyring.Key(signingKeyName) + require.NoError(t, err) + + signerAddr, err := signerKey.GetAddress() + require.NoError(t, err) + + *expectedErrMsg = fmt.Sprintf( + "fee payer address: %s does not exist: unknown address", + signerAddr.String(), + ) + + var expectedTx cometbytes.HexBytes + txCtxMock := NewBaseTxContext( + t, signingKeyName, + keyring, + &expectedTx, + ) + + // intercept #BroadcastTx() call to mock response and prevent actual broadcast + txCtxMock.EXPECT().BroadcastTx(gomock.Any()). + DoAndReturn( + func(txBytes []byte) (*cosmostypes.TxResponse, error) { + var expectedTxHash cometbytes.HexBytes = comettypes.Tx(txBytes).Hash() + return &cosmostypes.TxResponse{ + Height: 1, + TxHash: expectedTxHash.String(), + }, nil + }, + ).Times(1) + + txCtxMock.EXPECT().QueryTx( + gomock.AssignableToTypeOf(context.Background()), + gomock.AssignableToTypeOf([]byte{}), + gomock.AssignableToTypeOf(false), + ).DoAndReturn( + func( + ctx context.Context, + txHash []byte, + _ bool, + ) (*cometrpctypes.ResultTx, error) { + return &cometrpctypes.ResultTx{ + Hash: txHash, + Height: 1, + TxResult: abci.ResponseDeliverTx{ + Code: 1, + Log: *expectedErrMsg, + Codespace: "test_codespace", + }, + Tx: expectedTx.Bytes(), + }, nil + }, + ) + + return txCtxMock +} + +// NewOneTimeErrCheckTxTxContext creates a mock transaction context to simulate +// a specific error scenario during the ABCI check-tx phase (i.e., during initial +// validation before the transaction is included in the block). +// +// Parameters: +// - t: The testing.T instance for the current test. +// - keyring: The Cosmos SDK keyring containing the signer's cryptographic keys. +// - signingKeyName: The name of the key within the keyring to be used for signing. +// - expectedTx: A pointer whose value will be set to the expected transaction +// bytes (in hexadecimal format). +// - expectedErrMsg: A pointer whose value will be set to the expected error +// message string. +// +// The function operates as follows: +// 1. Retrieves the signer's cryptographic key from the provided keyring based on +// the signingKeyName. +// 2. Determines the corresponding address of the signer's key. +// 3. Composes an error message suggesting that the fee payer's address is unrecognized. +// 4. Creates a base mock transaction context using the NewBaseTxContext function. +// 5. Sets up the mock behavior for the BroadcastTxSync method to return a specific +// error response related to the check phase of the transaction. +func NewOneTimeErrCheckTxTxContext( + t *testing.T, + keyring cosmoskeyring.Keyring, + signingKeyName string, + expectedErrMsg *string, +) *mockclient.MockTxContext { + t.Helper() + + signerKey, err := keyring.Key(signingKeyName) + require.NoError(t, err) + + signerAddr, err := signerKey.GetAddress() + require.NoError(t, err) + + *expectedErrMsg = fmt.Sprintf( + "fee payer address: %s does not exist: unknown address", + signerAddr.String(), + ) + + var expectedTx cometbytes.HexBytes + txCtxMock := NewBaseTxContext( + t, signingKeyName, + keyring, + &expectedTx, + ) + + // intercept #BroadcastTx() call to mock response and prevent actual broadcast + txCtxMock.EXPECT().BroadcastTx(gomock.Any()). + DoAndReturn( + func(txBytes []byte) (*cosmostypes.TxResponse, error) { + var expectedTxHash cometbytes.HexBytes = comettypes.Tx(txBytes).Hash() + return &cosmostypes.TxResponse{ + Height: 1, + TxHash: expectedTxHash.String(), + RawLog: *expectedErrMsg, + Code: 1, + Codespace: "test_codespace", + }, nil + }, + ).Times(1) + + return txCtxMock +} + +// NewOneTimeTxTxContext creates a mock transaction context primed to respond with +// a single successful transaction response. This function facilitates testing by +// ensuring that the BroadcastTxSync method will return a specific, controlled response +// without actually broadcasting the transaction to the network. +// +// Parameters: +// - t: The testing.T instance used for the current test, typically passed from +// the calling test function. +// - keyring: The Cosmos SDK keyring containing the available cryptographic keys. +// - signingKeyName: The name of the key within the keyring used for transaction signing. +// - expectedTx: A pointer whose value will be set to the expected transaction +// bytes (in hexadecimal format). +// +// The function operates as follows: +// 1. Constructs a base mock transaction context using the NewBaseTxContext function. +// 2. Configures the mock behavior for the BroadcastTxSync method to return a pre-defined +// successful transaction response, ensuring that this behavior will only be triggered once. +func NewOneTimeTxTxContext( + t *testing.T, + keyring cosmoskeyring.Keyring, + signingKeyName string, + expectedTx *cometbytes.HexBytes, +) *mockclient.MockTxContext { + t.Helper() + + txCtxMock := NewBaseTxContext( + t, signingKeyName, + keyring, + expectedTx, + ) + + // intercept #BroadcastTx() call to mock response and prevent actual broadcast + txCtxMock.EXPECT().BroadcastTx(gomock.Any()). + DoAndReturn( + func(txBytes []byte) (*cosmostypes.TxResponse, error) { + var expectedTxHash cometbytes.HexBytes = comettypes.Tx(txBytes).Hash() + return &cosmostypes.TxResponse{ + Height: 1, + TxHash: expectedTxHash.String(), + }, nil + }, + ).Times(1) + + return txCtxMock +} + +// NewBaseTxContext establishes a foundational mock transaction context with +// predefined behaviors suitable for a broad range of testing scenarios. It ensures +// that when interactions like transaction building, signing, and encoding occur +// in the test environment, they produce predictable and controlled outcomes. +// +// Parameters: +// - t: The testing.T instance used for the current test, typically passed from +// the calling test function. +// - signingKeyName: The name of the key within the keyring to be used for +// transaction signing. +// - keyring: The Cosmos SDK keyring containing the available cryptographic keys. +// - expectedTx: A pointer whose value will be set to the expected transaction +// bytes (in hexadecimal format). +// - expectedErrMsg: A pointer whose value will be set to the expected error +// message string. +// +// The function works as follows: +// 1. Invokes the NewAnyTimesTxTxContext to create a base mock transaction context. +// 2. Sets the expectation that NewTxBuilder method will be called exactly once. +// 3. Configures the mock behavior for the SignTx method to utilize the context's +// signing logic. +// 4. Overrides the EncodeTx method's behavior to intercept the encoding operation, +// capture the encoded transaction bytes, compute the transaction hash, and populate +// the expectedTx and expectedTxHash parameters accordingly. +func NewBaseTxContext( + t *testing.T, + signingKeyName string, + keyring cosmoskeyring.Keyring, + expectedTx *cometbytes.HexBytes, +) *mockclient.MockTxContext { + t.Helper() + + txCtxMock, txCtx := NewAnyTimesTxTxContext(t, keyring) + txCtxMock.EXPECT().NewTxBuilder(). + DoAndReturn(txCtx.NewTxBuilder). + AnyTimes() + txCtxMock.EXPECT().SignTx( + gomock.Eq(signingKeyName), + gomock.AssignableToTypeOf(txCtx.NewTxBuilder()), + gomock.Eq(false), gomock.Eq(false), + ).DoAndReturn(txCtx.SignTx).AnyTimes() + txCtxMock.EXPECT().EncodeTx(gomock.Any()). + DoAndReturn( + func(txBuilder cosmosclient.TxBuilder) (_ []byte, err error) { + // intercept cosmosTxContext#EncodeTx to get the encoded tx cometbytes + *expectedTx, err = txCtx.EncodeTx(txBuilder) + require.NoError(t, err) + return expectedTx.Bytes(), nil + }, + ).AnyTimes() + + return txCtxMock +} + +// NewAnyTimesTxTxContext initializes a mock transaction context that's configured to allow +// arbitrary calls to certain predefined interactions, primarily concerning the retrieval +// of account numbers and sequences. +// +// Parameters: +// - t: The testing.T instance used for the current test, typically passed from the calling test function. +// - keyring: The Cosmos SDK keyring containing the available cryptographic keys. +// +// The function operates in the following manner: +// 1. Establishes a new gomock controller for setting up mock expectations and behaviors. +// 2. Prepares a set of flags suitable for localnet testing environments. +// 3. Sets up a mock behavior to intercept the GetAccountNumberSequence method calls, +// ensuring that whenever this method is invoked, it consistently returns an account number +// and sequence of 1, without making real queries to the underlying infrastructure. +// 4. Constructs a client context tailored for localnet testing with the provided keyring +// and the mocked account retriever. +// 5. Initializes a transaction factory from the client context and validates its integrity. +// 6. Injects the transaction factory and client context dependencies to create a new transaction context. +// 7. Creates a mock transaction context that always returns the provided keyring when the GetKeyring method is called. +// +// This setup aids tests by facilitating the creation of mock transaction contexts that have predictable +// and controlled outcomes for account number and sequence retrieval operations. +// +// Returns: +// - A mock transaction context suitable for setting additional expectations in tests. +// - A real transaction context initialized with the supplied dependencies. + +func NewAnyTimesTxTxContext( + t *testing.T, + keyring cosmoskeyring.Keyring, +) (*mockclient.MockTxContext, client.TxContext) { + t.Helper() + + var ( + ctrl = gomock.NewController(t) + flagSet = testclient.NewLocalnetFlagSet(t) + ) + + // intercept #GetAccountNumberSequence() call to mock response and prevent actual query + accountRetrieverMock := mockclient.NewMockAccountRetriever(ctrl) + accountRetrieverMock.EXPECT().GetAccountNumberSequence(gomock.Any(), gomock.Any()). + Return(uint64(1), uint64(1), nil). + AnyTimes() + + clientCtx := testclient.NewLocalnetClientCtx(t, flagSet). + WithKeyring(keyring). + WithAccountRetriever(accountRetrieverMock) + + txFactory, err := cosmostx.NewFactoryCLI(clientCtx, flagSet) + require.NoError(t, err) + require.NotEmpty(t, txFactory) + + txCtxDeps := depinject.Supply(txFactory, clientCtx) + txCtx, err := tx.NewTxContext(txCtxDeps) + require.NoError(t, err) + txCtxMock := mockclient.NewMockTxContext(ctrl) + txCtxMock.EXPECT().GetKeyring().Return(keyring).AnyTimes() + + return txCtxMock, txCtx +} diff --git a/pkg/client/interface.go b/pkg/client/interface.go index 1007656a5..c853813ce 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -1,14 +1,63 @@ //go:generate mockgen -destination=../../internal/mocks/mockclient/events_query_client_mock.go -package=mockclient . Dialer,Connection,EventsQueryClient +//go:generate mockgen -destination=../../internal/mocks/mockclient/tx_client_mock.go -package=mockclient . TxContext +//go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_tx_builder_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/client TxBuilder +//go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_keyring_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/crypto/keyring Keyring +//go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_client_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/client AccountRetriever package client import ( "context" + comettypes "github.com/cometbft/cometbft/rpc/core/types" + cosmosclient "github.com/cosmos/cosmos-sdk/client" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "pocket/pkg/either" "pocket/pkg/observable" ) +// TxContext provides an interface which consolidates the operational dependencies +// required to facilitate the sender side of the tx lifecycle: build, sign, encode, +// broadcast, query (optional). +// +// TODO_IMPROVE: Avoid depending on cosmos-sdk structs or interfaces; add Pocket +// interface types to substitute: +// - ResultTx +// - TxResponse +// - Keyring +// - TxBuilder +type TxContext interface { + // GetKeyring returns the associated key management mechanism for the tx context. + GetKeyring() cosmoskeyring.Keyring + + // NewTxBuilder creates and returns a new tx builder instance. + NewTxBuilder() cosmosclient.TxBuilder + + // SignTx signs a tx using the specified key name. It can operate in offline mode, + // and can overwrite any existing signatures based on the provided flags. + SignTx( + keyName string, + txBuilder cosmosclient.TxBuilder, + offline, overwriteSig bool, + ) error + + // EncodeTx takes a tx builder and encodes it, returning its byte representation. + EncodeTx(txBuilder cosmosclient.TxBuilder) ([]byte, error) + + // BroadcastTx broadcasts the given tx to the network. + BroadcastTx(txBytes []byte) (*cosmostypes.TxResponse, error) + + // QueryTx retrieves a tx status based on its hash and optionally provides + // proof of the tx. + QueryTx( + ctx context.Context, + txHash []byte, + prove bool, + ) (*comettypes.ResultTx, error) +} + // BlocksObservable is an observable which is notified with an either // value which contains either an error or the event message bytes. // TODO_HACK: The purpose of this type is to work around gomock's lack of @@ -16,6 +65,8 @@ import ( // alias (i.e. EventsBytesObservable = observable.Observable[either.Either[[]byte]]). type BlocksObservable observable.ReplayObservable[Block] +// BlockClient is an interface which provides notifications about newly committed +// blocks as well as direct access to the latest block via some blockchain API. type BlockClient interface { // Blocks returns an observable which emits newly committed blocks. CommittedBlocksSequence(context.Context) BlocksObservable @@ -26,6 +77,8 @@ type BlockClient interface { Close() } +// Block is an interface which abstracts the details of a block to its minimal +// necessary components. type Block interface { Height() int64 Hash() []byte diff --git a/pkg/client/tx/context.go b/pkg/client/tx/context.go new file mode 100644 index 000000000..8fd1ed877 --- /dev/null +++ b/pkg/client/tx/context.go @@ -0,0 +1,94 @@ +package tx + +import ( + "context" + + "cosmossdk.io/depinject" + cometrpctypes "github.com/cometbft/cometbft/rpc/core/types" + cosmosclient "github.com/cosmos/cosmos-sdk/client" + cosmostx "github.com/cosmos/cosmos-sdk/client/tx" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" + + "pocket/pkg/client" +) + +var _ client.TxContext = (*cosmosTxContext)(nil) + +// cosmosTxContext is an internal implementation of the client.TxContext interface. +// It provides methods related to transaction context within the Cosmos SDK. +type cosmosTxContext struct { + // Holds cosmos-sdk client context. + // (see: https://pkg.go.dev/github.com/cosmos/cosmos-sdk@v0.47.5/client#Context) + clientCtx cosmosclient.Context + // Holds the cosmos-sdk tx factory. + // (see: https://pkg.go.dev/github.com/cosmos/cosmos-sdk@v0.47.5/client/tx#Factory) + txFactory cosmostx.Factory +} + +// NewTxContext initializes a new cosmosTxContext with the given dependencies. +// It uses depinject to populate its members and returns a client.TxContext +// interface type. +func NewTxContext(deps depinject.Config) (client.TxContext, error) { + txCtx := cosmosTxContext{} + + if err := depinject.Inject( + deps, + &txCtx.clientCtx, + &txCtx.txFactory, + ); err != nil { + return nil, err + } + + return txCtx, nil +} + +// GetKeyring returns the cosmos-sdk client Keyring associated with the transaction factory. +func (txCtx cosmosTxContext) GetKeyring() cosmoskeyring.Keyring { + return txCtx.txFactory.Keybase() +} + +// SignTx signs the provided transaction using the given key name. It can operate in offline mode +// and can optionally overwrite any existing signatures. +// It is a proxy to the cosmos-sdk auth module client SignTx function. +// (see: https://pkg.go.dev/github.com/cosmos/cosmos-sdk@v0.47.5/x/auth/client) +func (txCtx cosmosTxContext) SignTx( + signingKeyName string, + txBuilder cosmosclient.TxBuilder, + offline, overwriteSig bool, +) error { + return authclient.SignTx( + txCtx.txFactory, + txCtx.clientCtx, + signingKeyName, + txBuilder, + offline, overwriteSig, + ) +} + +// NewTxBuilder returns a new tx builder instance using the cosmos-sdk client tx config. +func (txCtx cosmosTxContext) NewTxBuilder() cosmosclient.TxBuilder { + return txCtx.clientCtx.TxConfig.NewTxBuilder() +} + +// EncodeTx encodes the provided tx and returns its bytes representation. +func (txCtx cosmosTxContext) EncodeTx(txBuilder cosmosclient.TxBuilder) ([]byte, error) { + return txCtx.clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) +} + +// BroadcastTx broadcasts the given tx to the network, blocking until the check-tx +// ABCI operation completes and returns a TxResponse of the tx status at that point in time. +func (txCtx cosmosTxContext) BroadcastTx(txBytes []byte) (*cosmostypes.TxResponse, error) { + return txCtx.clientCtx.BroadcastTxSync(txBytes) +} + +// QueryTx queries the transaction based on its hash and optionally provides proof +// of the transaction. It returns the tx query result. +func (txCtx cosmosTxContext) QueryTx( + ctx context.Context, + txHash []byte, + prove bool, +) (*cometrpctypes.ResultTx, error) { + return txCtx.clientCtx.Client.Tx(ctx, txHash, prove) +} diff --git a/pkg/either/types.go b/pkg/either/types.go index af171b6c1..ae7092479 100644 --- a/pkg/either/types.go +++ b/pkg/either/types.go @@ -1,9 +1,9 @@ package either -// AsyncError represents a value which could either be a synchronous error or -// an asynchronous error (sent through a channel). It wraps the more generic -// `Either` type specific for error channels. type ( + // AsyncError represents a value which could either be a synchronous error or + // an asynchronous error (sent through a channel). It wraps the more generic + // `Either` type specific for error channels. AsyncError Either[chan error] Bytes = Either[[]byte] ) From 01165479294562afaefe356b580cb1e11e3bd46d Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Thu, 2 Nov 2023 14:31:11 -0700 Subject: [PATCH 21/28] [Code Health] Support `godoc` by replacing the `pocket `module name with `github.com/pokt-network/poktroll` (#128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I started trying to explore how to get `godoc` documentation for our library and went down a rabbithole... In short, in order for it to be available at https://pkg.go.dev/github.com/pokt-network/poktroll, similar to https://pkg.go.dev/github.com/pokt-network/pocket, we need to replace the `pocket` module name with `github.com/pokt-network/poktroll`, which affects everything. ![Screenshot 2023-11-01 at 5 09 54 PM](https://github.com/pokt-network/poktroll/assets/1892194/ce946017-0745-434a-8c43-940f663124b4) --------- Co-authored-by: Bryan White --- Makefile | 37 +- app/app.go | 40 +- app/encoding.go | 2 +- app/simulation_test.go | 2 +- cmd/pocketd/cmd/config.go | 2 +- cmd/pocketd/cmd/root.go | 4 +- cmd/pocketd/main.go | 4 +- config.yml | 2 + docs/static/openapi.yml | 1350 ++--------------- go.mod | 2 +- internal/mocks/.gitkeep | 0 internal/mocks/mockclient/mocks.go | 10 + internal/mocks/mocks.go | 10 + internal/testchannel/drain.go | 2 +- internal/testclient/localnet.go | 4 +- internal/testclient/testblock/client.go | 8 +- internal/testclient/testeventsquery/client.go | 6 +- .../testclient/testeventsquery/connection.go | 4 +- internal/testclient/testtx/context.go | 8 +- pkg/client/block/block.go | 2 +- pkg/client/block/client.go | 10 +- pkg/client/block/client_integration_test.go | 4 +- pkg/client/block/client_test.go | 10 +- pkg/client/events_query/client.go | 10 +- .../events_query/client_integration_test.go | 2 +- pkg/client/events_query/client_test.go | 16 +- pkg/client/events_query/options.go | 2 +- .../events_query/websocket/connection.go | 2 +- pkg/client/events_query/websocket/dialer.go | 2 +- pkg/client/interface.go | 4 +- pkg/client/tx/context.go | 2 +- pkg/observable/channel/map.go | 2 +- pkg/observable/channel/map_test.go | 2 +- pkg/observable/channel/observable.go | 3 +- pkg/observable/channel/observable_test.go | 8 +- pkg/observable/channel/observation_test.go | 2 +- pkg/observable/channel/observer.go | 2 +- pkg/observable/channel/observer_manager.go | 2 +- pkg/observable/channel/observer_test.go | 2 +- pkg/observable/channel/replay.go | 2 +- pkg/observable/channel/replay_test.go | 4 +- pkg/relayer/proxy/interface.go | 6 +- pkg/relayer/proxy/jsonrpc.go | 4 +- pkg/relayer/proxy/proxy.go | 12 +- pkg/relayer/proxy/server_builder.go | 4 +- pkg/retry/retry_test.go | 2 +- proto/pocket/application/application.proto | 2 +- proto/pocket/application/genesis.proto | 2 +- proto/pocket/application/params.proto | 2 +- proto/pocket/application/query.proto | 14 +- proto/pocket/application/tx.proto | 2 +- proto/pocket/gateway/gateway.proto | 2 +- proto/pocket/gateway/genesis.proto | 2 +- proto/pocket/gateway/params.proto | 4 +- proto/pocket/gateway/query.proto | 14 +- proto/pocket/gateway/tx.proto | 2 +- proto/pocket/pocket/genesis.proto | 2 +- proto/pocket/pocket/params.proto | 4 +- proto/pocket/pocket/query.proto | 2 +- proto/pocket/pocket/tx.proto | 2 +- proto/pocket/service/genesis.proto | 2 +- proto/pocket/service/params.proto | 4 +- proto/pocket/service/query.proto | 2 +- proto/pocket/service/relay.proto | 2 +- proto/pocket/service/tx.proto | 2 +- proto/pocket/session/genesis.proto | 2 +- proto/pocket/session/params.proto | 4 +- proto/pocket/session/query.proto | 2 +- proto/pocket/session/session.proto | 2 +- proto/pocket/session/tx.proto | 2 +- proto/pocket/shared/service.proto | 2 +- proto/pocket/shared/supplier.proto | 2 +- proto/pocket/supplier/genesis.proto | 2 +- proto/pocket/supplier/params.proto | 4 +- proto/pocket/supplier/query.proto | 14 +- proto/pocket/supplier/tx.proto | 2 +- testutil/application/mocks/.gitkeep | 0 testutil/application/mocks/mocks.go | 10 + testutil/gateway/mocks/.gitkeep | 0 testutil/gateway/mocks/mocks.go | 10 + testutil/keeper/application.go | 8 +- testutil/keeper/gateway.go | 9 +- testutil/keeper/pocket.go | 5 +- testutil/keeper/service.go | 5 +- testutil/keeper/session.go | 12 +- testutil/keeper/supplier.go | 7 +- testutil/network/network.go | 12 +- testutil/session/mocks/mocks.go | 4 + testutil/supplier/mocks/.gitkeep | 0 testutil/supplier/mocks/mocks.go | 10 + x/application/client/cli/helpers_test.go | 6 +- x/application/client/cli/query.go | 2 +- x/application/client/cli/query_application.go | 2 +- .../client/cli/query_application_test.go | 6 +- x/application/client/cli/query_params.go | 2 +- x/application/client/cli/tx.go | 2 +- .../client/cli/tx_delegate_to_gateway.go | 2 +- .../client/cli/tx_delegate_to_gateway_test.go | 6 +- .../client/cli/tx_stake_application.go | 2 +- .../client/cli/tx_stake_application_test.go | 6 +- .../client/cli/tx_undelegate_from_gateway.go | 2 +- .../cli/tx_undelegate_from_gateway_test.go | 6 +- .../client/cli/tx_unstake_application.go | 2 +- .../client/cli/tx_unstake_application_test.go | 6 +- x/application/genesis.go | 4 +- x/application/genesis_test.go | 12 +- x/application/keeper/application.go | 2 +- x/application/keeper/application_test.go | 14 +- x/application/keeper/keeper.go | 2 +- x/application/keeper/msg_server.go | 2 +- .../keeper/msg_server_delegate_to_gateway.go | 2 +- .../msg_server_delegate_to_gateway_test.go | 10 +- .../keeper/msg_server_stake_application.go | 2 +- .../msg_server_stake_application_test.go | 10 +- x/application/keeper/msg_server_test.go | 6 +- .../msg_server_undelegate_from_gateway.go | 2 +- ...msg_server_undelegate_from_gateway_test.go | 10 +- .../keeper/msg_server_unstake_application.go | 2 +- .../msg_server_unstake_application_test.go | 10 +- x/application/keeper/params.go | 3 +- x/application/keeper/params_test.go | 5 +- x/application/keeper/query.go | 2 +- x/application/keeper/query_application.go | 2 +- .../keeper/query_application_test.go | 6 +- x/application/keeper/query_params.go | 3 +- x/application/keeper/query_params_test.go | 5 +- x/application/module.go | 8 +- x/application/module_simulation.go | 6 +- .../simulation/delegate_to_gateway.go | 4 +- x/application/simulation/stake_application.go | 4 +- .../simulation/undelegate_from_gateway.go | 4 +- .../simulation/unstake_application.go | 4 +- x/application/types/expected_keepers.go | 2 +- x/application/types/genesis.go | 2 +- x/application/types/genesis_test.go | 6 +- .../types/message_delegate_to_gateway_test.go | 2 +- .../types/message_stake_application.go | 4 +- .../types/message_stake_application_test.go | 4 +- .../message_undelegate_from_gateway_test.go | 2 +- .../types/message_unstake_application_test.go | 2 +- x/gateway/client/cli/helpers_test.go | 4 +- x/gateway/client/cli/query.go | 2 +- x/gateway/client/cli/query_gateway.go | 2 +- x/gateway/client/cli/query_gateway_test.go | 6 +- x/gateway/client/cli/query_params.go | 2 +- x/gateway/client/cli/tx.go | 2 +- x/gateway/client/cli/tx_stake_gateway.go | 2 +- x/gateway/client/cli/tx_stake_gateway_test.go | 6 +- x/gateway/client/cli/tx_unstake_gateway.go | 2 +- .../client/cli/tx_unstake_gateway_test.go | 6 +- x/gateway/genesis.go | 4 +- x/gateway/genesis_test.go | 9 +- x/gateway/keeper/gateway.go | 3 +- x/gateway/keeper/gateway_test.go | 10 +- x/gateway/keeper/keeper.go | 2 +- x/gateway/keeper/msg_server.go | 2 +- x/gateway/keeper/msg_server_stake_gateway.go | 2 +- .../keeper/msg_server_stake_gateway_test.go | 8 +- x/gateway/keeper/msg_server_test.go | 7 +- .../keeper/msg_server_unstake_gateway.go | 2 +- .../keeper/msg_server_unstake_gateway_test.go | 8 +- x/gateway/keeper/params.go | 3 +- x/gateway/keeper/params_test.go | 5 +- x/gateway/keeper/query.go | 2 +- x/gateway/keeper/query_gateway.go | 3 +- x/gateway/keeper/query_gateway_test.go | 6 +- x/gateway/keeper/query_params.go | 3 +- x/gateway/keeper/query_params_test.go | 5 +- x/gateway/module.go | 8 +- x/gateway/module_simulation.go | 6 +- x/gateway/simulation/stake_gateway.go | 4 +- x/gateway/simulation/unstake_gateway.go | 5 +- x/gateway/types/genesis_test.go | 4 +- x/gateway/types/message_stake_gateway_test.go | 2 +- .../types/message_unstake_gateway_test.go | 2 +- x/pocket/client/cli/query.go | 2 +- x/pocket/client/cli/query_params.go | 2 +- x/pocket/client/cli/tx.go | 2 +- x/pocket/genesis.go | 5 +- x/pocket/genesis_test.go | 9 +- x/pocket/keeper/keeper.go | 2 +- x/pocket/keeper/msg_server.go | 2 +- x/pocket/keeper/msg_server_test.go | 7 +- x/pocket/keeper/params.go | 3 +- x/pocket/keeper/params_test.go | 5 +- x/pocket/keeper/query.go | 2 +- x/pocket/keeper/query_params.go | 3 +- x/pocket/keeper/query_params_test.go | 5 +- x/pocket/module.go | 8 +- x/pocket/module_simulation.go | 7 +- x/pocket/types/genesis_test.go | 3 +- x/service/client/cli/query.go | 2 +- x/service/client/cli/query_params.go | 2 +- x/service/client/cli/tx.go | 2 +- x/service/genesis.go | 5 +- x/service/genesis_test.go | 9 +- x/service/keeper/keeper.go | 2 +- x/service/keeper/msg_server.go | 2 +- x/service/keeper/msg_server_test.go | 7 +- x/service/keeper/params.go | 3 +- x/service/keeper/params_test.go | 5 +- x/service/keeper/query.go | 2 +- x/service/keeper/query_params.go | 3 +- x/service/keeper/query_params_test.go | 5 +- x/service/module.go | 8 +- x/service/module_simulation.go | 7 +- x/service/types/genesis_test.go | 3 +- x/session/client/cli/query.go | 2 +- x/session/client/cli/query_get_session.go | 2 +- x/session/client/cli/query_params.go | 2 +- x/session/client/cli/tx.go | 2 +- x/session/genesis.go | 5 +- x/session/genesis_test.go | 9 +- x/session/keeper/keeper.go | 2 +- x/session/keeper/msg_server.go | 2 +- x/session/keeper/msg_server_test.go | 6 +- x/session/keeper/params.go | 3 +- x/session/keeper/params_test.go | 5 +- x/session/keeper/query.go | 2 +- x/session/keeper/query_get_session.go | 2 +- x/session/keeper/query_get_session_test.go | 8 +- x/session/keeper/query_params.go | 3 +- x/session/keeper/query_params_test.go | 4 +- x/session/keeper/session_hydrator.go | 4 +- x/session/keeper/session_hydrator_test.go | 8 +- x/session/module.go | 6 +- x/session/module_simulation.go | 7 +- x/session/types/expected_keepers.go | 4 +- x/session/types/genesis_test.go | 3 +- x/shared/helpers/service_configs.go | 2 +- x/supplier/client/cli/helpers_test.go | 8 +- x/supplier/client/cli/query.go | 2 +- x/supplier/client/cli/query_params.go | 2 +- x/supplier/client/cli/query_supplier.go | 2 +- x/supplier/client/cli/query_supplier_test.go | 8 +- x/supplier/client/cli/tx.go | 2 +- x/supplier/client/cli/tx_create_claim.go | 5 +- x/supplier/client/cli/tx_stake_supplier.go | 4 +- .../client/cli/tx_stake_supplier_test.go | 6 +- x/supplier/client/cli/tx_submit_proof.go | 5 +- x/supplier/client/cli/tx_unstake_supplier.go | 2 +- .../client/cli/tx_unstake_supplier_test.go | 6 +- x/supplier/genesis.go | 4 +- x/supplier/genesis_test.go | 12 +- x/supplier/keeper/keeper.go | 2 +- x/supplier/keeper/msg_server.go | 2 +- x/supplier/keeper/msg_server_create_claim.go | 2 +- .../keeper/msg_server_stake_supplier.go | 4 +- .../keeper/msg_server_stake_supplier_test.go | 10 +- x/supplier/keeper/msg_server_submit_proof.go | 2 +- x/supplier/keeper/msg_server_test.go | 7 +- .../keeper/msg_server_unstake_supplier.go | 2 +- .../msg_server_unstake_supplier_test.go | 10 +- x/supplier/keeper/params.go | 3 +- x/supplier/keeper/params_test.go | 5 +- x/supplier/keeper/query.go | 2 +- x/supplier/keeper/query_params.go | 3 +- x/supplier/keeper/query_params_test.go | 5 +- x/supplier/keeper/query_supplier.go | 4 +- x/supplier/keeper/query_supplier_test.go | 6 +- x/supplier/keeper/supplier.go | 4 +- x/supplier/keeper/supplier_test.go | 14 +- x/supplier/module.go | 8 +- x/supplier/module_simulation.go | 6 +- x/supplier/simulation/create_claim.go | 5 +- x/supplier/simulation/stake_supplier.go | 4 +- x/supplier/simulation/submit_proof.go | 5 +- x/supplier/simulation/unstake_supplier.go | 5 +- x/supplier/types/genesis.go | 4 +- x/supplier/types/genesis_test.go | 6 +- x/supplier/types/message_create_claim.go | 2 +- x/supplier/types/message_create_claim_test.go | 2 +- x/supplier/types/message_stake_supplier.go | 4 +- .../types/message_stake_supplier_test.go | 4 +- x/supplier/types/message_submit_proof.go | 2 +- x/supplier/types/message_submit_proof_test.go | 3 +- .../types/message_unstake_supplier_test.go | 2 +- 277 files changed, 878 insertions(+), 1755 deletions(-) delete mode 100644 internal/mocks/.gitkeep create mode 100644 internal/mocks/mockclient/mocks.go create mode 100644 internal/mocks/mocks.go delete mode 100644 testutil/application/mocks/.gitkeep create mode 100644 testutil/application/mocks/mocks.go delete mode 100644 testutil/gateway/mocks/.gitkeep create mode 100644 testutil/gateway/mocks/mocks.go delete mode 100644 testutil/supplier/mocks/.gitkeep create mode 100644 testutil/supplier/mocks/mocks.go diff --git a/Makefile b/Makefile index 3b68fff2a..c34357b58 100644 --- a/Makefile +++ b/Makefile @@ -35,9 +35,9 @@ help: ## Prints all the targets in all the Makefiles ### Checks ### ############## -.PHONY: go_version_check +.PHONY: check_go_version # Internal helper target - check go version -go_version_check: +check_go_version: @# Extract the version number from the `go version` command. @GO_VERSION=$$(go version | cut -d " " -f 3 | cut -c 3-) && \ MAJOR_VERSION=$$(echo $$GO_VERSION | cut -d "." -f 1) && \ @@ -48,9 +48,9 @@ go_version_check: exit 1; \ fi -.PHONY: docker_check +.PHONY: check_docker # Internal helper target - check if docker is installed -docker_check: +check_docker: { \ if ( ! ( command -v docker >/dev/null && (docker compose version >/dev/null || command -v docker-compose >/dev/null) )); then \ echo "Seems like you don't have Docker or docker-compose installed. Make sure you review build/localnet/README.md and docs/development/README.md before continuing"; \ @@ -58,6 +58,17 @@ docker_check: fi; \ } +.PHONY: check_godoc +# Internal helper target - check if godoc is installed +check_godoc: + { \ + if ( ! ( command -v godoc >/dev/null )); then \ + echo "Seems like you don't have godoc installed. Make sure you install it via 'go install golang.org/x/tools/cmd/godoc@latest' before continuing"; \ + exit 1; \ + fi; \ + } + + .PHONY: warn_destructive warn_destructive: ## Print WARNING to the user @echo "This is a destructive action that will affect docker resources outside the scope of this repo!" @@ -76,7 +87,7 @@ proto_regen: ## Delete existing protobuf artifacts and regenerate them ####################### .PHONY: docker_wipe -docker_wipe: docker_check warn_destructive prompt_user ## [WARNING] Remove all the docker containers, images and volumes. +docker_wipe: check_docker warn_destructive prompt_user ## [WARNING] Remove all the docker containers, images and volumes. docker ps -a -q | xargs -r -I {} docker stop {} docker ps -a -q | xargs -r -I {} docker rm {} docker images -q | xargs -r -I {} docker rmi {} @@ -113,22 +124,21 @@ test_e2e: ## Run all E2E tests export POCKET_NODE=$(POCKET_NODE) POCKETD_HOME=../../$(POCKETD_HOME) && go test -v ./e2e/tests/... -tags=e2e .PHONY: go_test -go_test: go_version_check ## Run all go tests +go_test: check_go_version ## Run all go tests go test -v -race -tags test ./... .PHONY: go_test_integration -go_test_integration: go_version_check ## Run all go tests, including integration +go_test_integration: check_go_version ## Run all go tests, including integration go test -v -race -tags test,integration ./... .PHONY: itest -itest: go_version_check ## Run tests iteratively (see usage for more) +itest: check_go_version ## Run tests iteratively (see usage for more) ./tools/scripts/itest.sh $(filter-out $@,$(MAKECMDGOALS)) # catch-all target for itest %: # no-op @: - .PHONY: go_mockgen go_mockgen: ## Use `mockgen` to generate mocks used for testing purposes of all the modules. find . -name "*_mock.go" | xargs --no-run-if-empty rm @@ -391,3 +401,12 @@ acc_balance_total_supply: ## Query the total supply of the network .PHONY: ignite_acc_list ignite_acc_list: ## List all the accounts in LocalNet ignite account list --keyring-dir=$(POCKETD_HOME) --keyring-backend test --address-prefix $(POCKET_ADDR_PREFIX) + +##################### +### Documentation ### +##################### +.PHONY: go_docs +go_docs: check_godoc ## Generate documentation for the project + echo "Visit http://localhost:6060/pkg/pocket/" + godoc -http=:6060 + diff --git a/app/app.go b/app/app.go index 05cad6ae7..c6a48ee69 100644 --- a/app/app.go +++ b/app/app.go @@ -112,26 +112,26 @@ import ( ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" "github.com/spf13/cast" - appparams "pocket/app/params" - "pocket/docs" - applicationmodule "pocket/x/application" - applicationmodulekeeper "pocket/x/application/keeper" - applicationmoduletypes "pocket/x/application/types" - gatewaymodule "pocket/x/gateway" - gatewaymodulekeeper "pocket/x/gateway/keeper" - gatewaymoduletypes "pocket/x/gateway/types" - pocketmodule "pocket/x/pocket" - pocketmodulekeeper "pocket/x/pocket/keeper" - pocketmoduletypes "pocket/x/pocket/types" - servicemodule "pocket/x/service" - servicemodulekeeper "pocket/x/service/keeper" - servicemoduletypes "pocket/x/service/types" - sessionmodule "pocket/x/session" - sessionmodulekeeper "pocket/x/session/keeper" - sessionmoduletypes "pocket/x/session/types" - suppliermodule "pocket/x/supplier" - suppliermodulekeeper "pocket/x/supplier/keeper" - suppliermoduletypes "pocket/x/supplier/types" + appparams "github.com/pokt-network/poktroll/app/params" + "github.com/pokt-network/poktroll/docs" + applicationmodule "github.com/pokt-network/poktroll/x/application" + applicationmodulekeeper "github.com/pokt-network/poktroll/x/application/keeper" + applicationmoduletypes "github.com/pokt-network/poktroll/x/application/types" + gatewaymodule "github.com/pokt-network/poktroll/x/gateway" + gatewaymodulekeeper "github.com/pokt-network/poktroll/x/gateway/keeper" + gatewaymoduletypes "github.com/pokt-network/poktroll/x/gateway/types" + pocketmodule "github.com/pokt-network/poktroll/x/pocket" + pocketmodulekeeper "github.com/pokt-network/poktroll/x/pocket/keeper" + pocketmoduletypes "github.com/pokt-network/poktroll/x/pocket/types" + servicemodule "github.com/pokt-network/poktroll/x/service" + servicemodulekeeper "github.com/pokt-network/poktroll/x/service/keeper" + servicemoduletypes "github.com/pokt-network/poktroll/x/service/types" + sessionmodule "github.com/pokt-network/poktroll/x/session" + sessionmodulekeeper "github.com/pokt-network/poktroll/x/session/keeper" + sessionmoduletypes "github.com/pokt-network/poktroll/x/session/types" + suppliermodule "github.com/pokt-network/poktroll/x/supplier" + suppliermodulekeeper "github.com/pokt-network/poktroll/x/supplier/keeper" + suppliermoduletypes "github.com/pokt-network/poktroll/x/supplier/types" ) const ( diff --git a/app/encoding.go b/app/encoding.go index e56329d30..3e32bee43 100644 --- a/app/encoding.go +++ b/app/encoding.go @@ -6,7 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/std" "github.com/cosmos/cosmos-sdk/x/auth/tx" - "pocket/app/params" + "github.com/pokt-network/poktroll/app/params" ) // makeEncodingConfig creates an EncodingConfig for an amino based test configuration. diff --git a/app/simulation_test.go b/app/simulation_test.go index 62df08755..ee0ed33ad 100644 --- a/app/simulation_test.go +++ b/app/simulation_test.go @@ -36,7 +36,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/stretchr/testify/require" - "pocket/app" + "github.com/pokt-network/poktroll/app" ) type storeKeysPrefixes struct { diff --git a/cmd/pocketd/cmd/config.go b/cmd/pocketd/cmd/config.go index af1cbb064..1182b1159 100644 --- a/cmd/pocketd/cmd/config.go +++ b/cmd/pocketd/cmd/config.go @@ -3,7 +3,7 @@ package cmd import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/app" + "github.com/pokt-network/poktroll/app" ) // InitSDKConfig initializes the SDK's config with the appropriate parameters diff --git a/cmd/pocketd/cmd/root.go b/cmd/pocketd/cmd/root.go index 59c45dcce..5b1fb3276 100644 --- a/cmd/pocketd/cmd/root.go +++ b/cmd/pocketd/cmd/root.go @@ -41,8 +41,8 @@ import ( // this line is used by starport scaffolding # root/moduleImport - "pocket/app" - appparams "pocket/app/params" + "github.com/pokt-network/poktroll/app" + appparams "github.com/pokt-network/poktroll/app/params" ) // NewRootCmd creates a new root command for a Cosmos SDK application diff --git a/cmd/pocketd/main.go b/cmd/pocketd/main.go index ef717a1d5..25c7ebcef 100644 --- a/cmd/pocketd/main.go +++ b/cmd/pocketd/main.go @@ -6,8 +6,8 @@ import ( "github.com/cosmos/cosmos-sdk/server" svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" - "pocket/app" - "pocket/cmd/pocketd/cmd" + "github.com/pokt-network/poktroll/app" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" ) func main() { diff --git a/config.yml b/config.yml index 609860430..8b235fc3f 100644 --- a/config.yml +++ b/config.yml @@ -1,4 +1,6 @@ version: 1 +build: + main: cmd/pocketd accounts: - name: faucet mnemonic: "baby advance work soap slow exclude blur humble lucky rough teach wide chuckle captain rack laundry butter main very cannon donate armor dress follow" diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index 0e4c63907..687106365 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -47201,189 +47201,13 @@ paths: ApplicationServiceConfig holds the service configuration the application stakes for title: The ID of the service this session is servicing - delegatee_gateway_pub_keys: + delegatee_gateway_addresses: type: array items: - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the - type of the serialized - - protocol buffer message. This string must - contain at least - - one "/" character. The last segment of the URL's - path must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name - should be in a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the - binary all types that they - - expect it to use in the context of Any. However, - for URLs which use the - - scheme `http`, `https`, or no scheme, one can - optionally set up a type - - server that maps type URLs to message - definitions as follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup - results based on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently - available in the official - - protobuf release, and it is not used for type - URLs beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty - scheme) might be - - used with implementation specific semantics. - additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol - buffer message along with a - - URL that describes the type of the serialized - message. - - - Protobuf library provides support to pack/unpack Any - values in the form - - of utility functions or additional generated methods - of the Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will - by default use - - 'type.googleapis.com/full.type.name' as the type URL - and the unpack - - methods only use the fully qualified type name after - the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" - will yield type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the - regular - - representation of the deserialized, embedded - message, with an - - additional field `@type` which contains the type - URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a - custom JSON - - representation, that representation will be embedded - adding a field - - `value` which holds the custom JSON in addition to - the `@type` - - field. Example (for message - [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: >- - The corresponding cosmos.crypto.PubKey (interface) - encoded into an cosmos.codec.Any for use in the non - nullable slice of delegatee Gateways the application - is delegated to. + type: string + title: >- + The Bech32 encoded addresses for all delegatee + Gateways, in a non-nullable slice suppliers: type: array items: @@ -47530,174 +47354,7 @@ paths: properties: '@type': type: string - description: >- - A URL/resource name that uniquely identifies the type of - the serialized - - protocol buffer message. This string must contain at - least - - one "/" character. The last segment of the URL's path - must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in - a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary - all types that they - - expect it to use in the context of Any. However, for - URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally - set up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based - on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in - the official - - protobuf release, and it is not used for type URLs - beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer - message along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values - in the form - - of utility functions or additional generated methods of the - Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by - default use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the - last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield - type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with - an - - additional field `@type` which contains the type URL. - Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom - JSON - - representation, that representation will be embedded adding - a field - - `value` which holds the custom JSON in addition to the - `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } parameters: - name: application_address description: >- @@ -47708,236 +47365,69 @@ paths: type: string - name: service_id.id description: >- - NOTE: `ServiceId.Id` may seem redundant but was desigtned created to - enable more complex service identification - - For example, what if we want to request a session for a certain - service but with some additional configs that identify it? - - - Unique identifier for the service - in: query - required: false - type: string - - name: service_id.name - description: >- - TODO_TECHDEBT: Name is currently unused but acts as a reminder than - an optional onchain representation of the service is necessary - - - (Optional) Semantic human readable name for the service - in: query - required: false - type: string - - name: block_height - description: The block height to query the session for - in: query - required: false - type: string - format: int64 - tags: - - Query - /pocket/session/params: - get: - summary: Parameters queries the parameters of the module. - operationId: PocketSessionParams - responses: - '200': - description: A successful response. - schema: - type: object - properties: - params: - description: params holds all the parameters of this module. - type: object - description: >- - QueryParamsResponse is response type for the Query/Params RPC - method. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the type of - the serialized - - protocol buffer message. This string must contain at - least - - one "/" character. The last segment of the URL's path - must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in - a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary - all types that they - - expect it to use in the context of Any. However, for - URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally - set up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based - on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in - the official - - protobuf release, and it is not used for type URLs - beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. - additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer - message along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values - in the form - - of utility functions or additional generated methods of the - Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by - default use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the - last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield - type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with - an - - additional field `@type` which contains the type URL. - Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + NOTE: `ServiceId.Id` may seem redundant but was desigtned created to + enable more complex service identification - If the embedded message type is well-known and has a custom - JSON + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? - representation, that representation will be embedded adding - a field - `value` which holds the custom JSON in addition to the - `@type` + Unique identifier for the service + in: query + required: false + type: string + - name: service_id.name + description: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder than + an optional onchain representation of the service is necessary - field. Example (for message [google.protobuf.Duration][]): - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } + (Optional) Semantic human readable name for the service + in: query + required: false + type: string + - name: block_height + description: The block height to query the session for + in: query + required: false + type: string + format: int64 + tags: + - Query + /pocket/session/params: + get: + summary: Parameters queries the parameters of the module. + operationId: PocketSessionParams + responses: + '200': + description: A successful response. + schema: + type: object + properties: + params: + description: params holds all the parameters of this module. + type: object + description: >- + QueryParamsResponse is response type for the Query/Params RPC + method. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} tags: - Query /pocket/supplier/params: @@ -77133,197 +76623,31 @@ definitions: id: type: string description: Unique identifier for the service - title: >- - NOTE: `ServiceId.Id` may seem redundant but was desigtned - created to enable more complex service identification - - For example, what if we want to request a session for a - certain service but with some additional configs that - identify it? - name: - type: string - description: (Optional) Semantic human readable name for the service - title: >- - TODO_TECHDEBT: Name is currently unused but acts as a - reminder than an optional onchain representation of the - service is necessary - title: >- - ApplicationServiceConfig holds the service configuration the - application stakes for - title: The ID of the service this session is servicing - delegatee_gateway_addresses: - type: array - items: - type: string - title: >- - The Bech32 encoded addresses for all delegatee Gateways, in a - non-nullable slice - delegatee_gateway_pub_keys: - type: array - items: - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the type of the - serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must - represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a - canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary all types - that they - - expect it to use in the context of Any. However, for URLs which - use the - - scheme `http`, `https`, or no scheme, one can optionally set up - a type - - server that maps type URLs to message definitions as follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the - official - - protobuf release, and it is not used for type URLs beginning - with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) might - be - - used with implementation specific semantics. - additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer message along - with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values in the - form - - of utility functions or additional generated methods of the Any - type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: >- - The corresponding cosmos.crypto.PubKey (interface) encoded into an - cosmos.codec.Any for use in the non nullable slice of delegatee - Gateways the application is delegated to. + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary + title: >- + ApplicationServiceConfig holds the service configuration the + application stakes for + title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -77752,258 +77076,86 @@ definitions: on-chain but is included in the header for convenience description: >- SessionHeader is a lightweight header for a session that can be - passed around. - - It is the minimal amount of data required to hydrate & retrieve - all data relevant to the session. - session_id: - type: string - title: A unique pseudoranom ID for this session - session_number: - type: string - format: int64 - title: The session number since genesis - num_blocks_per_session: - type: string - format: int64 - title: The number of blocks per session when this session started - application: - title: A fully hydrated application object this session is for - type: object - properties: - address: - type: string - title: >- - The Bech32 address of the application using cosmos' - ScalarDescriptor to ensure deterministic encoding - stake: - title: The total amount of uPOKT the application has staked - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - - NOTE: The amount field is an Int which implements the custom - method - - signatures required by gogoproto. - service_configs: - type: array - items: - type: object - properties: - service_id: - title: Unique and semantic identifier for the service - type: object - properties: - id: - type: string - description: Unique identifier for the service - title: >- - NOTE: `ServiceId.Id` may seem redundant but was - desigtned created to enable more complex service - identification - - For example, what if we want to request a session - for a certain service but with some additional - configs that identify it? - name: - type: string - description: >- - (Optional) Semantic human readable name for the - service - title: >- - TODO_TECHDEBT: Name is currently unused but acts as - a reminder than an optional onchain representation - of the service is necessary - title: >- - ApplicationServiceConfig holds the service configuration the - application stakes for - title: The ID of the service this session is servicing - delegatee_gateway_pub_keys: - type: array - items: - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the type of - the serialized - - protocol buffer message. This string must contain at - least - - one "/" character. The last segment of the URL's path - must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in - a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary - all types that they - - expect it to use in the context of Any. However, for - URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally - set up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based - on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in - the official - - protobuf release, and it is not used for type URLs - beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. - additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer - message along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values - in the form - - of utility functions or additional generated methods of the - Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by - default use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the - last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield - type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with - an - - additional field `@type` which contains the type URL. - Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + passed around. - If the embedded message type is well-known and has a custom - JSON + It is the minimal amount of data required to hydrate & retrieve + all data relevant to the session. + session_id: + type: string + title: A unique pseudoranom ID for this session + session_number: + type: string + format: int64 + title: The session number since genesis + num_blocks_per_session: + type: string + format: int64 + title: The number of blocks per session when this session started + application: + title: A fully hydrated application object this session is for + type: object + properties: + address: + type: string + title: >- + The Bech32 address of the application using cosmos' + ScalarDescriptor to ensure deterministic encoding + stake: + title: The total amount of uPOKT the application has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. - representation, that representation will be embedded adding - a field - `value` which holds the custom JSON in addition to the - `@type` + NOTE: The amount field is an Int which implements the custom + method - field. Example (for message [google.protobuf.Duration][]): + signatures required by gogoproto. + service_configs: + type: array + items: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: >- - The corresponding cosmos.crypto.PubKey (interface) encoded - into an cosmos.codec.Any for use in the non nullable slice of - delegatee Gateways the application is delegated to. + For example, what if we want to request a session + for a certain service but with some additional + configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as + a reminder than an optional onchain representation + of the service is necessary + title: >- + ApplicationServiceConfig holds the service configuration the + application stakes for + title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice suppliers: type: array items: @@ -78252,179 +77404,13 @@ definitions: ApplicationServiceConfig holds the service configuration the application stakes for title: The ID of the service this session is servicing - delegatee_gateway_pub_keys: + delegatee_gateway_addresses: type: array items: - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the type of the - serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must - represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a - canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary all - types that they - - expect it to use in the context of Any. However, for URLs - which use the - - scheme `http`, `https`, or no scheme, one can optionally set - up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on - the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the - official - - protobuf release, and it is not used for type URLs beginning - with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. - additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer message - along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values in - the form - - of utility functions or additional generated methods of the Any - type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default - use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the last - '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a - field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: >- - The corresponding cosmos.crypto.PubKey (interface) encoded into an - cosmos.codec.Any for use in the non nullable slice of delegatee - Gateways the application is delegated to. + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice suppliers: type: array items: diff --git a/go.mod b/go.mod index 12d4e67a9..95c298124 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module pocket +module github.com/pokt-network/poktroll go 1.19 diff --git a/internal/mocks/.gitkeep b/internal/mocks/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/internal/mocks/mockclient/mocks.go b/internal/mocks/mockclient/mocks.go new file mode 100644 index 000000000..0d9a6b981 --- /dev/null +++ b/internal/mocks/mockclient/mocks.go @@ -0,0 +1,10 @@ +package mockclient + +// This file is in place to declare the package for dynamically generated structs. +// +// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. +// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go +// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. diff --git a/internal/mocks/mocks.go b/internal/mocks/mocks.go new file mode 100644 index 000000000..423f63d3e --- /dev/null +++ b/internal/mocks/mocks.go @@ -0,0 +1,10 @@ +package mocks + +// This file is in place to declare the package for dynamically generated structs. +// +// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. +// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go +// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. diff --git a/internal/testchannel/drain.go b/internal/testchannel/drain.go index 4ea41a297..4b7ff00a3 100644 --- a/internal/testchannel/drain.go +++ b/internal/testchannel/drain.go @@ -3,7 +3,7 @@ package testchannel import ( "time" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) // DrainChannel attempts to receive from the given channel, blocking, until it is diff --git a/internal/testclient/localnet.go b/internal/testclient/localnet.go index a73352e12..61d5c0ad8 100644 --- a/internal/testclient/localnet.go +++ b/internal/testclient/localnet.go @@ -9,8 +9,8 @@ import ( "github.com/spf13/pflag" "github.com/stretchr/testify/require" - "pocket/app" - "pocket/cmd/pocketd/cmd" + "github.com/pokt-network/poktroll/app" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" ) // CometLocalWebsocketURL provides a default URL pointing to the localnet websocket endpoint. diff --git a/internal/testclient/testblock/client.go b/internal/testclient/testblock/client.go index 0d0f1b78b..0918ee64f 100644 --- a/internal/testclient/testblock/client.go +++ b/internal/testclient/testblock/client.go @@ -7,10 +7,10 @@ import ( "cosmossdk.io/depinject" "github.com/stretchr/testify/require" - "pocket/internal/testclient" - "pocket/internal/testclient/testeventsquery" - "pocket/pkg/client" - "pocket/pkg/client/block" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/block" ) func NewLocalnetClient(ctx context.Context, t *testing.T) client.BlockClient { diff --git a/internal/testclient/testeventsquery/client.go b/internal/testclient/testeventsquery/client.go index 2a715aa4b..d55a765ab 100644 --- a/internal/testclient/testeventsquery/client.go +++ b/internal/testclient/testeventsquery/client.go @@ -3,9 +3,9 @@ package testeventsquery import ( "testing" - "pocket/internal/testclient" - "pocket/pkg/client" - eventsquery "pocket/pkg/client/events_query" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/pkg/client" + eventsquery "github.com/pokt-network/poktroll/pkg/client/events_query" ) // NewLocalnetClient returns a new events query client which is configured to diff --git a/internal/testclient/testeventsquery/connection.go b/internal/testclient/testeventsquery/connection.go index 9351c05d6..27a38f2ae 100644 --- a/internal/testclient/testeventsquery/connection.go +++ b/internal/testclient/testeventsquery/connection.go @@ -1,12 +1,12 @@ package testeventsquery import ( - "pocket/pkg/either" "testing" "github.com/golang/mock/gomock" - "pocket/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/pkg/either" ) // NewOneTimeMockConnAndDialer returns a new mock connection and mock dialer that diff --git a/internal/testclient/testtx/context.go b/internal/testclient/testtx/context.go index b87a97f08..e7d1f8446 100644 --- a/internal/testclient/testtx/context.go +++ b/internal/testclient/testtx/context.go @@ -17,10 +17,10 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "pocket/internal/mocks/mockclient" - "pocket/internal/testclient" - "pocket/pkg/client" - "pocket/pkg/client/tx" + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/tx" ) // TODO_IMPROVE: these mock constructor helpers could include parameters for the diff --git a/pkg/client/block/block.go b/pkg/client/block/block.go index 5fe9a2e1e..f5bd94516 100644 --- a/pkg/client/block/block.go +++ b/pkg/client/block/block.go @@ -5,7 +5,7 @@ import ( "github.com/cometbft/cometbft/types" - "pocket/pkg/client" + "github.com/pokt-network/poktroll/pkg/client" ) // cometBlockEvent is used to deserialize incoming committed block event messages diff --git a/pkg/client/block/client.go b/pkg/client/block/client.go index 387f5f16b..54569e60d 100644 --- a/pkg/client/block/client.go +++ b/pkg/client/block/client.go @@ -7,11 +7,11 @@ import ( "cosmossdk.io/depinject" - "pocket/pkg/client" - "pocket/pkg/either" - "pocket/pkg/observable" - "pocket/pkg/observable/channel" - "pocket/pkg/retry" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" + "github.com/pokt-network/poktroll/pkg/retry" ) const ( diff --git a/pkg/client/block/client_integration_test.go b/pkg/client/block/client_integration_test.go index 4f51d7873..fd7e633ab 100644 --- a/pkg/client/block/client_integration_test.go +++ b/pkg/client/block/client_integration_test.go @@ -12,8 +12,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "pocket/internal/testclient/testblock" - "pocket/pkg/client" + "github.com/pokt-network/poktroll/internal/testclient/testblock" + "github.com/pokt-network/poktroll/pkg/client" ) const blockIntegrationSubTimeout = 5 * time.Second diff --git a/pkg/client/block/client_test.go b/pkg/client/block/client_test.go index c787f5ad2..b983ff274 100644 --- a/pkg/client/block/client_test.go +++ b/pkg/client/block/client_test.go @@ -11,11 +11,11 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "pocket/internal/testclient" - "pocket/internal/testclient/testeventsquery" - "pocket/pkg/client" - "pocket/pkg/client/block" - eventsquery "pocket/pkg/client/events_query" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/block" + eventsquery "github.com/pokt-network/poktroll/pkg/client/events_query" ) const blockAssertionLoopTimeout = 500 * time.Millisecond diff --git a/pkg/client/events_query/client.go b/pkg/client/events_query/client.go index bd11e57fb..88ace493f 100644 --- a/pkg/client/events_query/client.go +++ b/pkg/client/events_query/client.go @@ -11,11 +11,11 @@ import ( "go.uber.org/multierr" - "pocket/pkg/client" - "pocket/pkg/client/events_query/websocket" - "pocket/pkg/either" - "pocket/pkg/observable" - "pocket/pkg/observable/channel" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/events_query/websocket" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) var _ client.EventsQueryClient = (*eventsQueryClient)(nil) diff --git a/pkg/client/events_query/client_integration_test.go b/pkg/client/events_query/client_integration_test.go index 1287775a5..05bf09c1a 100644 --- a/pkg/client/events_query/client_integration_test.go +++ b/pkg/client/events_query/client_integration_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" ) // The query use to subscribe for new block events on the websocket endpoint exposed by CometBFT nodes diff --git a/pkg/client/events_query/client_test.go b/pkg/client/events_query/client_test.go index a96516f0e..0ba52ec88 100644 --- a/pkg/client/events_query/client_test.go +++ b/pkg/client/events_query/client_test.go @@ -13,14 +13,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "pocket/internal/mocks/mockclient" - "pocket/internal/testchannel" - "pocket/internal/testclient/testeventsquery" - "pocket/internal/testerrors" - eventsquery "pocket/pkg/client/events_query" - "pocket/pkg/client/events_query/websocket" - "pocket/pkg/either" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testchannel" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/internal/testerrors" + eventsquery "github.com/pokt-network/poktroll/pkg/client/events_query" + "github.com/pokt-network/poktroll/pkg/client/events_query/websocket" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" ) func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { diff --git a/pkg/client/events_query/options.go b/pkg/client/events_query/options.go index affa437f3..0e2a622fe 100644 --- a/pkg/client/events_query/options.go +++ b/pkg/client/events_query/options.go @@ -1,6 +1,6 @@ package eventsquery -import "pocket/pkg/client" +import "github.com/pokt-network/poktroll/pkg/client" // WithDialer returns a client.EventsQueryClientOption which sets the given dialer on the // resulting eventsQueryClient when passed to NewEventsQueryClient(). diff --git a/pkg/client/events_query/websocket/connection.go b/pkg/client/events_query/websocket/connection.go index af82920df..b9311bea3 100644 --- a/pkg/client/events_query/websocket/connection.go +++ b/pkg/client/events_query/websocket/connection.go @@ -3,7 +3,7 @@ package websocket import ( gorillaws "github.com/gorilla/websocket" - "pocket/pkg/client" + "github.com/pokt-network/poktroll/pkg/client" ) var _ client.Connection = (*websocketConn)(nil) diff --git a/pkg/client/events_query/websocket/dialer.go b/pkg/client/events_query/websocket/dialer.go index dc5e9a606..bd0597d03 100644 --- a/pkg/client/events_query/websocket/dialer.go +++ b/pkg/client/events_query/websocket/dialer.go @@ -5,7 +5,7 @@ import ( "github.com/gorilla/websocket" - "pocket/pkg/client" + "github.com/pokt-network/poktroll/pkg/client" ) var _ client.Dialer = (*websocketDialer)(nil) diff --git a/pkg/client/interface.go b/pkg/client/interface.go index c853813ce..2d67c90c0 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -14,8 +14,8 @@ import ( cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" cosmostypes "github.com/cosmos/cosmos-sdk/types" - "pocket/pkg/either" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" ) // TxContext provides an interface which consolidates the operational dependencies diff --git a/pkg/client/tx/context.go b/pkg/client/tx/context.go index 8fd1ed877..5865ae526 100644 --- a/pkg/client/tx/context.go +++ b/pkg/client/tx/context.go @@ -11,7 +11,7 @@ import ( cosmostypes "github.com/cosmos/cosmos-sdk/types" authclient "github.com/cosmos/cosmos-sdk/x/auth/client" - "pocket/pkg/client" + "github.com/pokt-network/poktroll/pkg/client" ) var _ client.TxContext = (*cosmosTxContext)(nil) diff --git a/pkg/observable/channel/map.go b/pkg/observable/channel/map.go index 912043ca9..c6722e09d 100644 --- a/pkg/observable/channel/map.go +++ b/pkg/observable/channel/map.go @@ -3,7 +3,7 @@ package channel import ( "context" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) type MapFn[S, D any] func(src S) (dst D, skip bool) diff --git a/pkg/observable/channel/map_test.go b/pkg/observable/channel/map_test.go index 01014619f..98b9aa8ad 100644 --- a/pkg/observable/channel/map_test.go +++ b/pkg/observable/channel/map_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/pkg/observable/channel" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) func TestMap_Word_BytesToPalindrome(t *testing.T) { diff --git a/pkg/observable/channel/observable.go b/pkg/observable/channel/observable.go index 6c92d29d3..fa898200f 100644 --- a/pkg/observable/channel/observable.go +++ b/pkg/observable/channel/observable.go @@ -2,7 +2,8 @@ package channel import ( "context" - "pocket/pkg/observable" + + "github.com/pokt-network/poktroll/pkg/observable" ) // TODO_DISCUSS: what should this be? should it be configurable? It seems to be most diff --git a/pkg/observable/channel/observable_test.go b/pkg/observable/channel/observable_test.go index 918370cb0..cb89c79d8 100644 --- a/pkg/observable/channel/observable_test.go +++ b/pkg/observable/channel/observable_test.go @@ -10,10 +10,10 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" - "pocket/internal/testchannel" - "pocket/internal/testerrors" - "pocket/pkg/observable" - "pocket/pkg/observable/channel" + "github.com/pokt-network/poktroll/internal/testchannel" + "github.com/pokt-network/poktroll/internal/testerrors" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) const ( diff --git a/pkg/observable/channel/observation_test.go b/pkg/observable/channel/observation_test.go index 17e20e393..71a3aa098 100644 --- a/pkg/observable/channel/observation_test.go +++ b/pkg/observable/channel/observation_test.go @@ -4,7 +4,7 @@ import ( "context" "sync" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) // NOTE: this file does not contain any tests, only test helpers. diff --git a/pkg/observable/channel/observer.go b/pkg/observable/channel/observer.go index a989b2092..95e796e41 100644 --- a/pkg/observable/channel/observer.go +++ b/pkg/observable/channel/observer.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) const ( diff --git a/pkg/observable/channel/observer_manager.go b/pkg/observable/channel/observer_manager.go index 44807c047..65acca5bc 100644 --- a/pkg/observable/channel/observer_manager.go +++ b/pkg/observable/channel/observer_manager.go @@ -4,7 +4,7 @@ import ( "context" "sync" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) var _ observerManager[any] = (*channelObserverManager[any])(nil) diff --git a/pkg/observable/channel/observer_test.go b/pkg/observable/channel/observer_test.go index fe7c865a9..034541c85 100644 --- a/pkg/observable/channel/observer_test.go +++ b/pkg/observable/channel/observer_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) func TestObserver_Unsubscribe(t *testing.T) { diff --git a/pkg/observable/channel/replay.go b/pkg/observable/channel/replay.go index b7c54f877..583edb4e5 100644 --- a/pkg/observable/channel/replay.go +++ b/pkg/observable/channel/replay.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) // replayPartialBufferTimeout is the duration to wait for the replay buffer to diff --git a/pkg/observable/channel/replay_test.go b/pkg/observable/channel/replay_test.go index b04857196..0f9b3e9ac 100644 --- a/pkg/observable/channel/replay_test.go +++ b/pkg/observable/channel/replay_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "pocket/internal/testerrors" - "pocket/pkg/observable/channel" + "github.com/pokt-network/poktroll/internal/testerrors" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) func TestReplayObservable(t *testing.T) { diff --git a/pkg/relayer/proxy/interface.go b/pkg/relayer/proxy/interface.go index 3987c757c..27ee83e72 100644 --- a/pkg/relayer/proxy/interface.go +++ b/pkg/relayer/proxy/interface.go @@ -3,9 +3,9 @@ package proxy import ( "context" - "pocket/pkg/observable" - "pocket/x/service/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/x/service/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // RelayerProxy is the interface for the proxy that serves relays to the application. diff --git a/pkg/relayer/proxy/jsonrpc.go b/pkg/relayer/proxy/jsonrpc.go index 3c105b429..ac9295c6b 100644 --- a/pkg/relayer/proxy/jsonrpc.go +++ b/pkg/relayer/proxy/jsonrpc.go @@ -5,8 +5,8 @@ import ( "net/http" "net/url" - "pocket/x/service/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/x/service/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) var _ RelayServer = (*jsonRPCServer)(nil) diff --git a/pkg/relayer/proxy/proxy.go b/pkg/relayer/proxy/proxy.go index 2e14459a3..033e9caaf 100644 --- a/pkg/relayer/proxy/proxy.go +++ b/pkg/relayer/proxy/proxy.go @@ -10,12 +10,12 @@ import ( "golang.org/x/sync/errgroup" // TODO_INCOMPLETE(@red-0ne): Import the appropriate block client interface once available. - // blocktypes "pocket/pkg/client" - "pocket/pkg/observable" - "pocket/pkg/observable/channel" - "pocket/x/service/types" - sessiontypes "pocket/x/session/types" - suppliertypes "pocket/x/supplier/types" + // blocktypes "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" + "github.com/pokt-network/poktroll/x/service/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" ) var _ RelayerProxy = (*relayerProxy)(nil) diff --git a/pkg/relayer/proxy/server_builder.go b/pkg/relayer/proxy/server_builder.go index 492a4bcd6..eb21cc1f2 100644 --- a/pkg/relayer/proxy/server_builder.go +++ b/pkg/relayer/proxy/server_builder.go @@ -3,8 +3,8 @@ package proxy import ( "context" - sharedtypes "pocket/x/shared/types" - suppliertypes "pocket/x/supplier/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" ) // BuildProvidedServices builds the advertised relay servers from the supplier's on-chain advertised services. diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go index 8a1154c30..d328bda73 100644 --- a/pkg/retry/retry_test.go +++ b/pkg/retry/retry_test.go @@ -19,7 +19,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/pkg/retry" + "github.com/pokt-network/poktroll/pkg/retry" ) var testErr = fmt.Errorf("test error") diff --git a/proto/pocket/application/application.proto b/proto/pocket/application/application.proto index 2c754a5dc..e5763d697 100644 --- a/proto/pocket/application/application.proto +++ b/proto/pocket/application/application.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.application; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; diff --git a/proto/pocket/application/genesis.proto b/proto/pocket/application/genesis.proto index 6598d6636..e2741972a 100644 --- a/proto/pocket/application/genesis.proto +++ b/proto/pocket/application/genesis.proto @@ -6,7 +6,7 @@ import "gogoproto/gogo.proto"; import "pocket/application/params.proto"; import "pocket/application/application.proto"; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; // GenesisState defines the application module's genesis state. message GenesisState { diff --git a/proto/pocket/application/params.proto b/proto/pocket/application/params.proto index 71d2f69ff..18390d1d2 100644 --- a/proto/pocket/application/params.proto +++ b/proto/pocket/application/params.proto @@ -3,7 +3,7 @@ package pocket.application; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; // Params defines the parameters for the module. message Params { diff --git a/proto/pocket/application/query.proto b/proto/pocket/application/query.proto index fd25f232d..28a48fb99 100644 --- a/proto/pocket/application/query.proto +++ b/proto/pocket/application/query.proto @@ -8,25 +8,25 @@ import "cosmos/base/query/v1beta1/pagination.proto"; import "pocket/application/params.proto"; import "pocket/application/application.proto"; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; // Query defines the gRPC querier service. service Query { - + // Parameters queries the parameters of the module. rpc Params (QueryParamsRequest) returns (QueryParamsResponse) { option (google.api.http).get = "/pocket/application/params"; - + } - + // Queries a list of Application items. rpc Application (QueryGetApplicationRequest) returns (QueryGetApplicationResponse) { option (google.api.http).get = "/pocket/application/application/{address}"; - + } rpc ApplicationAll (QueryAllApplicationRequest) returns (QueryAllApplicationResponse) { option (google.api.http).get = "/pocket/application/application"; - + } } // QueryParamsRequest is request type for the Query/Params RPC method. @@ -34,7 +34,7 @@ message QueryParamsRequest {} // QueryParamsResponse is response type for the Query/Params RPC method. message QueryParamsResponse { - + // params holds all the parameters of this module. Params params = 1 [(gogoproto.nullable) = false]; } diff --git a/proto/pocket/application/tx.proto b/proto/pocket/application/tx.proto index e9f50a048..1f4b9674c 100644 --- a/proto/pocket/application/tx.proto +++ b/proto/pocket/application/tx.proto @@ -7,7 +7,7 @@ import "cosmos/base/v1beta1/coin.proto"; import "cosmos/msg/v1/msg.proto"; import "pocket/shared/service.proto"; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; // Msg defines the Msg service. service Msg { diff --git a/proto/pocket/gateway/gateway.proto b/proto/pocket/gateway/gateway.proto index f2a450cb3..ed7b08751 100644 --- a/proto/pocket/gateway/gateway.proto +++ b/proto/pocket/gateway/gateway.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.gateway; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; diff --git a/proto/pocket/gateway/genesis.proto b/proto/pocket/gateway/genesis.proto index 1f1b97cf8..85e6fb8a5 100644 --- a/proto/pocket/gateway/genesis.proto +++ b/proto/pocket/gateway/genesis.proto @@ -6,7 +6,7 @@ import "gogoproto/gogo.proto"; import "pocket/gateway/params.proto"; import "pocket/gateway/gateway.proto"; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; // GenesisState defines the gateway module's genesis state. message GenesisState { diff --git a/proto/pocket/gateway/params.proto b/proto/pocket/gateway/params.proto index 8d5f42acd..040f5630d 100644 --- a/proto/pocket/gateway/params.proto +++ b/proto/pocket/gateway/params.proto @@ -3,10 +3,10 @@ package pocket.gateway; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/gateway/query.proto b/proto/pocket/gateway/query.proto index 2076a32c0..48bd62f98 100644 --- a/proto/pocket/gateway/query.proto +++ b/proto/pocket/gateway/query.proto @@ -8,25 +8,25 @@ import "cosmos/base/query/v1beta1/pagination.proto"; import "pocket/gateway/params.proto"; import "pocket/gateway/gateway.proto"; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; // Query defines the gRPC querier service. service Query { - + // Parameters queries the parameters of the module. rpc Params (QueryParamsRequest) returns (QueryParamsResponse) { option (google.api.http).get = "/pocket/gateway/params"; - + } - + // Queries a list of Gateway items. rpc Gateway (QueryGetGatewayRequest) returns (QueryGetGatewayResponse) { option (google.api.http).get = "/pocket/gateway/gateway/{address}"; - + } rpc GatewayAll (QueryAllGatewayRequest) returns (QueryAllGatewayResponse) { option (google.api.http).get = "/pocket/gateway/gateway"; - + } } // QueryParamsRequest is request type for the Query/Params RPC method. @@ -34,7 +34,7 @@ message QueryParamsRequest {} // QueryParamsResponse is response type for the Query/Params RPC method. message QueryParamsResponse { - + // params holds all the parameters of this module. Params params = 1 [(gogoproto.nullable) = false]; } diff --git a/proto/pocket/gateway/tx.proto b/proto/pocket/gateway/tx.proto index 43b4b3245..6b0814add 100644 --- a/proto/pocket/gateway/tx.proto +++ b/proto/pocket/gateway/tx.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package pocket.gateway; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; import "cosmos/msg/v1/msg.proto"; import "cosmos_proto/cosmos.proto"; diff --git a/proto/pocket/pocket/genesis.proto b/proto/pocket/pocket/genesis.proto index e35b3ae9b..52d21410c 100644 --- a/proto/pocket/pocket/genesis.proto +++ b/proto/pocket/pocket/genesis.proto @@ -4,7 +4,7 @@ package pocket.pocket; import "gogoproto/gogo.proto"; import "pocket/pocket/params.proto"; -option go_package = "pocket/x/pocket/types"; +option go_package = "github.com/pokt-network/poktroll/x/pocket/types"; // GenesisState defines the pocket module's genesis state. message GenesisState { diff --git a/proto/pocket/pocket/params.proto b/proto/pocket/pocket/params.proto index 61db3feb2..a760a6fb6 100644 --- a/proto/pocket/pocket/params.proto +++ b/proto/pocket/pocket/params.proto @@ -3,10 +3,10 @@ package pocket.pocket; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/pocket/types"; +option go_package = "github.com/pokt-network/poktroll/x/pocket/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/pocket/query.proto b/proto/pocket/pocket/query.proto index 3e983730d..55c4471c0 100644 --- a/proto/pocket/pocket/query.proto +++ b/proto/pocket/pocket/query.proto @@ -5,7 +5,7 @@ import "gogoproto/gogo.proto"; import "google/api/annotations.proto"; import "pocket/pocket/params.proto"; -option go_package = "pocket/x/pocket/types"; +option go_package = "github.com/pokt-network/poktroll/x/pocket/types"; // Query defines the gRPC querier service. service Query { diff --git a/proto/pocket/pocket/tx.proto b/proto/pocket/pocket/tx.proto index ba08384d3..a0e741350 100644 --- a/proto/pocket/pocket/tx.proto +++ b/proto/pocket/pocket/tx.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.pocket; -option go_package = "pocket/x/pocket/types"; +option go_package = "github.com/pokt-network/poktroll/x/pocket/types"; // Msg defines the Msg service. service Msg {} \ No newline at end of file diff --git a/proto/pocket/service/genesis.proto b/proto/pocket/service/genesis.proto index 50f61d258..5ca50c9f0 100644 --- a/proto/pocket/service/genesis.proto +++ b/proto/pocket/service/genesis.proto @@ -4,7 +4,7 @@ package pocket.service; import "gogoproto/gogo.proto"; import "pocket/service/params.proto"; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; // GenesisState defines the service module's genesis state. message GenesisState { diff --git a/proto/pocket/service/params.proto b/proto/pocket/service/params.proto index 54cc15bd6..9b7fe8363 100644 --- a/proto/pocket/service/params.proto +++ b/proto/pocket/service/params.proto @@ -3,10 +3,10 @@ package pocket.service; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/service/query.proto b/proto/pocket/service/query.proto index cd7c836fd..4abf2a13e 100644 --- a/proto/pocket/service/query.proto +++ b/proto/pocket/service/query.proto @@ -6,7 +6,7 @@ import "google/api/annotations.proto"; import "cosmos/base/query/v1beta1/pagination.proto"; import "pocket/service/params.proto"; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; // Query defines the gRPC querier service. service Query { diff --git a/proto/pocket/service/relay.proto b/proto/pocket/service/relay.proto index 70c5854dd..3450a1e40 100644 --- a/proto/pocket/service/relay.proto +++ b/proto/pocket/service/relay.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.service; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; import "cosmos_proto/cosmos.proto"; // TODO(@Olshansk): Uncomment the line below once the `service.proto` is added. diff --git a/proto/pocket/service/tx.proto b/proto/pocket/service/tx.proto index 21f556e08..70aef03b7 100644 --- a/proto/pocket/service/tx.proto +++ b/proto/pocket/service/tx.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.service; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; // Msg defines the Msg service. service Msg {} \ No newline at end of file diff --git a/proto/pocket/session/genesis.proto b/proto/pocket/session/genesis.proto index d0dc85aeb..2ee8ed8ff 100644 --- a/proto/pocket/session/genesis.proto +++ b/proto/pocket/session/genesis.proto @@ -4,7 +4,7 @@ package pocket.session; import "gogoproto/gogo.proto"; import "pocket/session/params.proto"; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; // GenesisState defines the session module's genesis state. message GenesisState { diff --git a/proto/pocket/session/params.proto b/proto/pocket/session/params.proto index 391c43b5f..428f2999e 100644 --- a/proto/pocket/session/params.proto +++ b/proto/pocket/session/params.proto @@ -3,10 +3,10 @@ package pocket.session; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/session/query.proto b/proto/pocket/session/query.proto index cd3ef8380..f8b1c7187 100644 --- a/proto/pocket/session/query.proto +++ b/proto/pocket/session/query.proto @@ -9,7 +9,7 @@ import "pocket/session/params.proto"; import "pocket/session/session.proto"; import "pocket/shared/service.proto"; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; // Query defines the gRPC querier service. service Query { diff --git a/proto/pocket/session/session.proto b/proto/pocket/session/session.proto index 7d864e1b1..e8f14b35e 100644 --- a/proto/pocket/session/session.proto +++ b/proto/pocket/session/session.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.session; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; import "cosmos_proto/cosmos.proto"; import "pocket/shared/service.proto"; diff --git a/proto/pocket/session/tx.proto b/proto/pocket/session/tx.proto index 0590fcee9..6d793ffdb 100644 --- a/proto/pocket/session/tx.proto +++ b/proto/pocket/session/tx.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.session; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; // Msg defines the Msg service. service Msg {} \ No newline at end of file diff --git a/proto/pocket/shared/service.proto b/proto/pocket/shared/service.proto index 42827cef2..c911bad1f 100644 --- a/proto/pocket/shared/service.proto +++ b/proto/pocket/shared/service.proto @@ -4,7 +4,7 @@ syntax = "proto3"; // but rather a manually created package to resolve circular dependencies. package pocket.shared; -option go_package = "pocket/x/shared/types"; +option go_package = "github.com/pokt-network/poktroll/x/shared/types"; // TODO_CLEANUP(@Olshansk): Add native optional identifiers once its supported; https://github.com/ignite/cli/issues/3698 diff --git a/proto/pocket/shared/supplier.proto b/proto/pocket/shared/supplier.proto index f16cd9a11..90e6999b3 100644 --- a/proto/pocket/shared/supplier.proto +++ b/proto/pocket/shared/supplier.proto @@ -4,7 +4,7 @@ package pocket.shared; // NOTE that the `shared` package is not a Cosmos module, // but rather a manually created package to resolve circular dependencies. -option go_package = "pocket/x/shared/types"; +option go_package = "github.com/pokt-network/poktroll/x/shared/types"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; diff --git a/proto/pocket/supplier/genesis.proto b/proto/pocket/supplier/genesis.proto index 186ef81b8..81d3550d0 100644 --- a/proto/pocket/supplier/genesis.proto +++ b/proto/pocket/supplier/genesis.proto @@ -6,7 +6,7 @@ import "gogoproto/gogo.proto"; import "pocket/supplier/params.proto"; import "pocket/shared/supplier.proto"; -option go_package = "pocket/x/supplier/types"; +option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; // GenesisState defines the supplier module's genesis state. message GenesisState { diff --git a/proto/pocket/supplier/params.proto b/proto/pocket/supplier/params.proto index 8e0d4c79b..6623b4b82 100644 --- a/proto/pocket/supplier/params.proto +++ b/proto/pocket/supplier/params.proto @@ -3,10 +3,10 @@ package pocket.supplier; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/supplier/types"; +option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/supplier/query.proto b/proto/pocket/supplier/query.proto index 339f03307..6ed7eb194 100644 --- a/proto/pocket/supplier/query.proto +++ b/proto/pocket/supplier/query.proto @@ -8,25 +8,25 @@ import "cosmos/base/query/v1beta1/pagination.proto"; import "pocket/supplier/params.proto"; import "pocket/shared/supplier.proto"; -option go_package = "pocket/x/supplier/types"; +option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; // Query defines the gRPC querier service. service Query { - + // Parameters queries the parameters of the module. rpc Params (QueryParamsRequest) returns (QueryParamsResponse) { option (google.api.http).get = "/pocket/supplier/params"; - + } - + // Queries a list of Supplier items. rpc Supplier (QueryGetSupplierRequest) returns (QueryGetSupplierResponse) { option (google.api.http).get = "/pocket/supplier/supplier/{address}"; - + } rpc SupplierAll (QueryAllSupplierRequest) returns (QueryAllSupplierResponse) { option (google.api.http).get = "/pocket/supplier/supplier"; - + } } // QueryParamsRequest is request type for the Query/Params RPC method. @@ -34,7 +34,7 @@ message QueryParamsRequest {} // QueryParamsResponse is response type for the Query/Params RPC method. message QueryParamsResponse { - + // params holds all the parameters of this module. Params params = 1 [(gogoproto.nullable) = false]; } diff --git a/proto/pocket/supplier/tx.proto b/proto/pocket/supplier/tx.proto index e334cdd53..2671b83a9 100644 --- a/proto/pocket/supplier/tx.proto +++ b/proto/pocket/supplier/tx.proto @@ -9,7 +9,7 @@ import "cosmos/msg/v1/msg.proto"; import "pocket/session/session.proto"; import "pocket/shared/service.proto"; -option go_package = "pocket/x/supplier/types"; +option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; // Msg defines the Msg service. service Msg { diff --git a/testutil/application/mocks/.gitkeep b/testutil/application/mocks/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/testutil/application/mocks/mocks.go b/testutil/application/mocks/mocks.go new file mode 100644 index 000000000..423f63d3e --- /dev/null +++ b/testutil/application/mocks/mocks.go @@ -0,0 +1,10 @@ +package mocks + +// This file is in place to declare the package for dynamically generated structs. +// +// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. +// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go +// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. diff --git a/testutil/gateway/mocks/.gitkeep b/testutil/gateway/mocks/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/testutil/gateway/mocks/mocks.go b/testutil/gateway/mocks/mocks.go new file mode 100644 index 000000000..423f63d3e --- /dev/null +++ b/testutil/gateway/mocks/mocks.go @@ -0,0 +1,10 @@ +package mocks + +// This file is in place to declare the package for dynamically generated structs. +// +// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. +// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go +// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. diff --git a/testutil/keeper/application.go b/testutil/keeper/application.go index 0546b27a7..7a92cd2f1 100644 --- a/testutil/keeper/application.go +++ b/testutil/keeper/application.go @@ -15,10 +15,10 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - mocks "pocket/testutil/application/mocks" - "pocket/x/application/keeper" - "pocket/x/application/types" - gatewaytypes "pocket/x/gateway/types" + mocks "github.com/pokt-network/poktroll/testutil/application/mocks" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + gatewaytypes "github.com/pokt-network/poktroll/x/gateway/types" ) // StakedGatewayMap is used to mock whether a gateway is staked or not for use diff --git a/testutil/keeper/gateway.go b/testutil/keeper/gateway.go index 2a7f27ea9..7a7fa84bb 100644 --- a/testutil/keeper/gateway.go +++ b/testutil/keeper/gateway.go @@ -3,11 +3,6 @@ package keeper import ( "testing" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" - - mocks "pocket/testutil/gateway/mocks" - tmdb "github.com/cometbft/cometbft-db" "github.com/cometbft/cometbft/libs/log" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -19,6 +14,10 @@ import ( typesparams "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + + mocks "github.com/pokt-network/poktroll/testutil/gateway/mocks" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func GatewayKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { diff --git a/testutil/keeper/pocket.go b/testutil/keeper/pocket.go index 096be904f..cebf1b0bf 100644 --- a/testutil/keeper/pocket.go +++ b/testutil/keeper/pocket.go @@ -13,8 +13,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" typesparams "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/stretchr/testify/require" - "pocket/x/pocket/keeper" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) func PocketKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { diff --git a/testutil/keeper/service.go b/testutil/keeper/service.go index 404e2e7fc..3bdb9f219 100644 --- a/testutil/keeper/service.go +++ b/testutil/keeper/service.go @@ -13,8 +13,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" typesparams "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/stretchr/testify/require" - "pocket/x/service/keeper" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) func ServiceKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { diff --git a/testutil/keeper/session.go b/testutil/keeper/session.go index c25a188a9..338204789 100644 --- a/testutil/keeper/session.go +++ b/testutil/keeper/session.go @@ -16,12 +16,12 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "pocket/testutil/sample" - mocks "pocket/testutil/session/mocks" - apptypes "pocket/x/application/types" - "pocket/x/session/keeper" - "pocket/x/session/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/testutil/sample" + mocks "github.com/pokt-network/poktroll/testutil/session/mocks" + apptypes "github.com/pokt-network/poktroll/x/application/types" + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) type option[V any] func(k *keeper.Keeper) diff --git a/testutil/keeper/supplier.go b/testutil/keeper/supplier.go index 769d314c2..d54095fd6 100644 --- a/testutil/keeper/supplier.go +++ b/testutil/keeper/supplier.go @@ -15,9 +15,9 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - mocks "pocket/testutil/supplier/mocks" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + mocks "github.com/pokt-network/poktroll/testutil/supplier/mocks" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SupplierKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { @@ -49,6 +49,7 @@ func SupplierKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { storeKey, memStoreKey, paramsSubspace, + mockBankKeeper, ) diff --git a/testutil/network/network.go b/testutil/network/network.go index f0c0fb92b..c8ab3efcb 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -21,12 +21,12 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/require" - "pocket/app" - "pocket/testutil/sample" - apptypes "pocket/x/application/types" - gatewaytypes "pocket/x/gateway/types" - sharedtypes "pocket/x/shared/types" - suppliertypes "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/app" + "github.com/pokt-network/poktroll/testutil/sample" + apptypes "github.com/pokt-network/poktroll/x/application/types" + gatewaytypes "github.com/pokt-network/poktroll/x/gateway/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" ) type ( diff --git a/testutil/session/mocks/mocks.go b/testutil/session/mocks/mocks.go index 4ccc3e251..423f63d3e 100644 --- a/testutil/session/mocks/mocks.go +++ b/testutil/session/mocks/mocks.go @@ -1,6 +1,10 @@ package mocks // This file is in place to declare the package for dynamically generated structs. +// // Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. // For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go // Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. diff --git a/testutil/supplier/mocks/.gitkeep b/testutil/supplier/mocks/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/testutil/supplier/mocks/mocks.go b/testutil/supplier/mocks/mocks.go new file mode 100644 index 000000000..423f63d3e --- /dev/null +++ b/testutil/supplier/mocks/mocks.go @@ -0,0 +1,10 @@ +package mocks + +// This file is in place to declare the package for dynamically generated structs. +// +// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. +// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go +// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. diff --git a/x/application/client/cli/helpers_test.go b/x/application/client/cli/helpers_test.go index fb6bb5152..46f1e9440 100644 --- a/x/application/client/cli/helpers_test.go +++ b/x/application/client/cli/helpers_test.go @@ -7,9 +7,9 @@ import ( "github.com/stretchr/testify/require" - "pocket/cmd/pocketd/cmd" - "pocket/testutil/network" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/types" ) // Dummy variable to avoid unused import error. diff --git a/x/application/client/cli/query.go b/x/application/client/cli/query.go index 2c42f0de6..86cd73b25 100644 --- a/x/application/client/cli/query.go +++ b/x/application/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/application/client/cli/query_application.go b/x/application/client/cli/query_application.go index a11a3059e..61a5eb35b 100644 --- a/x/application/client/cli/query_application.go +++ b/x/application/client/cli/query_application.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func CmdListApplication() *cobra.Command { diff --git a/x/application/client/cli/query_application_test.go b/x/application/client/cli/query_application_test.go index 376f63a62..10a8b77c6 100644 --- a/x/application/client/cli/query_application_test.go +++ b/x/application/client/cli/query_application_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/testutil/nullify" - "pocket/x/application/client/cli" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" ) func TestShowApplication(t *testing.T) { diff --git a/x/application/client/cli/query_params.go b/x/application/client/cli/query_params.go index 1c14c41b5..7a47d705c 100644 --- a/x/application/client/cli/query_params.go +++ b/x/application/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/application/client/cli/tx.go b/x/application/client/cli/tx.go index be1ead388..cb09f9b3f 100644 --- a/x/application/client/cli/tx.go +++ b/x/application/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) var ( diff --git a/x/application/client/cli/tx_delegate_to_gateway.go b/x/application/client/cli/tx_delegate_to_gateway.go index f993739db..324f88622 100644 --- a/x/application/client/cli/tx_delegate_to_gateway.go +++ b/x/application/client/cli/tx_delegate_to_gateway.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) var _ = strconv.Itoa(0) diff --git a/x/application/client/cli/tx_delegate_to_gateway_test.go b/x/application/client/cli/tx_delegate_to_gateway_test.go index b70f4558b..97ad29a91 100644 --- a/x/application/client/cli/tx_delegate_to_gateway_test.go +++ b/x/application/client/cli/tx_delegate_to_gateway_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/application/client/cli" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" ) func TestCLI_DelegateToGateway(t *testing.T) { diff --git a/x/application/client/cli/tx_stake_application.go b/x/application/client/cli/tx_stake_application.go index b486bc6be..4b077e6c2 100644 --- a/x/application/client/cli/tx_stake_application.go +++ b/x/application/client/cli/tx_stake_application.go @@ -10,7 +10,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/cobra" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) var _ = strconv.Itoa(0) diff --git a/x/application/client/cli/tx_stake_application_test.go b/x/application/client/cli/tx_stake_application_test.go index 12f9f75d8..f88b02fea 100644 --- a/x/application/client/cli/tx_stake_application_test.go +++ b/x/application/client/cli/tx_stake_application_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/application/client/cli" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" ) func TestCLI_StakeApplication(t *testing.T) { diff --git a/x/application/client/cli/tx_undelegate_from_gateway.go b/x/application/client/cli/tx_undelegate_from_gateway.go index 0b31de3d2..95a770baa 100644 --- a/x/application/client/cli/tx_undelegate_from_gateway.go +++ b/x/application/client/cli/tx_undelegate_from_gateway.go @@ -3,7 +3,7 @@ package cli import ( "strconv" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" diff --git a/x/application/client/cli/tx_undelegate_from_gateway_test.go b/x/application/client/cli/tx_undelegate_from_gateway_test.go index 93e9382ff..a1c57973d 100644 --- a/x/application/client/cli/tx_undelegate_from_gateway_test.go +++ b/x/application/client/cli/tx_undelegate_from_gateway_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/application/client/cli" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" ) func TestCLI_UndelegateFromGateway(t *testing.T) { diff --git a/x/application/client/cli/tx_unstake_application.go b/x/application/client/cli/tx_unstake_application.go index a6bb6a689..ebf720a82 100644 --- a/x/application/client/cli/tx_unstake_application.go +++ b/x/application/client/cli/tx_unstake_application.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) var _ = strconv.Itoa(0) diff --git a/x/application/client/cli/tx_unstake_application_test.go b/x/application/client/cli/tx_unstake_application_test.go index b2906e9df..2681a7d77 100644 --- a/x/application/client/cli/tx_unstake_application_test.go +++ b/x/application/client/cli/tx_unstake_application_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/application/client/cli" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" ) func TestCLI_UnstakeApplication(t *testing.T) { diff --git a/x/application/genesis.go b/x/application/genesis.go index 89e0ccac9..713c9da61 100644 --- a/x/application/genesis.go +++ b/x/application/genesis.go @@ -3,8 +3,8 @@ package application import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/keeper" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/application/genesis_test.go b/x/application/genesis_test.go index 1c8e883ad..51d1f7ec9 100644 --- a/x/application/genesis_test.go +++ b/x/application/genesis_test.go @@ -6,12 +6,12 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/testutil/sample" - "pocket/x/application" - "pocket/x/application/types" - sharedtypes "pocket/x/shared/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // Please see `x/application/types/genesis_test.go` for extensive tests related to the validity of the genesis state. diff --git a/x/application/keeper/application.go b/x/application/keeper/application.go index 64025ff1b..b95b77f9e 100644 --- a/x/application/keeper/application.go +++ b/x/application/keeper/application.go @@ -4,7 +4,7 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) // SetApplication set a specific application in the store from its index diff --git a/x/application/keeper/application_test.go b/x/application/keeper/application_test.go index 38c16c0e6..0fd7d7ea1 100644 --- a/x/application/keeper/application_test.go +++ b/x/application/keeper/application_test.go @@ -9,13 +9,13 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/require" - "pocket/cmd/pocketd/cmd" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/testutil/sample" - "pocket/x/application/keeper" - "pocket/x/application/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // Prevent strconv unused error diff --git a/x/application/keeper/keeper.go b/x/application/keeper/keeper.go index 6d0f54b47..38ac34db4 100644 --- a/x/application/keeper/keeper.go +++ b/x/application/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) type ( diff --git a/x/application/keeper/msg_server.go b/x/application/keeper/msg_server.go index 4772c9c72..8f740e15c 100644 --- a/x/application/keeper/msg_server.go +++ b/x/application/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) type msgServer struct { diff --git a/x/application/keeper/msg_server_delegate_to_gateway.go b/x/application/keeper/msg_server_delegate_to_gateway.go index 1fe9bb786..a50523905 100644 --- a/x/application/keeper/msg_server_delegate_to_gateway.go +++ b/x/application/keeper/msg_server_delegate_to_gateway.go @@ -3,7 +3,7 @@ package keeper import ( "context" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/application/keeper/msg_server_delegate_to_gateway_test.go b/x/application/keeper/msg_server_delegate_to_gateway_test.go index 470c8e572..2e48edf5f 100644 --- a/x/application/keeper/msg_server_delegate_to_gateway_test.go +++ b/x/application/keeper/msg_server_delegate_to_gateway_test.go @@ -6,11 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/application/keeper" - "pocket/x/application/types" - sharedtypes "pocket/x/shared/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestMsgServer_DelegateToGateway_SuccessfullyDelegate(t *testing.T) { diff --git a/x/application/keeper/msg_server_stake_application.go b/x/application/keeper/msg_server_stake_application.go index 6f4a73f64..cc735919b 100644 --- a/x/application/keeper/msg_server_stake_application.go +++ b/x/application/keeper/msg_server_stake_application.go @@ -6,7 +6,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func (k msgServer) StakeApplication( diff --git a/x/application/keeper/msg_server_stake_application_test.go b/x/application/keeper/msg_server_stake_application_test.go index ad8edffb6..3b67cd20c 100644 --- a/x/application/keeper/msg_server_stake_application_test.go +++ b/x/application/keeper/msg_server_stake_application_test.go @@ -6,11 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/application/keeper" - "pocket/x/application/types" - sharedtypes "pocket/x/shared/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { diff --git a/x/application/keeper/msg_server_test.go b/x/application/keeper/msg_server_test.go index a3a8d787d..cc6a8f16f 100644 --- a/x/application/keeper/msg_server_test.go +++ b/x/application/keeper/msg_server_test.go @@ -7,9 +7,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/application/keeper" - "pocket/x/application/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/application/keeper/msg_server_undelegate_from_gateway.go b/x/application/keeper/msg_server_undelegate_from_gateway.go index 39889063a..a1239d7ef 100644 --- a/x/application/keeper/msg_server_undelegate_from_gateway.go +++ b/x/application/keeper/msg_server_undelegate_from_gateway.go @@ -6,7 +6,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func (k msgServer) UndelegateFromGateway(goCtx context.Context, msg *types.MsgUndelegateFromGateway) (*types.MsgUndelegateFromGatewayResponse, error) { diff --git a/x/application/keeper/msg_server_undelegate_from_gateway_test.go b/x/application/keeper/msg_server_undelegate_from_gateway_test.go index e9c9bf70c..30824a6d2 100644 --- a/x/application/keeper/msg_server_undelegate_from_gateway_test.go +++ b/x/application/keeper/msg_server_undelegate_from_gateway_test.go @@ -6,11 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/application/keeper" - "pocket/x/application/types" - sharedtypes "pocket/x/shared/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestMsgServer_UndelegateFromGateway_SuccessfullyUndelegate(t *testing.T) { diff --git a/x/application/keeper/msg_server_unstake_application.go b/x/application/keeper/msg_server_unstake_application.go index 376ea45fb..8f062487b 100644 --- a/x/application/keeper/msg_server_unstake_application.go +++ b/x/application/keeper/msg_server_unstake_application.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) // TODO(#73): Determine if an application needs an unbonding period after unstaking. diff --git a/x/application/keeper/msg_server_unstake_application_test.go b/x/application/keeper/msg_server_unstake_application_test.go index 370f29905..fece27bd0 100644 --- a/x/application/keeper/msg_server_unstake_application_test.go +++ b/x/application/keeper/msg_server_unstake_application_test.go @@ -6,11 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/application/keeper" - "pocket/x/application/types" - sharedtypes "pocket/x/shared/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestMsgServer_UnstakeApplication_Success(t *testing.T) { diff --git a/x/application/keeper/params.go b/x/application/keeper/params.go index c7dab2f49..566999f58 100644 --- a/x/application/keeper/params.go +++ b/x/application/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + + "github.com/pokt-network/poktroll/x/application/types" ) // GetParams get all parameters as types.Params diff --git a/x/application/keeper/params_test.go b/x/application/keeper/params_test.go index 36d9af4ff..fb0b40921 100644 --- a/x/application/keeper/params_test.go +++ b/x/application/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/application/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) func TestGetParams(t *testing.T) { diff --git a/x/application/keeper/query.go b/x/application/keeper/query.go index 9b386f4ce..288fcb527 100644 --- a/x/application/keeper/query.go +++ b/x/application/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/application/keeper/query_application.go b/x/application/keeper/query_application.go index 9b2aa7be5..1a6b2a1b7 100644 --- a/x/application/keeper/query_application.go +++ b/x/application/keeper/query_application.go @@ -9,7 +9,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func (k Keeper) ApplicationAll(goCtx context.Context, req *types.QueryAllApplicationRequest) (*types.QueryAllApplicationResponse, error) { diff --git a/x/application/keeper/query_application_test.go b/x/application/keeper/query_application_test.go index 714191885..feb0fef55 100644 --- a/x/application/keeper/query_application_test.go +++ b/x/application/keeper/query_application_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/application/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/application/types" ) // Prevent strconv unused error diff --git a/x/application/keeper/query_params.go b/x/application/keeper/query_params.go index ad362722b..b0c717d4f 100644 --- a/x/application/keeper/query_params.go +++ b/x/application/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/application/types" + + "github.com/pokt-network/poktroll/x/application/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/application/keeper/query_params_test.go b/x/application/keeper/query_params_test.go index 8ea47b150..6463c3764 100644 --- a/x/application/keeper/query_params_test.go +++ b/x/application/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/application/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/application/module.go b/x/application/module.go index 3ebe43d9e..44ba01150 100644 --- a/x/application/module.go +++ b/x/application/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/application/client/cli" - "pocket/x/application/keeper" - "pocket/x/application/types" + + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) var ( diff --git a/x/application/module_simulation.go b/x/application/module_simulation.go index 40f47fe42..9982f2db9 100644 --- a/x/application/module_simulation.go +++ b/x/application/module_simulation.go @@ -9,9 +9,9 @@ import ( simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "pocket/testutil/sample" - applicationsimulation "pocket/x/application/simulation" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/sample" + applicationsimulation "github.com/pokt-network/poktroll/x/application/simulation" + "github.com/pokt-network/poktroll/x/application/types" ) // avoid unused import issue diff --git a/x/application/simulation/delegate_to_gateway.go b/x/application/simulation/delegate_to_gateway.go index b9e337d58..37070e16e 100644 --- a/x/application/simulation/delegate_to_gateway.go +++ b/x/application/simulation/delegate_to_gateway.go @@ -3,8 +3,8 @@ package simulation import ( "math/rand" - "pocket/x/application/keeper" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/application/simulation/stake_application.go b/x/application/simulation/stake_application.go index 107fc0b6a..d17afd14e 100644 --- a/x/application/simulation/stake_application.go +++ b/x/application/simulation/stake_application.go @@ -7,8 +7,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/application/keeper" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) // TODO(@Olshansk): Implement simulation for application staking diff --git a/x/application/simulation/undelegate_from_gateway.go b/x/application/simulation/undelegate_from_gateway.go index c5702c0d5..9ada988fd 100644 --- a/x/application/simulation/undelegate_from_gateway.go +++ b/x/application/simulation/undelegate_from_gateway.go @@ -3,8 +3,8 @@ package simulation import ( "math/rand" - "pocket/x/application/keeper" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/application/simulation/unstake_application.go b/x/application/simulation/unstake_application.go index 51c35dc4a..3dcccd3b5 100644 --- a/x/application/simulation/unstake_application.go +++ b/x/application/simulation/unstake_application.go @@ -8,8 +8,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/application/keeper" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) // TODO(@Olshansk): Implement simulation for application staking diff --git a/x/application/types/expected_keepers.go b/x/application/types/expected_keepers.go index ab3cd615d..55ab9222a 100644 --- a/x/application/types/expected_keepers.go +++ b/x/application/types/expected_keepers.go @@ -6,7 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" - gatewaytypes "pocket/x/gateway/types" + gatewaytypes "github.com/pokt-network/poktroll/x/gateway/types" ) // AccountKeeper defines the expected account keeper used for simulations (noalias) diff --git a/x/application/types/genesis.go b/x/application/types/genesis.go index b9fa03931..ce8046f53 100644 --- a/x/application/types/genesis.go +++ b/x/application/types/genesis.go @@ -6,7 +6,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - servicehelpers "pocket/x/shared/helpers" + servicehelpers "github.com/pokt-network/poktroll/x/shared/helpers" ) // DefaultIndex is the default global index diff --git a/x/application/types/genesis_test.go b/x/application/types/genesis_test.go index 74c31c2ed..01dd9c174 100644 --- a/x/application/types/genesis_test.go +++ b/x/application/types/genesis_test.go @@ -6,9 +6,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" - "pocket/x/application/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestGenesisState_Validate(t *testing.T) { diff --git a/x/application/types/message_delegate_to_gateway_test.go b/x/application/types/message_delegate_to_gateway_test.go index bf1254a29..0769d3e65 100644 --- a/x/application/types/message_delegate_to_gateway_test.go +++ b/x/application/types/message_delegate_to_gateway_test.go @@ -3,7 +3,7 @@ package types import ( "testing" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" "github.com/stretchr/testify/require" ) diff --git a/x/application/types/message_stake_application.go b/x/application/types/message_stake_application.go index 678c49cac..9dba9eebf 100644 --- a/x/application/types/message_stake_application.go +++ b/x/application/types/message_stake_application.go @@ -5,8 +5,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/types" - servicehelpers "pocket/x/shared/helpers" - sharedtypes "pocket/x/shared/types" + servicehelpers "github.com/pokt-network/poktroll/x/shared/helpers" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) const TypeMsgStakeApplication = "stake_application" diff --git a/x/application/types/message_stake_application_test.go b/x/application/types/message_stake_application_test.go index 90ec6969c..c14119898 100644 --- a/x/application/types/message_stake_application_test.go +++ b/x/application/types/message_stake_application_test.go @@ -6,8 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestMsgStakeApplication_ValidateBasic(t *testing.T) { diff --git a/x/application/types/message_undelegate_from_gateway_test.go b/x/application/types/message_undelegate_from_gateway_test.go index 72919eefb..b40b520e9 100644 --- a/x/application/types/message_undelegate_from_gateway_test.go +++ b/x/application/types/message_undelegate_from_gateway_test.go @@ -3,7 +3,7 @@ package types import ( "testing" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" "github.com/stretchr/testify/require" ) diff --git a/x/application/types/message_unstake_application_test.go b/x/application/types/message_unstake_application_test.go index ca31b6dd0..fdc9a5a91 100644 --- a/x/application/types/message_unstake_application_test.go +++ b/x/application/types/message_unstake_application_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) func TestMsgUnstakeApplication_ValidateBasic(t *testing.T) { diff --git a/x/gateway/client/cli/helpers_test.go b/x/gateway/client/cli/helpers_test.go index 32f159e55..927212a46 100644 --- a/x/gateway/client/cli/helpers_test.go +++ b/x/gateway/client/cli/helpers_test.go @@ -3,8 +3,8 @@ package cli_test import ( "testing" - "pocket/testutil/network" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/gateway/types" "github.com/stretchr/testify/require" ) diff --git a/x/gateway/client/cli/query.go b/x/gateway/client/cli/query.go index 5bffe1840..b896f7583 100644 --- a/x/gateway/client/cli/query.go +++ b/x/gateway/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/gateway/client/cli/query_gateway.go b/x/gateway/client/cli/query_gateway.go index be9a3f964..30076ed22 100644 --- a/x/gateway/client/cli/query_gateway.go +++ b/x/gateway/client/cli/query_gateway.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) func CmdListGateway() *cobra.Command { diff --git a/x/gateway/client/cli/query_gateway_test.go b/x/gateway/client/cli/query_gateway_test.go index 37e0ec3b3..fba32713c 100644 --- a/x/gateway/client/cli/query_gateway_test.go +++ b/x/gateway/client/cli/query_gateway_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/testutil/nullify" - "pocket/x/gateway/client/cli" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/gateway/client/cli" + "github.com/pokt-network/poktroll/x/gateway/types" ) // Prevent strconv unused error diff --git a/x/gateway/client/cli/query_params.go b/x/gateway/client/cli/query_params.go index b868cb116..447a64b79 100644 --- a/x/gateway/client/cli/query_params.go +++ b/x/gateway/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/gateway/client/cli/tx.go b/x/gateway/client/cli/tx.go index 3d8c8a520..52be2fb88 100644 --- a/x/gateway/client/cli/tx.go +++ b/x/gateway/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) var DefaultRelativePacketTimeoutTimestamp = uint64((time.Duration(10) * time.Minute).Nanoseconds()) diff --git a/x/gateway/client/cli/tx_stake_gateway.go b/x/gateway/client/cli/tx_stake_gateway.go index cabb4de31..2104b2523 100644 --- a/x/gateway/client/cli/tx_stake_gateway.go +++ b/x/gateway/client/cli/tx_stake_gateway.go @@ -3,7 +3,7 @@ package cli import ( "strconv" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" diff --git a/x/gateway/client/cli/tx_stake_gateway_test.go b/x/gateway/client/cli/tx_stake_gateway_test.go index 1d9d43e57..ce1fb39d0 100644 --- a/x/gateway/client/cli/tx_stake_gateway_test.go +++ b/x/gateway/client/cli/tx_stake_gateway_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/gateway/client/cli" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/gateway/client/cli" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestCLI_StakeGateway(t *testing.T) { diff --git a/x/gateway/client/cli/tx_unstake_gateway.go b/x/gateway/client/cli/tx_unstake_gateway.go index 28bfa0623..b57fd9eb7 100644 --- a/x/gateway/client/cli/tx_unstake_gateway.go +++ b/x/gateway/client/cli/tx_unstake_gateway.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) var _ = strconv.Itoa(0) diff --git a/x/gateway/client/cli/tx_unstake_gateway_test.go b/x/gateway/client/cli/tx_unstake_gateway_test.go index 2bd58b552..b0aa9fc16 100644 --- a/x/gateway/client/cli/tx_unstake_gateway_test.go +++ b/x/gateway/client/cli/tx_unstake_gateway_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/gateway/client/cli" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/gateway/client/cli" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestCLI_UnstakeGateway(t *testing.T) { diff --git a/x/gateway/genesis.go b/x/gateway/genesis.go index 060183c3d..eb9791d31 100644 --- a/x/gateway/genesis.go +++ b/x/gateway/genesis.go @@ -3,8 +3,8 @@ package gateway import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/gateway/genesis_test.go b/x/gateway/genesis_test.go index ad8ff9d51..2a020c7cb 100644 --- a/x/gateway/genesis_test.go +++ b/x/gateway/genesis_test.go @@ -4,10 +4,11 @@ import ( "testing" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/gateway" - "pocket/x/gateway/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/gateway" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestGenesis(t *testing.T) { diff --git a/x/gateway/keeper/gateway.go b/x/gateway/keeper/gateway.go index 2e6dca3de..4cb1e5092 100644 --- a/x/gateway/keeper/gateway.go +++ b/x/gateway/keeper/gateway.go @@ -3,7 +3,8 @@ package keeper import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/types" ) // SetGateway set a specific gateway in the store from its index diff --git a/x/gateway/keeper/gateway_test.go b/x/gateway/keeper/gateway_test.go index b61928b70..b343591a0 100644 --- a/x/gateway/keeper/gateway_test.go +++ b/x/gateway/keeper/gateway_test.go @@ -4,11 +4,11 @@ import ( "strconv" "testing" - "pocket/cmd/pocketd/cmd" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" diff --git a/x/gateway/keeper/keeper.go b/x/gateway/keeper/keeper.go index ea74f170b..fcc143222 100644 --- a/x/gateway/keeper/keeper.go +++ b/x/gateway/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) type ( diff --git a/x/gateway/keeper/msg_server.go b/x/gateway/keeper/msg_server.go index 9c9e6c757..fafeff27b 100644 --- a/x/gateway/keeper/msg_server.go +++ b/x/gateway/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) type msgServer struct { diff --git a/x/gateway/keeper/msg_server_stake_gateway.go b/x/gateway/keeper/msg_server_stake_gateway.go index a1a893d41..919e0f4dd 100644 --- a/x/gateway/keeper/msg_server_stake_gateway.go +++ b/x/gateway/keeper/msg_server_stake_gateway.go @@ -6,7 +6,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) func (k msgServer) StakeGateway( diff --git a/x/gateway/keeper/msg_server_stake_gateway_test.go b/x/gateway/keeper/msg_server_stake_gateway_test.go index 92c787aef..597cc76d2 100644 --- a/x/gateway/keeper/msg_server_stake_gateway_test.go +++ b/x/gateway/keeper/msg_server_stake_gateway_test.go @@ -6,10 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestMsgServer_StakeGateway_SuccessfulCreateAndUpdate(t *testing.T) { diff --git a/x/gateway/keeper/msg_server_test.go b/x/gateway/keeper/msg_server_test.go index fc97c41e2..598dda0e5 100644 --- a/x/gateway/keeper/msg_server_test.go +++ b/x/gateway/keeper/msg_server_test.go @@ -6,9 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/gateway/keeper/msg_server_unstake_gateway.go b/x/gateway/keeper/msg_server_unstake_gateway.go index b27f89dfd..16c913693 100644 --- a/x/gateway/keeper/msg_server_unstake_gateway.go +++ b/x/gateway/keeper/msg_server_unstake_gateway.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) // TODO_TECHDEBT(#49): Add un-delegation from delegated apps diff --git a/x/gateway/keeper/msg_server_unstake_gateway_test.go b/x/gateway/keeper/msg_server_unstake_gateway_test.go index e4d7e5e4d..421316b78 100644 --- a/x/gateway/keeper/msg_server_unstake_gateway_test.go +++ b/x/gateway/keeper/msg_server_unstake_gateway_test.go @@ -6,10 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestMsgServer_UnstakeGateway_Success(t *testing.T) { diff --git a/x/gateway/keeper/params.go b/x/gateway/keeper/params.go index d4fc473ed..e16780bc5 100644 --- a/x/gateway/keeper/params.go +++ b/x/gateway/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/types" ) // GetParams get all parameters as types.Params diff --git a/x/gateway/keeper/params_test.go b/x/gateway/keeper/params_test.go index 171bb5d30..748d837cf 100644 --- a/x/gateway/keeper/params_test.go +++ b/x/gateway/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/gateway/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestGetParams(t *testing.T) { diff --git a/x/gateway/keeper/query.go b/x/gateway/keeper/query.go index 2b7887a31..ffa80d00d 100644 --- a/x/gateway/keeper/query.go +++ b/x/gateway/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/gateway/keeper/query_gateway.go b/x/gateway/keeper/query_gateway.go index 0cd5e651d..bd0576a2d 100644 --- a/x/gateway/keeper/query_gateway.go +++ b/x/gateway/keeper/query_gateway.go @@ -8,7 +8,8 @@ import ( "github.com/cosmos/cosmos-sdk/types/query" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/types" ) func (k Keeper) GatewayAll(goCtx context.Context, req *types.QueryAllGatewayRequest) (*types.QueryAllGatewayResponse, error) { diff --git a/x/gateway/keeper/query_gateway_test.go b/x/gateway/keeper/query_gateway_test.go index 3597a6965..9d44de980 100644 --- a/x/gateway/keeper/query_gateway_test.go +++ b/x/gateway/keeper/query_gateway_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/gateway/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/gateway/types" ) // Prevent strconv unused error diff --git a/x/gateway/keeper/query_params.go b/x/gateway/keeper/query_params.go index 188c71c02..8d3afe26d 100644 --- a/x/gateway/keeper/query_params.go +++ b/x/gateway/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/gateway/keeper/query_params_test.go b/x/gateway/keeper/query_params_test.go index b2fa1f57d..062f049ff 100644 --- a/x/gateway/keeper/query_params_test.go +++ b/x/gateway/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/gateway/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/gateway/module.go b/x/gateway/module.go index e1aacb5d9..86f16fb7c 100644 --- a/x/gateway/module.go +++ b/x/gateway/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/gateway/client/cli" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/client/cli" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) var ( diff --git a/x/gateway/module_simulation.go b/x/gateway/module_simulation.go index e6eac7056..0aab36ae9 100644 --- a/x/gateway/module_simulation.go +++ b/x/gateway/module_simulation.go @@ -3,9 +3,9 @@ package gateway import ( "math/rand" - "pocket/testutil/sample" - gatewaysimulation "pocket/x/gateway/simulation" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/sample" + gatewaysimulation "github.com/pokt-network/poktroll/x/gateway/simulation" + "github.com/pokt-network/poktroll/x/gateway/types" "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/gateway/simulation/stake_gateway.go b/x/gateway/simulation/stake_gateway.go index 986035de2..9a76e82fc 100644 --- a/x/gateway/simulation/stake_gateway.go +++ b/x/gateway/simulation/stake_gateway.go @@ -7,8 +7,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func SimulateMsgStakeGateway( diff --git a/x/gateway/simulation/unstake_gateway.go b/x/gateway/simulation/unstake_gateway.go index aaf9c3543..e82ebf748 100644 --- a/x/gateway/simulation/unstake_gateway.go +++ b/x/gateway/simulation/unstake_gateway.go @@ -6,8 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func SimulateMsgUnstakeGateway( diff --git a/x/gateway/types/genesis_test.go b/x/gateway/types/genesis_test.go index e08ce6f13..ea1e6cdf8 100644 --- a/x/gateway/types/genesis_test.go +++ b/x/gateway/types/genesis_test.go @@ -3,8 +3,8 @@ package types_test import ( "testing" - "pocket/testutil/sample" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/gateway/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" diff --git a/x/gateway/types/message_stake_gateway_test.go b/x/gateway/types/message_stake_gateway_test.go index 584a08ad7..94299afaa 100644 --- a/x/gateway/types/message_stake_gateway_test.go +++ b/x/gateway/types/message_stake_gateway_test.go @@ -6,7 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) func TestMsgStakeGateway_ValidateBasic(t *testing.T) { diff --git a/x/gateway/types/message_unstake_gateway_test.go b/x/gateway/types/message_unstake_gateway_test.go index ded54f1a5..759aa11d7 100644 --- a/x/gateway/types/message_unstake_gateway_test.go +++ b/x/gateway/types/message_unstake_gateway_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) func TestMsgUnstakeGateway_ValidateBasic(t *testing.T) { diff --git a/x/pocket/client/cli/query.go b/x/pocket/client/cli/query.go index d4206d0e4..7fb94d626 100644 --- a/x/pocket/client/cli/query.go +++ b/x/pocket/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/pocket/client/cli/query_params.go b/x/pocket/client/cli/query_params.go index c1c31f62a..8461c3c60 100644 --- a/x/pocket/client/cli/query_params.go +++ b/x/pocket/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/pocket/client/cli/tx.go b/x/pocket/client/cli/tx.go index 06b2b0eb0..70032b9b8 100644 --- a/x/pocket/client/cli/tx.go +++ b/x/pocket/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) var ( diff --git a/x/pocket/genesis.go b/x/pocket/genesis.go index 23992c930..1a6b8738d 100644 --- a/x/pocket/genesis.go +++ b/x/pocket/genesis.go @@ -2,8 +2,9 @@ package pocket import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/pocket/keeper" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/pocket/genesis_test.go b/x/pocket/genesis_test.go index 8da506367..8e5f5ba18 100644 --- a/x/pocket/genesis_test.go +++ b/x/pocket/genesis_test.go @@ -4,10 +4,11 @@ import ( "testing" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/pocket" - "pocket/x/pocket/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/pocket" + "github.com/pokt-network/poktroll/x/pocket/types" ) func TestGenesis(t *testing.T) { diff --git a/x/pocket/keeper/keeper.go b/x/pocket/keeper/keeper.go index b5fb8a2be..44114c392 100644 --- a/x/pocket/keeper/keeper.go +++ b/x/pocket/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) type ( diff --git a/x/pocket/keeper/msg_server.go b/x/pocket/keeper/msg_server.go index 1916656f5..310ebb2e5 100644 --- a/x/pocket/keeper/msg_server.go +++ b/x/pocket/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) type msgServer struct { diff --git a/x/pocket/keeper/msg_server_test.go b/x/pocket/keeper/msg_server_test.go index cc89704f3..e27eda21b 100644 --- a/x/pocket/keeper/msg_server_test.go +++ b/x/pocket/keeper/msg_server_test.go @@ -6,9 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/pocket/keeper" - "pocket/x/pocket/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/pocket/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/pocket/keeper/params.go b/x/pocket/keeper/params.go index 8527fa7c9..b4b197932 100644 --- a/x/pocket/keeper/params.go +++ b/x/pocket/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/types" ) // GetParams get all parameters as types.Params diff --git a/x/pocket/keeper/params_test.go b/x/pocket/keeper/params_test.go index feb90768d..06afe192c 100644 --- a/x/pocket/keeper/params_test.go +++ b/x/pocket/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/pocket/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) func TestGetParams(t *testing.T) { diff --git a/x/pocket/keeper/query.go b/x/pocket/keeper/query.go index dd2416549..24adda2bb 100644 --- a/x/pocket/keeper/query.go +++ b/x/pocket/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/pocket/keeper/query_params.go b/x/pocket/keeper/query_params.go index b3af6fbbd..3fdff1051 100644 --- a/x/pocket/keeper/query_params.go +++ b/x/pocket/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/pocket/keeper/query_params_test.go b/x/pocket/keeper/query_params_test.go index a91d8d93a..d40a517f4 100644 --- a/x/pocket/keeper/query_params_test.go +++ b/x/pocket/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/pocket/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/pocket/module.go b/x/pocket/module.go index 83daf8b91..684ea1d95 100644 --- a/x/pocket/module.go +++ b/x/pocket/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/pocket/client/cli" - "pocket/x/pocket/keeper" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/client/cli" + "github.com/pokt-network/poktroll/x/pocket/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) var ( diff --git a/x/pocket/module_simulation.go b/x/pocket/module_simulation.go index 0b5db9a81..5d5746526 100644 --- a/x/pocket/module_simulation.go +++ b/x/pocket/module_simulation.go @@ -8,9 +8,10 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "pocket/testutil/sample" - pocketsimulation "pocket/x/pocket/simulation" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/testutil/sample" + pocketsimulation "github.com/pokt-network/poktroll/x/pocket/simulation" + "github.com/pokt-network/poktroll/x/pocket/types" ) // avoid unused import issue diff --git a/x/pocket/types/genesis_test.go b/x/pocket/types/genesis_test.go index d09e041a2..c2eb457a8 100644 --- a/x/pocket/types/genesis_test.go +++ b/x/pocket/types/genesis_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/types" ) func TestGenesisState_Validate(t *testing.T) { diff --git a/x/service/client/cli/query.go b/x/service/client/cli/query.go index 9d7cd5323..f52a37c7a 100644 --- a/x/service/client/cli/query.go +++ b/x/service/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/service/client/cli/query_params.go b/x/service/client/cli/query_params.go index 18bf13450..cd99b673f 100644 --- a/x/service/client/cli/query_params.go +++ b/x/service/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/service/client/cli/tx.go b/x/service/client/cli/tx.go index e5c1c6ef5..b4c63f360 100644 --- a/x/service/client/cli/tx.go +++ b/x/service/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) var ( diff --git a/x/service/genesis.go b/x/service/genesis.go index d46f97436..a0c3211ba 100644 --- a/x/service/genesis.go +++ b/x/service/genesis.go @@ -2,8 +2,9 @@ package service import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/service/keeper" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/service/genesis_test.go b/x/service/genesis_test.go index a1ff0b888..4eed97699 100644 --- a/x/service/genesis_test.go +++ b/x/service/genesis_test.go @@ -4,10 +4,11 @@ import ( "testing" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/service" - "pocket/x/service/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/service" + "github.com/pokt-network/poktroll/x/service/types" ) func TestGenesis(t *testing.T) { diff --git a/x/service/keeper/keeper.go b/x/service/keeper/keeper.go index f81d8d56c..fb4409d2f 100644 --- a/x/service/keeper/keeper.go +++ b/x/service/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) type ( diff --git a/x/service/keeper/msg_server.go b/x/service/keeper/msg_server.go index 422d49a12..e5f891b01 100644 --- a/x/service/keeper/msg_server.go +++ b/x/service/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) type msgServer struct { diff --git a/x/service/keeper/msg_server_test.go b/x/service/keeper/msg_server_test.go index a9d15f912..78c80d8e6 100644 --- a/x/service/keeper/msg_server_test.go +++ b/x/service/keeper/msg_server_test.go @@ -6,9 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/service/keeper" - "pocket/x/service/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/service/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/service/keeper/params.go b/x/service/keeper/params.go index 34a1a742c..948fec7a1 100644 --- a/x/service/keeper/params.go +++ b/x/service/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/types" ) // GetParams get all parameters as types.Params diff --git a/x/service/keeper/params_test.go b/x/service/keeper/params_test.go index 3a7b4aee3..097742cd1 100644 --- a/x/service/keeper/params_test.go +++ b/x/service/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/service/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) func TestGetParams(t *testing.T) { diff --git a/x/service/keeper/query.go b/x/service/keeper/query.go index a0bfe1122..ac3116f9a 100644 --- a/x/service/keeper/query.go +++ b/x/service/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/service/keeper/query_params.go b/x/service/keeper/query_params.go index 6068e16c6..c04f32dd9 100644 --- a/x/service/keeper/query_params.go +++ b/x/service/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/service/keeper/query_params_test.go b/x/service/keeper/query_params_test.go index 239a6799c..243a9ed80 100644 --- a/x/service/keeper/query_params_test.go +++ b/x/service/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/service/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/service/module.go b/x/service/module.go index 4a02e77d5..a7ebd35f2 100644 --- a/x/service/module.go +++ b/x/service/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/service/client/cli" - "pocket/x/service/keeper" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/client/cli" + "github.com/pokt-network/poktroll/x/service/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) var ( diff --git a/x/service/module_simulation.go b/x/service/module_simulation.go index e0d2b39e1..f449b4ce4 100644 --- a/x/service/module_simulation.go +++ b/x/service/module_simulation.go @@ -8,9 +8,10 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "pocket/testutil/sample" - servicesimulation "pocket/x/service/simulation" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/testutil/sample" + servicesimulation "github.com/pokt-network/poktroll/x/service/simulation" + "github.com/pokt-network/poktroll/x/service/types" ) // avoid unused import issue diff --git a/x/service/types/genesis_test.go b/x/service/types/genesis_test.go index 99eafc91c..77e357f78 100644 --- a/x/service/types/genesis_test.go +++ b/x/service/types/genesis_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/types" ) func TestGenesisState_Validate(t *testing.T) { diff --git a/x/session/client/cli/query.go b/x/session/client/cli/query.go index 3b4376f72..cd6bf6f70 100644 --- a/x/session/client/cli/query.go +++ b/x/session/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/session/client/cli/query_get_session.go b/x/session/client/cli/query_get_session.go index deebf1a31..1c21aa881 100644 --- a/x/session/client/cli/query_get_session.go +++ b/x/session/client/cli/query_get_session.go @@ -7,7 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) var _ = strconv.Itoa(0) diff --git a/x/session/client/cli/query_params.go b/x/session/client/cli/query_params.go index 9139ba693..5f8aa0609 100644 --- a/x/session/client/cli/query_params.go +++ b/x/session/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/session/client/cli/tx.go b/x/session/client/cli/tx.go index 5bd4f72aa..248ae5237 100644 --- a/x/session/client/cli/tx.go +++ b/x/session/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) var ( diff --git a/x/session/genesis.go b/x/session/genesis.go index 6603b6d87..a14b12f31 100644 --- a/x/session/genesis.go +++ b/x/session/genesis.go @@ -2,8 +2,9 @@ package session import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/session/keeper" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/session/genesis_test.go b/x/session/genesis_test.go index afedb7434..50298ada9 100644 --- a/x/session/genesis_test.go +++ b/x/session/genesis_test.go @@ -4,10 +4,11 @@ import ( "testing" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/session" - "pocket/x/session/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/session" + "github.com/pokt-network/poktroll/x/session/types" ) func TestGenesis(t *testing.T) { diff --git a/x/session/keeper/keeper.go b/x/session/keeper/keeper.go index 4515d5a1e..292890964 100644 --- a/x/session/keeper/keeper.go +++ b/x/session/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) type ( diff --git a/x/session/keeper/msg_server.go b/x/session/keeper/msg_server.go index 7dcf714c9..6dbe55462 100644 --- a/x/session/keeper/msg_server.go +++ b/x/session/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) type msgServer struct { diff --git a/x/session/keeper/msg_server_test.go b/x/session/keeper/msg_server_test.go index 61c82603a..b00a56aba 100644 --- a/x/session/keeper/msg_server_test.go +++ b/x/session/keeper/msg_server_test.go @@ -7,9 +7,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/session/keeper" - "pocket/x/session/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/session/keeper/params.go b/x/session/keeper/params.go index 37247b35e..142887657 100644 --- a/x/session/keeper/params.go +++ b/x/session/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/x/session/types" ) // GetParams get all parameters as types.Params diff --git a/x/session/keeper/params_test.go b/x/session/keeper/params_test.go index 562c4afd7..bf020e294 100644 --- a/x/session/keeper/params_test.go +++ b/x/session/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/session/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) func TestGetParams(t *testing.T) { diff --git a/x/session/keeper/query.go b/x/session/keeper/query.go index 7add9b6e4..700ec87e8 100644 --- a/x/session/keeper/query.go +++ b/x/session/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/session/keeper/query_get_session.go b/x/session/keeper/query_get_session.go index f937f5033..d9fd3deaf 100644 --- a/x/session/keeper/query_get_session.go +++ b/x/session/keeper/query_get_session.go @@ -7,7 +7,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) func (k Keeper) GetSession(goCtx context.Context, req *types.QueryGetSessionRequest) (*types.QueryGetSessionResponse, error) { diff --git a/x/session/keeper/query_get_session_test.go b/x/session/keeper/query_get_session_test.go index b18bc830c..5f15a94e9 100644 --- a/x/session/keeper/query_get_session_test.go +++ b/x/session/keeper/query_get_session_test.go @@ -6,10 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/cmd/pocketd/cmd" - keepertest "pocket/testutil/keeper" - "pocket/x/session/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func init() { diff --git a/x/session/keeper/query_params.go b/x/session/keeper/query_params.go index 9a6775da2..75734ad7a 100644 --- a/x/session/keeper/query_params.go +++ b/x/session/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/x/session/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/session/keeper/query_params_test.go b/x/session/keeper/query_params_test.go index c7ff9b68a..85f0ddd9b 100644 --- a/x/session/keeper/query_params_test.go +++ b/x/session/keeper/query_params_test.go @@ -6,8 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/session/types" + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/session/keeper/session_hydrator.go b/x/session/keeper/session_hydrator.go index 7f4e45afb..b6e78e2c8 100644 --- a/x/session/keeper/session_hydrator.go +++ b/x/session/keeper/session_hydrator.go @@ -11,8 +11,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" _ "golang.org/x/crypto/sha3" - "pocket/x/session/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) var SHA3HashLen = crypto.SHA3_256.Size() diff --git a/x/session/keeper/session_hydrator_test.go b/x/session/keeper/session_hydrator_test.go index 7a1c10a19..c50d653ab 100644 --- a/x/session/keeper/session_hydrator_test.go +++ b/x/session/keeper/session_hydrator_test.go @@ -5,10 +5,10 @@ import ( "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/session/keeper" - "pocket/x/session/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) func TestSession_HydrateSession_Success_BaseCase(t *testing.T) { diff --git a/x/session/module.go b/x/session/module.go index e6131b51f..990ada0c0 100644 --- a/x/session/module.go +++ b/x/session/module.go @@ -14,9 +14,9 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" - "pocket/x/session/client/cli" - "pocket/x/session/keeper" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/client/cli" + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) var ( diff --git a/x/session/module_simulation.go b/x/session/module_simulation.go index 9f75aea86..befedd421 100644 --- a/x/session/module_simulation.go +++ b/x/session/module_simulation.go @@ -8,9 +8,10 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "pocket/testutil/sample" - sessionsimulation "pocket/x/session/simulation" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/testutil/sample" + sessionsimulation "github.com/pokt-network/poktroll/x/session/simulation" + "github.com/pokt-network/poktroll/x/session/types" ) // avoid unused import issue diff --git a/x/session/types/expected_keepers.go b/x/session/types/expected_keepers.go index 1bbae52a1..697528f21 100644 --- a/x/session/types/expected_keepers.go +++ b/x/session/types/expected_keepers.go @@ -6,8 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" - apptypes "pocket/x/application/types" - sharedtypes "pocket/x/shared/types" + apptypes "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // AccountKeeper defines the expected account keeper used for simulations (noalias) diff --git a/x/session/types/genesis_test.go b/x/session/types/genesis_test.go index e435341ae..97252a21e 100644 --- a/x/session/types/genesis_test.go +++ b/x/session/types/genesis_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/x/session/types" ) func TestGenesisState_Validate(t *testing.T) { diff --git a/x/shared/helpers/service_configs.go b/x/shared/helpers/service_configs.go index 9955a4d5f..6884da7ae 100644 --- a/x/shared/helpers/service_configs.go +++ b/x/shared/helpers/service_configs.go @@ -3,7 +3,7 @@ package helpers import ( "fmt" - sharedtypes "pocket/x/shared/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // ValidateAppServiceConfigs returns an error if any of the application service configs are invalid diff --git a/x/supplier/client/cli/helpers_test.go b/x/supplier/client/cli/helpers_test.go index 09f193920..d6066c28a 100644 --- a/x/supplier/client/cli/helpers_test.go +++ b/x/supplier/client/cli/helpers_test.go @@ -7,10 +7,10 @@ import ( "github.com/stretchr/testify/require" - "pocket/cmd/pocketd/cmd" - "pocket/testutil/network" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + "github.com/pokt-network/poktroll/testutil/network" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // Dummy variable to avoid unused import error. diff --git a/x/supplier/client/cli/query.go b/x/supplier/client/cli/query.go index b5e1e4bf4..da1e3de17 100644 --- a/x/supplier/client/cli/query.go +++ b/x/supplier/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/supplier/client/cli/query_params.go b/x/supplier/client/cli/query_params.go index d308b3e96..339dbcf00 100644 --- a/x/supplier/client/cli/query_params.go +++ b/x/supplier/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/supplier/client/cli/query_supplier.go b/x/supplier/client/cli/query_supplier.go index 44adb2c8e..cfbc6b4ec 100644 --- a/x/supplier/client/cli/query_supplier.go +++ b/x/supplier/client/cli/query_supplier.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func CmdListSupplier() *cobra.Command { diff --git a/x/supplier/client/cli/query_supplier_test.go b/x/supplier/client/cli/query_supplier_test.go index 4dbc5ed36..378811a2f 100644 --- a/x/supplier/client/cli/query_supplier_test.go +++ b/x/supplier/client/cli/query_supplier_test.go @@ -12,10 +12,10 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/testutil/nullify" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/client/cli" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/nullify" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/client/cli" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestShowSupplier(t *testing.T) { diff --git a/x/supplier/client/cli/tx.go b/x/supplier/client/cli/tx.go index c1059e51d..c76c24dae 100644 --- a/x/supplier/client/cli/tx.go +++ b/x/supplier/client/cli/tx.go @@ -7,7 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/spf13/cobra" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) var ( diff --git a/x/supplier/client/cli/tx_create_claim.go b/x/supplier/client/cli/tx_create_claim.go index b611e9795..db951891f 100644 --- a/x/supplier/client/cli/tx_create_claim.go +++ b/x/supplier/client/cli/tx_create_claim.go @@ -5,13 +5,14 @@ import ( "strconv" "encoding/json" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - sessiontypes "pocket/x/session/types" - "pocket/x/supplier/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // TODO(@bryanchriswhite): Add unit tests for the CLI command when implementing the business logic. diff --git a/x/supplier/client/cli/tx_stake_supplier.go b/x/supplier/client/cli/tx_stake_supplier.go index 7ea78e077..f223799cf 100644 --- a/x/supplier/client/cli/tx_stake_supplier.go +++ b/x/supplier/client/cli/tx_stake_supplier.go @@ -11,8 +11,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/cobra" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) var _ = strconv.Itoa(0) diff --git a/x/supplier/client/cli/tx_stake_supplier_test.go b/x/supplier/client/cli/tx_stake_supplier_test.go index f6de5641e..f61d41fc9 100644 --- a/x/supplier/client/cli/tx_stake_supplier_test.go +++ b/x/supplier/client/cli/tx_stake_supplier_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/supplier/client/cli" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/supplier/client/cli" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestCLI_StakeSupplier(t *testing.T) { diff --git a/x/supplier/client/cli/tx_submit_proof.go b/x/supplier/client/cli/tx_submit_proof.go index 798d67492..64c498026 100644 --- a/x/supplier/client/cli/tx_submit_proof.go +++ b/x/supplier/client/cli/tx_submit_proof.go @@ -5,13 +5,14 @@ import ( "strconv" "encoding/json" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - sessiontypes "pocket/x/session/types" - "pocket/x/supplier/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) var _ = strconv.Itoa(0) diff --git a/x/supplier/client/cli/tx_unstake_supplier.go b/x/supplier/client/cli/tx_unstake_supplier.go index 7f3ea3003..40ac4a83f 100644 --- a/x/supplier/client/cli/tx_unstake_supplier.go +++ b/x/supplier/client/cli/tx_unstake_supplier.go @@ -6,7 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func CmdUnstakeSupplier() *cobra.Command { diff --git a/x/supplier/client/cli/tx_unstake_supplier_test.go b/x/supplier/client/cli/tx_unstake_supplier_test.go index 9956be635..179e5dce5 100644 --- a/x/supplier/client/cli/tx_unstake_supplier_test.go +++ b/x/supplier/client/cli/tx_unstake_supplier_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/supplier/client/cli" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/supplier/client/cli" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestCLI_UnstakeSupplier(t *testing.T) { diff --git a/x/supplier/genesis.go b/x/supplier/genesis.go index 889f6d197..fb7d59806 100644 --- a/x/supplier/genesis.go +++ b/x/supplier/genesis.go @@ -3,8 +3,8 @@ package supplier import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/supplier/genesis_test.go b/x/supplier/genesis_test.go index 9bae65111..b6af0545c 100644 --- a/x/supplier/genesis_test.go +++ b/x/supplier/genesis_test.go @@ -6,12 +6,12 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/testutil/sample" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier" - "pocket/x/supplier/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier" + "github.com/pokt-network/poktroll/x/supplier/types" ) // Please see `x/supplier/types/genesis_test.go` for extensive tests related to the validity of the genesis state. diff --git a/x/supplier/keeper/keeper.go b/x/supplier/keeper/keeper.go index cde9ba5e3..d77218231 100644 --- a/x/supplier/keeper/keeper.go +++ b/x/supplier/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) type ( diff --git a/x/supplier/keeper/msg_server.go b/x/supplier/keeper/msg_server.go index e117ef1d9..83879071c 100644 --- a/x/supplier/keeper/msg_server.go +++ b/x/supplier/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) type msgServer struct { diff --git a/x/supplier/keeper/msg_server_create_claim.go b/x/supplier/keeper/msg_server_create_claim.go index 25a1ea1e8..752da7343 100644 --- a/x/supplier/keeper/msg_server_create_claim.go +++ b/x/supplier/keeper/msg_server_create_claim.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k msgServer) CreateClaim(goCtx context.Context, msg *types.MsgCreateClaim) (*types.MsgCreateClaimResponse, error) { diff --git a/x/supplier/keeper/msg_server_stake_supplier.go b/x/supplier/keeper/msg_server_stake_supplier.go index 978bd71b9..5fea47135 100644 --- a/x/supplier/keeper/msg_server_stake_supplier.go +++ b/x/supplier/keeper/msg_server_stake_supplier.go @@ -6,8 +6,8 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k msgServer) StakeSupplier( diff --git a/x/supplier/keeper/msg_server_stake_supplier_test.go b/x/supplier/keeper/msg_server_stake_supplier_test.go index 5b5949fe5..7ac0ee031 100644 --- a/x/supplier/keeper/msg_server_stake_supplier_test.go +++ b/x/supplier/keeper/msg_server_stake_supplier_test.go @@ -6,11 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) { diff --git a/x/supplier/keeper/msg_server_submit_proof.go b/x/supplier/keeper/msg_server_submit_proof.go index cd8f104d1..2715b7c8d 100644 --- a/x/supplier/keeper/msg_server_submit_proof.go +++ b/x/supplier/keeper/msg_server_submit_proof.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k msgServer) SubmitProof(goCtx context.Context, msg *types.MsgSubmitProof) (*types.MsgSubmitProofResponse, error) { diff --git a/x/supplier/keeper/msg_server_test.go b/x/supplier/keeper/msg_server_test.go index 2ca2981e7..7e4d01f27 100644 --- a/x/supplier/keeper/msg_server_test.go +++ b/x/supplier/keeper/msg_server_test.go @@ -6,9 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/supplier/keeper/msg_server_unstake_supplier.go b/x/supplier/keeper/msg_server_unstake_supplier.go index b1028f278..830a4c37a 100644 --- a/x/supplier/keeper/msg_server_unstake_supplier.go +++ b/x/supplier/keeper/msg_server_unstake_supplier.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // TODO(#73): Determine if an application needs an unbonding period after unstaking. diff --git a/x/supplier/keeper/msg_server_unstake_supplier_test.go b/x/supplier/keeper/msg_server_unstake_supplier_test.go index 57e4dcdc0..163cbe5ae 100644 --- a/x/supplier/keeper/msg_server_unstake_supplier_test.go +++ b/x/supplier/keeper/msg_server_unstake_supplier_test.go @@ -6,11 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestMsgServer_UnstakeSupplier_Success(t *testing.T) { diff --git a/x/supplier/keeper/params.go b/x/supplier/keeper/params.go index 9c24f3acc..86ca01cbe 100644 --- a/x/supplier/keeper/params.go +++ b/x/supplier/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/types" ) // GetParams get all parameters as types.Params diff --git a/x/supplier/keeper/params_test.go b/x/supplier/keeper/params_test.go index 80fb79d25..5a7e866fe 100644 --- a/x/supplier/keeper/params_test.go +++ b/x/supplier/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/supplier/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestGetParams(t *testing.T) { diff --git a/x/supplier/keeper/query.go b/x/supplier/keeper/query.go index 87e49fbc5..2d1d5c18e 100644 --- a/x/supplier/keeper/query.go +++ b/x/supplier/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/supplier/keeper/query_params.go b/x/supplier/keeper/query_params.go index dbadba386..67e2a17f4 100644 --- a/x/supplier/keeper/query_params.go +++ b/x/supplier/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/supplier/keeper/query_params_test.go b/x/supplier/keeper/query_params_test.go index 831196046..d9b909305 100644 --- a/x/supplier/keeper/query_params_test.go +++ b/x/supplier/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/supplier/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/supplier/keeper/query_supplier.go b/x/supplier/keeper/query_supplier.go index 193b0fb5e..14d5afab5 100644 --- a/x/supplier/keeper/query_supplier.go +++ b/x/supplier/keeper/query_supplier.go @@ -9,8 +9,8 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k Keeper) SupplierAll(goCtx context.Context, req *types.QueryAllSupplierRequest) (*types.QueryAllSupplierResponse, error) { diff --git a/x/supplier/keeper/query_supplier_test.go b/x/supplier/keeper/query_supplier_test.go index aa9095f79..6690e3f75 100644 --- a/x/supplier/keeper/query_supplier_test.go +++ b/x/supplier/keeper/query_supplier_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/supplier/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/supplier/types" ) // Prevent strconv unused error diff --git a/x/supplier/keeper/supplier.go b/x/supplier/keeper/supplier.go index 68c800c24..0c31db2be 100644 --- a/x/supplier/keeper/supplier.go +++ b/x/supplier/keeper/supplier.go @@ -4,8 +4,8 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // SetSupplier set a specific supplier in the store from its index diff --git a/x/supplier/keeper/supplier_test.go b/x/supplier/keeper/supplier_test.go index fef1f8645..c02c5758e 100644 --- a/x/supplier/keeper/supplier_test.go +++ b/x/supplier/keeper/supplier_test.go @@ -9,13 +9,13 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/require" - "pocket/cmd/pocketd/cmd" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/testutil/sample" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) // Prevent strconv unused error diff --git a/x/supplier/module.go b/x/supplier/module.go index 421271406..a39dc9303 100644 --- a/x/supplier/module.go +++ b/x/supplier/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/supplier/client/cli" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/client/cli" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) var ( diff --git a/x/supplier/module_simulation.go b/x/supplier/module_simulation.go index 6f0e01ba5..eb30e7dd3 100644 --- a/x/supplier/module_simulation.go +++ b/x/supplier/module_simulation.go @@ -3,9 +3,9 @@ package supplier import ( "math/rand" - "pocket/testutil/sample" - suppliersimulation "pocket/x/supplier/simulation" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/sample" + suppliersimulation "github.com/pokt-network/poktroll/x/supplier/simulation" + "github.com/pokt-network/poktroll/x/supplier/types" "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/supplier/simulation/create_claim.go b/x/supplier/simulation/create_claim.go index 60d3ecff0..cae471c06 100644 --- a/x/supplier/simulation/create_claim.go +++ b/x/supplier/simulation/create_claim.go @@ -6,8 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SimulateMsgCreateClaim( diff --git a/x/supplier/simulation/stake_supplier.go b/x/supplier/simulation/stake_supplier.go index 103e321bd..95fb7e8d6 100644 --- a/x/supplier/simulation/stake_supplier.go +++ b/x/supplier/simulation/stake_supplier.go @@ -7,8 +7,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SimulateMsgStakeSupplier( diff --git a/x/supplier/simulation/submit_proof.go b/x/supplier/simulation/submit_proof.go index 8996997d6..3473f1c7b 100644 --- a/x/supplier/simulation/submit_proof.go +++ b/x/supplier/simulation/submit_proof.go @@ -6,8 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SimulateMsgSubmitProof( diff --git a/x/supplier/simulation/unstake_supplier.go b/x/supplier/simulation/unstake_supplier.go index c37290835..3955b25db 100644 --- a/x/supplier/simulation/unstake_supplier.go +++ b/x/supplier/simulation/unstake_supplier.go @@ -6,8 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SimulateMsgUnstakeSupplier( diff --git a/x/supplier/types/genesis.go b/x/supplier/types/genesis.go index d4ae794c6..2f4ff1872 100644 --- a/x/supplier/types/genesis.go +++ b/x/supplier/types/genesis.go @@ -6,8 +6,8 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - servicehelpers "pocket/x/shared/helpers" - sharedtypes "pocket/x/shared/types" + servicehelpers "github.com/pokt-network/poktroll/x/shared/helpers" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // DefaultIndex is the default global index diff --git a/x/supplier/types/genesis_test.go b/x/supplier/types/genesis_test.go index be0988fa6..ba806c98b 100644 --- a/x/supplier/types/genesis_test.go +++ b/x/supplier/types/genesis_test.go @@ -6,9 +6,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestGenesisState_Validate(t *testing.T) { diff --git a/x/supplier/types/message_create_claim.go b/x/supplier/types/message_create_claim.go index 4bcfada3b..7d68c6a94 100644 --- a/x/supplier/types/message_create_claim.go +++ b/x/supplier/types/message_create_claim.go @@ -4,7 +4,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sessiontypes "pocket/x/session/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" ) const TypeMsgCreateClaim = "create_claim" diff --git a/x/supplier/types/message_create_claim_test.go b/x/supplier/types/message_create_claim_test.go index c10e3a4c9..8401c0d3d 100644 --- a/x/supplier/types/message_create_claim_test.go +++ b/x/supplier/types/message_create_claim_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) // TODO(@bryanchriswhite): Add unit tests for message validation when adding the business logic. diff --git a/x/supplier/types/message_stake_supplier.go b/x/supplier/types/message_stake_supplier.go index 02c6dd164..7d1dbde07 100644 --- a/x/supplier/types/message_stake_supplier.go +++ b/x/supplier/types/message_stake_supplier.go @@ -5,8 +5,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/types" - servicehelpers "pocket/x/shared/helpers" - sharedtypes "pocket/x/shared/types" + servicehelpers "github.com/pokt-network/poktroll/x/shared/helpers" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) const TypeMsgStakeSupplier = "stake_supplier" diff --git a/x/supplier/types/message_stake_supplier_test.go b/x/supplier/types/message_stake_supplier_test.go index 76d8ba6d4..57ce3e4fd 100644 --- a/x/supplier/types/message_stake_supplier_test.go +++ b/x/supplier/types/message_stake_supplier_test.go @@ -6,8 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // TODO_CLEANUP: This test has a lot of copy-pasted code from test to test. diff --git a/x/supplier/types/message_submit_proof.go b/x/supplier/types/message_submit_proof.go index 32faa796b..ad00eb225 100644 --- a/x/supplier/types/message_submit_proof.go +++ b/x/supplier/types/message_submit_proof.go @@ -4,7 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - sessiontypes "pocket/x/session/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" ) const TypeMsgSubmitProof = "submit_proof" diff --git a/x/supplier/types/message_submit_proof_test.go b/x/supplier/types/message_submit_proof_test.go index 7f7a15862..8479db05d 100644 --- a/x/supplier/types/message_submit_proof_test.go +++ b/x/supplier/types/message_submit_proof_test.go @@ -5,7 +5,8 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/stretchr/testify/require" - "pocket/testutil/sample" + + "github.com/pokt-network/poktroll/testutil/sample" ) // TODO(@bryanchriswhite): Add unit tests for message validation when adding the business logic. diff --git a/x/supplier/types/message_unstake_supplier_test.go b/x/supplier/types/message_unstake_supplier_test.go index cc2481bbb..b447397ed 100644 --- a/x/supplier/types/message_unstake_supplier_test.go +++ b/x/supplier/types/message_unstake_supplier_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) func TestMsgUnstakeSupplier_ValidateBasic(t *testing.T) { From fe95b82e96d3d3de23dc8976f6d57fe937e70e89 Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Thu, 2 Nov 2023 15:16:27 -0700 Subject: [PATCH 22/28] Update README.md Add some links to more docs. --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 659bd96a6..a6b55c5ad 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ **poktroll** is a rollup built using [Rollkit](https://rollkit.dev/), [Cosmos SDK](https://docs.cosmos.network) and [CometBFT](https://cometbft.com/), created with [Ignite CLI](https://ignite.com/cli) for the Shannon upgrade of the [Pocket Network](https://pokt.network) blockchain. +- [Where are the docs?](#where-are-the-docs) + - [Roadmap](#roadmap) + - [Godoc](#godoc) + - [Pocket V1 (Shannon) Docs](#pocket-v1-shannon-docs) - [Getting Started](#getting-started) - [Makefile](#makefile) - [Development](#development) @@ -9,7 +13,17 @@ ## Where are the docs? -_This repository is still young & early._ +_This repository is still young & early. We're focusing on development right now._ + +### Roadmap + +You can find our Roadmap Changelog [here](https://github.com/pokt-network/poktroll/blob/main/docs/roadmap_changelog.md). + +### Godoc + +The godocs for this repository can be found at [pkg.go.dev/github.com/pokt-network/poktroll](https://pkg.go.dev/github.com/pokt-network/poktroll). + +### Pocket V1 (Shannon) Docs It is the result of a research spike conducted by the Core [Pocket Network](https://pokt.network/) Protocol Team at [GROVE](https://grove.city/) documented [here](https://www.pokt.network/why-pokt-network-is-rolling-with-rollkit-a-technical-deep-dive/) (deep dive) and [here](https://www.pokt.network/a-sovereign-rollup-and-a-modular-future/) (summary). From 425368716dc7d5578f0120b057adf9305bdc434c Mon Sep 17 00:00:00 2001 From: Bryan White Date: Fri, 3 Nov 2023 22:29:03 +0100 Subject: [PATCH 23/28] [Tooling] add `go_lint` & `go_imports` make targets & CI step (#129) * Added godocs target * Updated all imports for godoc * fix: missing import * tooling: add `go_lint` & `go_imports` make targets * chore: move ignite comment line to clean up imports * fix: goimports * chore: add linter CI step * fix: stub mock pkgs for CI * chore: add main package path to config.yml so ignite doesn't get confused * Merge with main. Remove .gitkeep. Add back mocks.go. Tests are passing * Update Makefile Co-authored-by: Bryan White * Import updates * Update path to main package in config.yml * fix: goimports * refactor: improve readability and extensibility * fix: go_imports make target * Revert "fix: goimports" This reverts commit 10cf6b67e41d28108daa31c9ebee4cfb518c84ee. * chore: improve comment * fix: goimport in internal/testclient/keyring.go * core: review feedback improvement Co-authored-by: Daniel Olshansky --------- Co-authored-by: Daniel Olshansky --- .github/workflows/go.yml | 3 + .golangci.yml | 56 ++++++++++ Makefile | 13 +++ internal/mocks/mockclient/mocks.go | 3 +- internal/testclient/keyring.go | 3 +- testutil/application/mocks/mocks.go | 3 +- testutil/gateway/mocks/mocks.go | 3 +- testutil/keeper/session.go | 2 +- testutil/supplier/mocks/mocks.go | 3 +- tools/scripts/goimports/filters/filters.go | 103 ++++++++++++++++++ tools/scripts/goimports/main.go | 120 +++++++++++++++++++++ x/pocket/types/codec.go | 2 +- x/service/types/codec.go | 2 +- x/session/types/codec.go | 2 +- 14 files changed, 309 insertions(+), 9 deletions(-) create mode 100644 .golangci.yml create mode 100644 tools/scripts/goimports/filters/filters.go create mode 100644 tools/scripts/goimports/main.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1ebbe4a07..1b60ddc4c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -35,6 +35,9 @@ jobs: - name: Generate mocks run: make go_mockgen + - name: Run golangci-lint + run: make go_lint + - name: Build run: ignite chain build --debug --skip-proto diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..3dc12bb05 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,56 @@ +linters-settings: + govet: + check-shadowing: true + +# TODO_TECHDEBT: Enable each linter listed, 1 by 1, fixing issues as they appear. +# Don't forget to delete the `disable-all: true` line as well. +linters: + disable-all: true + enable: +# - govet +# - revive +# - errcheck +# - unused + - goimports + +issues: + exclude-use-default: true + max-issues-per-linter: 0 + max-same-issues: 0 + # TODO_CONSIDERATION/TODO_TECHDEBT: Perhaps we should prefer enforcing the best + # practices suggested by the linters over convention or the default in generated + # code (where possible). The more exceptions we have, the bigger the gaps will be + # in our linting coverage. We could eliminate or reduce these exceptions step- + # by-step. + exclude-rules: + # Exclude cosmos-sdk module genesis.go files as they are generated with an + # empty import block containing a comment used by ignite CLI. + - path: ^x/.+/genesis\.go$ + linters: + - goimports + # Exclude cosmos-sdk module module.go files as they are generated with unused + # parameters and unchecked errors. + - path: ^x/.+/module\.go$ + linters: + - revive + - errcheck + # Exclude cosmos-sdk module tx.go files as they are generated with unused + # constants. + - path: ^x/.+/cli/tx\.go$ + linters: + - unused + # Exclude simulation code as it's generated with lots of unused parameters. + - path: .*/simulation/.*|_simulation\.go$ + linters: + - revive + # Exclude cosmos-sdk module codec files as they are scaffolded with a unused + # paramerters and a comment used by ignite CLI. + - path: ^x/.+/codec.go$ + linters: + - revive + - path: _test\.go$ + linters: + - errcheck + # TODO_IMPROVE: see https://golangci-lint.run/usage/configuration/#issues-configuration + #new: true, + #fix: true, diff --git a/Makefile b/Makefile index c34357b58..d6d3257dc 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ POCKET_ADDR_PREFIX = pokt .PHONY: install_ci_deps install_ci_deps: ## Installs `mockgen` go install "github.com/golang/mock/mockgen@v1.6.0" && mockgen --version + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && golangci-lint --version + go install golang.org/x/tools/cmd/goimports@latest ######################## ### Makefile Helpers ### @@ -115,6 +117,17 @@ localnet_regenesis: ## Regenerate the localnet genesis file cp ${HOME}/.pocket/config/*_key.json $(POCKETD_HOME)/config/ cp ${HOME}/.pocket/config/genesis.json $(POCKETD_HOME)/config/ +############### +### Linting ### +############### + +.PHONY: go_lint +go_lint: ## Run all go linters + golangci-lint run --timeout 5m + +go_imports: check_go_version ## Run goimports on all go files + go run ./tools/scripts/goimports + ############# ### Tests ### ############# diff --git a/internal/mocks/mockclient/mocks.go b/internal/mocks/mockclient/mocks.go index 0d9a6b981..d89152942 100644 --- a/internal/mocks/mockclient/mocks.go +++ b/internal/mocks/mockclient/mocks.go @@ -7,4 +7,5 @@ package mockclient // Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests // // IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation -// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. +// since we are leveraging `ignite` to compile `.proto` files which runs `go mod tidy` before generating, requiring the entire dependency tree +// to be valid before mock implementations have been generated. diff --git a/internal/testclient/keyring.go b/internal/testclient/keyring.go index 49a6b1c67..a3c8dc14c 100644 --- a/internal/testclient/keyring.go +++ b/internal/testclient/keyring.go @@ -1,10 +1,11 @@ package testclient import ( + "testing" + cosmoshd "github.com/cosmos/cosmos-sdk/crypto/hd" cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/stretchr/testify/require" - "testing" ) func NewKey( diff --git a/testutil/application/mocks/mocks.go b/testutil/application/mocks/mocks.go index 423f63d3e..595954e65 100644 --- a/testutil/application/mocks/mocks.go +++ b/testutil/application/mocks/mocks.go @@ -7,4 +7,5 @@ package mocks // Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests // // IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation -// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. +// since we are leveraging `ignite` to compile `.proto` files which runs `go mod tidy` before generating, requiring the entire dependency tree +// to be valid before mock implementations have been generated. diff --git a/testutil/gateway/mocks/mocks.go b/testutil/gateway/mocks/mocks.go index 423f63d3e..595954e65 100644 --- a/testutil/gateway/mocks/mocks.go +++ b/testutil/gateway/mocks/mocks.go @@ -7,4 +7,5 @@ package mocks // Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests // // IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation -// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. +// since we are leveraging `ignite` to compile `.proto` files which runs `go mod tidy` before generating, requiring the entire dependency tree +// to be valid before mock implementations have been generated. diff --git a/testutil/keeper/session.go b/testutil/keeper/session.go index 338204789..e4be2537f 100644 --- a/testutil/keeper/session.go +++ b/testutil/keeper/session.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/require" "github.com/pokt-network/poktroll/testutil/sample" - mocks "github.com/pokt-network/poktroll/testutil/session/mocks" + "github.com/pokt-network/poktroll/testutil/session/mocks" apptypes "github.com/pokt-network/poktroll/x/application/types" "github.com/pokt-network/poktroll/x/session/keeper" "github.com/pokt-network/poktroll/x/session/types" diff --git a/testutil/supplier/mocks/mocks.go b/testutil/supplier/mocks/mocks.go index 423f63d3e..595954e65 100644 --- a/testutil/supplier/mocks/mocks.go +++ b/testutil/supplier/mocks/mocks.go @@ -7,4 +7,5 @@ package mocks // Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests // // IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation -// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. +// since we are leveraging `ignite` to compile `.proto` files which runs `go mod tidy` before generating, requiring the entire dependency tree +// to be valid before mock implementations have been generated. diff --git a/tools/scripts/goimports/filters/filters.go b/tools/scripts/goimports/filters/filters.go new file mode 100644 index 000000000..67a048a51 --- /dev/null +++ b/tools/scripts/goimports/filters/filters.go @@ -0,0 +1,103 @@ +// The filters package contains functions that can be used to filter file paths. + +package filters + +import ( + "bufio" + "bytes" + "os" + "path/filepath" + "strings" +) + +const igniteScaffoldComment = "// this line is used by starport scaffolding" + +var ( + importStart = []byte("import (") + importEnd = []byte(")") +) + +// FilterFn is a function that returns true if the given path matches the +// filter's criteria. +type FilterFn func(path string) (bool, error) + +// PathMatchesGoExtension matches go source files. +func PathMatchesGoExtension(path string) (bool, error) { + return filepath.Ext(path) == ".go", nil +} + +// PathMatchesProtobufGo matches generated protobuf go source files. +func PathMatchesProtobufGo(path string) (bool, error) { + return strings.HasSuffix(path, ".pb.go"), nil +} + +// PathMatchesProtobufGatewayGo matches generated protobuf gateway go source files. +func PathMatchesProtobufGatewayGo(path string) (bool, error) { + return strings.HasSuffix(path, ".pb.gw.go"), nil +} + +// PathMatchesMockGo matches generated mock go source files. +func PathMatchesMockGo(path string) (bool, error) { + return strings.HasSuffix(path, "_mock.go"), nil +} + +// PathMatchesTestGo matches go test files. +func PathMatchesTestGo(path string) (bool, error) { + return strings.HasSuffix(path, "_test.go"), nil +} + +// ContentMatchesEmptyImportScaffold matches files that can't be goimport'd due +// to ignite incompatibility. +func ContentMatchesEmptyImportScaffold(path string) (bool, error) { + return containsEmptyImportScaffold(path) +} + +// containsEmptyImportScaffold checks if the go file at goSrcPath contains an +// import statement like the following: +// +// import ( +// // this line is used by starport scaffolding ... +// ) +func containsEmptyImportScaffold(goSrcPath string) (isEmptyImport bool, _ error) { + file, err := os.Open(goSrcPath) + if err != nil { + return false, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + scanner.Split(importBlockSplit) + + for scanner.Scan() { + trimmedImportBlock := strings.Trim(scanner.Text(), "\n\t") + if strings.HasPrefix(trimmedImportBlock, igniteScaffoldComment) { + return true, nil + } + } + + if scanner.Err() != nil { + return false, scanner.Err() + } + + return false, nil +} + +// importBlockSplit is a split function intended to be used with bufio.Scanner +// to extract the contents of a multi-line go import block. +func importBlockSplit(data []byte, _ bool) (advance int, token []byte, err error) { + // Search for the beginning of the import block + startIdx := bytes.Index(data, importStart) + if startIdx == -1 { + return 0, nil, nil + } + + // Search for the end of the import block from the start index + endIdx := bytes.Index(data[startIdx:], importEnd) + if endIdx == -1 { + return 0, nil, nil + } + + // Return the entire import block, including "import (" and ")" + importBlock := data[startIdx+len(importStart) : startIdx-len(importEnd)+endIdx+1] + return startIdx + endIdx + 1, importBlock, nil +} diff --git a/tools/scripts/goimports/main.go b/tools/scripts/goimports/main.go new file mode 100644 index 000000000..ca5c26648 --- /dev/null +++ b/tools/scripts/goimports/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/pokt-network/poktroll/tools/scripts/goimports/filters" +) + +// defaultArgs are always passed to goimports. +// -w: write result to (source) file instead of stdout +// -local: put imports beginning with this string after 3rd-party packages (comma-separated list) +// (see: goimports -h for more info) +var ( + defaultArgs = []string{"-w", "-local", "github.com/pokt-network/poktroll"} + defaultIncludeFilters = []filters.FilterFn{ + filters.PathMatchesGoExtension, + } + defaultExcludeFilters = []filters.FilterFn{ + filters.PathMatchesProtobufGo, + filters.PathMatchesProtobufGatewayGo, + filters.PathMatchesMockGo, + filters.PathMatchesTestGo, + filters.ContentMatchesEmptyImportScaffold, + } +) + +func main() { + root := "." + var filesToProcess []string + + // Walk the file system and accumulate matching files + err := filepath.Walk(root, walkRepoRootFn( + root, + defaultIncludeFilters, + defaultExcludeFilters, + &filesToProcess, + )) + if err != nil { + fmt.Printf("Error processing files: %s\n", err) + return + } + + // Run goimports on all accumulated files + if len(filesToProcess) > 0 { + cmd := exec.Command("goimports", append(defaultArgs, filesToProcess...)...) + if err := cmd.Run(); err != nil { + fmt.Printf("Failed running goimports: %v\n", err) + } + } +} + +func walkRepoRootFn( + rootPath string, + includeFilters []filters.FilterFn, + excludeFilters []filters.FilterFn, + filesToProcess *[]string, +) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Don't process the root directory but don't skip it either; that would + // exclude everything. + if info.Name() == rootPath { + return nil + } + + // No need to process directories + if info.IsDir() { + // Skip directories that start with a period + if strings.HasPrefix(info.Name(), ".") { + return filepath.SkipDir + } + return nil + } + + // Don't process paths which don't match any include filter. + var shouldIncludePath bool + for _, includeFilter := range includeFilters { + pathMatches, err := includeFilter(path) + if err != nil { + panic(err) + } + + if pathMatches { + shouldIncludePath = true + break + } + } + if !shouldIncludePath { + return nil + } + + // Don't process paths which match any exclude filter. + var shouldExcludePath bool + for _, excludeFilter := range excludeFilters { + pathMatches, err := excludeFilter(path) + if err != nil { + panic(err) + } + + if pathMatches { + shouldExcludePath = true + break + } + } + if shouldExcludePath { + return nil + } + + *filesToProcess = append(*filesToProcess, path) + + return nil + } +} diff --git a/x/pocket/types/codec.go b/x/pocket/types/codec.go index 844157a87..72399f81e 100644 --- a/x/pocket/types/codec.go +++ b/x/pocket/types/codec.go @@ -3,8 +3,8 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" - // this line is used by starport scaffolding # 1 "github.com/cosmos/cosmos-sdk/types/msgservice" + // this line is used by starport scaffolding # 1 ) func RegisterCodec(cdc *codec.LegacyAmino) { diff --git a/x/service/types/codec.go b/x/service/types/codec.go index 844157a87..72399f81e 100644 --- a/x/service/types/codec.go +++ b/x/service/types/codec.go @@ -3,8 +3,8 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" - // this line is used by starport scaffolding # 1 "github.com/cosmos/cosmos-sdk/types/msgservice" + // this line is used by starport scaffolding # 1 ) func RegisterCodec(cdc *codec.LegacyAmino) { diff --git a/x/session/types/codec.go b/x/session/types/codec.go index 844157a87..72399f81e 100644 --- a/x/session/types/codec.go +++ b/x/session/types/codec.go @@ -3,8 +3,8 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" - // this line is used by starport scaffolding # 1 "github.com/cosmos/cosmos-sdk/types/msgservice" + // this line is used by starport scaffolding # 1 ) func RegisterCodec(cdc *codec.LegacyAmino) { From 6e5cad87803c424570c2a17aaa5a0138bbc1661b Mon Sep 17 00:00:00 2001 From: Bryan White Date: Mon, 6 Nov 2023 21:27:23 +0100 Subject: [PATCH 24/28] fix: flaky block client test (#132) * fix: flakey block cliekt test * chore: simplify & react to review feedback * chore: add godoc comment * chore: simplify --- internal/testclient/testeventsquery/client.go | 43 ++++++ pkg/client/block/client.go | 2 +- pkg/client/block/client_test.go | 129 +++++++++--------- 3 files changed, 105 insertions(+), 69 deletions(-) diff --git a/internal/testclient/testeventsquery/client.go b/internal/testclient/testeventsquery/client.go index d55a765ab..0aa618fe9 100644 --- a/internal/testclient/testeventsquery/client.go +++ b/internal/testclient/testeventsquery/client.go @@ -1,11 +1,18 @@ package testeventsquery import ( + "context" "testing" + "time" + "github.com/golang/mock/gomock" + + "github.com/pokt-network/poktroll/internal/mocks/mockclient" "github.com/pokt-network/poktroll/internal/testclient" "github.com/pokt-network/poktroll/pkg/client" eventsquery "github.com/pokt-network/poktroll/pkg/client/events_query" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) // NewLocalnetClient returns a new events query client which is configured to @@ -15,3 +22,39 @@ func NewLocalnetClient(t *testing.T, opts ...client.EventsQueryClientOption) cli return eventsquery.NewEventsQueryClient(testclient.CometLocalWebsocketURL, opts...) } + +// NewAnyTimesEventsBytesEventsQueryClient returns a new events query client which +// is configured to return the expected event bytes when queried with the expected +// query, any number of times. The returned client also expects to be closed once. +func NewAnyTimesEventsBytesEventsQueryClient( + ctx context.Context, + t *testing.T, + expectedQuery string, + expectedEventBytes []byte, +) client.EventsQueryClient { + t.Helper() + + ctrl := gomock.NewController(t) + eventsQueryClient := mockclient.NewMockEventsQueryClient(ctrl) + eventsQueryClient.EXPECT().Close().Times(1) + eventsQueryClient.EXPECT(). + EventsBytes(gomock.AssignableToTypeOf(ctx), gomock.Eq(expectedQuery)). + DoAndReturn( + func(ctx context.Context, query string) (client.EventsBytesObservable, error) { + bytesObsvbl, bytesPublishCh := channel.NewReplayObservable[either.Bytes](ctx, 1) + + // Now that the observable is set up, publish the expected event bytes. + // Only need to send once because it's a ReplayObservable. + bytesPublishCh <- either.Success(expectedEventBytes) + + // Wait a tick for the observables to be set up. This isn't strictly + // necessary but is done to mitigate test flakiness. + time.Sleep(10 * time.Millisecond) + + return bytesObsvbl, nil + }, + ). + AnyTimes() + + return eventsQueryClient +} diff --git a/pkg/client/block/client.go b/pkg/client/block/client.go index 54569e60d..18526508d 100644 --- a/pkg/client/block/client.go +++ b/pkg/client/block/client.go @@ -155,7 +155,7 @@ func (bClient *blockClient) retryPublishBlocksFactory(ctx context.Context) func( } // NB: must cast back to generic observable type to use with Map. - // client.BlocksObservable is only used to workaround gomock's lack of + // client.BlocksObservable cannot be an alias due to gomock's lack of // support for generic types. eventsBz := observable.Observable[either.Either[[]byte]](eventsBzObsvbl) blockEventFromEventBz := newEventsBytesToBlockMapFn(errCh) diff --git a/pkg/client/block/client_test.go b/pkg/client/block/client_test.go index b983ff274..b2a5515b3 100644 --- a/pkg/client/block/client_test.go +++ b/pkg/client/block/client_test.go @@ -8,17 +8,20 @@ import ( "cosmossdk.io/depinject" comettypes "github.com/cometbft/cometbft/types" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" "github.com/pokt-network/poktroll/internal/testclient" "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" "github.com/pokt-network/poktroll/pkg/client" "github.com/pokt-network/poktroll/pkg/client/block" - eventsquery "github.com/pokt-network/poktroll/pkg/client/events_query" ) -const blockAssertionLoopTimeout = 500 * time.Millisecond +const ( + testTimeoutDuration = 100 * time.Millisecond + + // duplicates pkg/client/block/client.go's committedBlocksQuery for testing purposes + committedBlocksQuery = "tm.event='NewBlock'" +) func TestBlockClient(t *testing.T) { var ( @@ -38,19 +41,15 @@ func TestBlockClient(t *testing.T) { ctx = context.Background() ) - // Set up a mock connection and dialer which are expected to be used once. - connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) - connMock.EXPECT().Send(gomock.Any()).Return(nil).Times(1) - // Mock the Receive method to return the expected block event. - connMock.EXPECT().Receive().DoAndReturn(func() ([]byte, error) { - blockEventJson, err := json.Marshal(expectedBlockEvent) - require.NoError(t, err) - return blockEventJson, nil - }).AnyTimes() - - // Set up events query client dependency. - dialerOpt := eventsquery.WithDialer(dialerMock) - eventsQueryClient := testeventsquery.NewLocalnetClient(t, dialerOpt) + expectedEventBz, err := json.Marshal(expectedBlockEvent) + require.NoError(t, err) + + eventsQueryClient := testeventsquery.NewAnyTimesEventsBytesEventsQueryClient( + ctx, t, + committedBlocksQuery, + expectedEventBz, + ) + deps := depinject.Supply(eventsQueryClient) // Set up block client. @@ -58,60 +57,54 @@ func TestBlockClient(t *testing.T) { require.NoError(t, err) require.NotNil(t, blockClient) - // Run LatestBlock and CommittedBlockSequence concurrently because they can - // block, leading to an unresponsive test. This function sends multiple values - // on the actualBlockCh which are all asserted against in blockAssertionLoop. - // If any of the methods under test hang, the test will time out. - var ( - actualBlockCh = make(chan client.Block, 1) - done = make(chan struct{}, 1) - ) - go func() { - // Test LatestBlock method. - actualBlock := blockClient.LatestBlock(ctx) - require.Equal(t, expectedHeight, actualBlock.Height()) - require.Equal(t, expectedHash, actualBlock.Hash()) - - // Test CommittedBlockSequence method. - blockObservable := blockClient.CommittedBlocksSequence(ctx) - require.NotNil(t, blockObservable) - - // Ensure that the observable is replayable via Last. - actualBlockCh <- blockObservable.Last(ctx, 1)[0] - - // Ensure that the observable is replayable via Subscribe. - blockObserver := blockObservable.Subscribe(ctx) - for block := range blockObserver.Ch() { - actualBlockCh <- block - break - } - - // Signal test completion - done <- struct{}{} - }() - - // blockAssertionLoop ensures that the blocks retrieved from both LatestBlock - // method and CommittedBlocksSequence method match the expected block height - // and hash. This loop waits for blocks to be sent on the actualBlockCh channel - // by the methods being tested. Once the methods are done, they send a signal on - // the "done" channel. If the blockAssertionLoop doesn't receive any block or - // the done signal within a specific timeout, it assumes something has gone wrong - // and fails the test. -blockAssertionLoop: - for { - select { - case actualBlock := <-actualBlockCh: - require.Equal(t, expectedHeight, actualBlock.Height()) - require.Equal(t, expectedHash, actualBlock.Hash()) - case <-done: - break blockAssertionLoop - case <-time.After(blockAssertionLoopTimeout): - t.Fatal("timed out waiting for block event") - } + tests := []struct { + name string + fn func() client.Block + }{ + { + name: "LatestBlock successfully returns latest block", + fn: func() client.Block { + lastBlock := blockClient.LatestBlock(ctx) + return lastBlock + }, + }, + { + name: "CommittedBlocksSequence successfully returns latest block", + fn: func() client.Block { + blockObservable := blockClient.CommittedBlocksSequence(ctx) + require.NotNil(t, blockObservable) + + // Ensure that the observable is replayable via Last. + lastBlock := blockObservable.Last(ctx, 1)[0] + require.Equal(t, expectedHeight, lastBlock.Height()) + require.Equal(t, expectedHash, lastBlock.Hash()) + + return lastBlock + }, + }, } - // Wait a tick for the observables to be set up. - time.Sleep(time.Millisecond) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var actualBlockCh = make(chan client.Block, 10) + + // Run test functions asynchronously because they can block, leading + // to an unresponsive test. If any of the methods under test hang, + // the test will time out in the select statement that follows. + go func(fn func() client.Block) { + actualBlockCh <- fn() + close(actualBlockCh) + }(tt.fn) + + select { + case actualBlock := <-actualBlockCh: + require.Equal(t, expectedHeight, actualBlock.Height()) + require.Equal(t, expectedHash, actualBlock.Hash()) + case <-time.After(testTimeoutDuration): + t.Fatal("timed out waiting for block event") + } + }) + } blockClient.Close() } From e64e26e4fd9c030e1b0cfbfc0ab95d3bef7b4b05 Mon Sep 17 00:00:00 2001 From: Dima Kniazev Date: Mon, 6 Nov 2023 15:38:41 -0800 Subject: [PATCH 25/28] [CI] Build container images (#107) * wip - need info from GitHub CI * build image as a part of main ci * troublshoot w/o test * should be a cp here * wip * more label control * install directly from github * use wget * rerun ci * troubleshoot * more information * it was git context * kill previous run if a new commit is pushed * this should work * remove buildlog * resolve conflicts * perform the tests as well * we will be allright withoug keeping the bin dir * bring back ignite version * also build on mai * refine label actions * Update .github/workflows/go.yml Co-authored-by: Daniel Olshansky * add requested changes * pocketd has been replaced with poktrolld * only change the binary name for now, take care of other pocketd instances later * Update .github/label-actions.yml Co-authored-by: Daniel Olshansky * rename pocketd with poktrolld where necessary * typofix * also use poktrolld for e2e tests --------- Co-authored-by: Daniel Olshansky --- .github/label-actions.yml | 35 ++++++++++++ .github/workflows/go.yml | 54 ++++++++++++++++++- .github/workflows/label-actions.yml | 21 ++++++++ .gitignore | 6 ++- Dockerfile.dev | 23 ++++++++ Makefile | 37 ++++++++----- Tiltfile | 6 +-- e2e/tests/node.go | 2 +- .../client/cli/tx_delegate_to_gateway.go | 2 +- .../client/cli/tx_stake_application.go | 2 +- .../client/cli/tx_undelegate_from_gateway.go | 2 +- .../client/cli/tx_unstake_application.go | 2 +- x/gateway/client/cli/tx_stake_gateway.go | 2 +- x/gateway/client/cli/tx_unstake_gateway.go | 2 +- x/supplier/client/cli/tx_stake_supplier.go | 2 +- x/supplier/client/cli/tx_unstake_supplier.go | 2 +- 16 files changed, 171 insertions(+), 29 deletions(-) create mode 100644 .github/label-actions.yml create mode 100644 .github/workflows/label-actions.yml create mode 100644 Dockerfile.dev diff --git a/.github/label-actions.yml b/.github/label-actions.yml new file mode 100644 index 000000000..b4dc1814d --- /dev/null +++ b/.github/label-actions.yml @@ -0,0 +1,35 @@ +# When `devnet-e2e-test` is added, also assign `devnet` to the PR. +devnet-e2e-test: + prs: + comment: The CI will now also run the e2e tests on devnet, which increases the time it takes to complete all CI checks. + label: + - devnet + +# When `devnet-e2e-test` is removed, also delete `devnet` from the PR. +-devnet-e2e-test: + prs: + unlabel: + - devnet + +# When `devnet` is added, also assign `push-image` to the PR. +devnet: + prs: + label: + - push-image + +# When `devnet` is removed, also delete `devnet-e2e-test` from the PR. +-devnet: + prs: + unlabel: + - devnet-e2e-test + +# Let the developer know that they need to push another commit after attaching the label to PR. +push-image: + prs: + comment: The image is going to be pushed after the next commit. If you want to run an e2e test, it is necessary to push another commit. You can use `make trigger_ci` to push an empty commit. + +# When `push-image` is removed, also delete `devnet` from the PR. +-push-image: + prs: + unlabel: + - devnet diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1b60ddc4c..6f76334c1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -8,11 +8,16 @@ on: branches: ["main"] pull_request: +concurrency: + group: ${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest steps: - name: install ignite + # If this step fails due to ignite.com failing, see #116 for a temporary workaround run: | curl https://get.ignite.com/cli! | bash ignite version @@ -39,7 +44,54 @@ jobs: run: make go_lint - name: Build - run: ignite chain build --debug --skip-proto + run: ignite chain build -v --debug --skip-proto - name: Test run: make go_test + + - name: Set up Docker Buildx + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + uses: docker/setup-buildx-action@v3 + + - name: Docker Metadata action + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + id: meta + uses: docker/metadata-action@v5 + env: + DOCKER_METADATA_PR_HEAD_SHA: "true" + with: + images: | + ghcr.io/pokt-network/pocketd + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=sha,format=long + + - name: Login to GitHub Container Registry + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Copy binary to inside of the Docker context + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + run: | + mkdir -p ./bin # Make sure the bin directory exists + cp $(go env GOPATH)/bin/poktrolld ./bin # Copy the binary to the repo's bin directory + + - name: Build and push Docker image + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + uses: docker/build-push-action@v5 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # NB: Uncomment below if arm64 build is needed; arm64 builds are off by default because build times are significant. + platforms: linux/amd64 #,linux/arm64 + file: Dockerfile.dev + cache-from: type=gha + cache-to: type=gha,mode=max + context: . diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml new file mode 100644 index 000000000..caf1a31cc --- /dev/null +++ b/.github/workflows/label-actions.yml @@ -0,0 +1,21 @@ +name: 'Label Actions' + +on: + issues: + types: [labeled, unlabeled] + pull_request_target: + types: [labeled, unlabeled] + discussion: + types: [labeled, unlabeled] + +permissions: + contents: read + issues: write + pull-requests: write + discussions: write + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/label-actions@v3 diff --git a/.gitignore b/.gitignore index aa7066b08..5dc239e58 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ go.work # Don't commit binaries bin -!bin/.keep # Before we provision the localnet, `ignite` creates the accounts, genesis, etc. for us # As many of the files are dynamic, we only preserve the config files in git history. @@ -57,4 +56,7 @@ ts-client/ **/*_mock.go # Localnet config -localnet_config.yaml \ No newline at end of file +localnet_config.yaml + +# Relase artifacts produced by `ignite chain build --release` +release diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 000000000..2d10955e0 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,23 @@ +# This Dockerfile is used to build container image for development purposes. +# It intentionally contains no security features, ships with code and troubleshooting tools. + +FROM golang:1.20 as base + +RUN apt update && \ + apt-get install -y \ + ca-certificates \ + curl jq make + +# enable faster module downloading. +ENV GOPROXY https://proxy.golang.org + +COPY . /poktroll + +WORKDIR /poktroll + +RUN mv /poktroll/bin/poktrolld /usr/bin/poktrolld + +EXPOSE 8545 +EXPOSE 8546 + +ENTRYPOINT ["ignite"] diff --git a/Makefile b/Makefile index d6d3257dc..19c8fe59e 100644 --- a/Makefile +++ b/Makefile @@ -231,11 +231,11 @@ todo_this_commit: ## List all the TODOs needed to be done in this commit .PHONY: gateway_list gateway_list: ## List all the staked gateways - pocketd --home=$(POCKETD_HOME) q gateway list-gateway --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q gateway list-gateway --node $(POCKET_NODE) .PHONY: gateway_stake gateway_stake: ## Stake tokens for the gateway specified (must specify the gateway env var) - pocketd --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) .PHONY: gateway1_stake gateway1_stake: ## Stake gateway1 @@ -251,7 +251,7 @@ gateway3_stake: ## Stake gateway3 .PHONY: gateway_unstake gateway_unstake: ## Unstake an gateway (must specify the GATEWAY env var) - pocketd --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) .PHONY: gateway1_unstake gateway1_unstake: ## Unstake gateway1 @@ -271,11 +271,11 @@ gateway3_unstake: ## Unstake gateway3 .PHONY: app_list app_list: ## List all the staked applications - pocketd --home=$(POCKETD_HOME) q application list-application --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q application list-application --node $(POCKET_NODE) .PHONY: app_stake app_stake: ## Stake tokens for the application specified (must specify the APP and SERVICES env vars) - pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt $(SERVICES) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx application stake-application 1000upokt $(SERVICES) --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_stake app1_stake: ## Stake app1 @@ -291,7 +291,7 @@ app3_stake: ## Stake app3 .PHONY: app_unstake app_unstake: ## Unstake an application (must specify the APP env var) - pocketd --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_unstake app1_unstake: ## Unstake app1 @@ -307,7 +307,7 @@ app3_unstake: ## Unstake app3 .PHONY: app_delegate app_delegate: ## Delegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked - pocketd --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_delegate_gateway1 app1_delegate_gateway1: ## Delegate trust to gateway1 @@ -323,7 +323,7 @@ app3_delegate_gateway3: ## Delegate trust to gateway3 .PHONY: app_undelegate app_undelegate: ## Undelegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked - pocketd --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_undelegate_gateway1 app1_undelegate_gateway1: ## Undelegate trust to gateway1 @@ -343,13 +343,13 @@ app3_undelegate_gateway3: ## Undelegate trust to gateway3 .PHONY: supplier_list supplier_list: ## List all the staked supplier - pocketd --home=$(POCKETD_HOME) q supplier list-supplier --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q supplier list-supplier --node $(POCKET_NODE) # TODO(@Olshansk, @okdas): Add more services (in addition to anvil) for apps and suppliers to stake for. # TODO_TECHDEBT: svc1, svc2 and svc3 below are only in place to make GetSession testable .PHONY: supplier_stake supplier_stake: ## Stake tokens for the supplier specified (must specify the APP env var) - pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt "$(SERVICES)" --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt "$(SERVICES)" --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) .PHONY: supplier1_stake supplier1_stake: ## Stake supplier1 @@ -365,7 +365,7 @@ supplier3_stake: ## Stake supplier3 .PHONY: supplier_unstake supplier_unstake: ## Unstake an supplier (must specify the SUPPLIER env var) - pocketd --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) .PHONY: supplier1_unstake supplier1_unstake: ## Unstake supplier1 @@ -386,10 +386,10 @@ supplier3_unstake: ## Unstake supplier3 .PHONY: acc_balance_query acc_balance_query: ## Query the balance of the account specified (make acc_balance_query ACC=pokt...) @echo "~~~ Balances ~~~" - pocketd --home=$(POCKETD_HOME) q bank balances $(ACC) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q bank balances $(ACC) --node $(POCKET_NODE) @echo "~~~ Spendable Balances ~~~" @echo "Querying spendable balance for $(ACC)" - pocketd --home=$(POCKETD_HOME) q bank spendable-balances $(ACC) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q bank spendable-balances $(ACC) --node $(POCKET_NODE) .PHONY: acc_balance_query_module_app acc_balance_query_module_app: ## Query the balance of the network level "application" module @@ -405,7 +405,7 @@ acc_balance_query_app1: ## Query the balance of app1 .PHONY: acc_balance_total_supply acc_balance_total_supply: ## Query the total supply of the network - pocketd --home=$(POCKETD_HOME) q bank total --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q bank total --node $(POCKET_NODE) ###################### ### Ignite Helpers ### @@ -415,6 +415,15 @@ acc_balance_total_supply: ## Query the total supply of the network ignite_acc_list: ## List all the accounts in LocalNet ignite account list --keyring-dir=$(POCKETD_HOME) --keyring-backend test --address-prefix $(POCKET_ADDR_PREFIX) +################## +### CI Helpers ### +################## + +.PHONY: trigger_ci +trigger_ci: ## Trigger the CI pipeline by submitting an empty commit; See https://github.com/pokt-network/pocket/issues/900 for details + git commit --allow-empty -m "Empty commit" + git push + ##################### ### Documentation ### ##################### diff --git a/Tiltfile b/Tiltfile index 1fd0dd779..17521b14b 100644 --- a/Tiltfile +++ b/Tiltfile @@ -101,12 +101,12 @@ docker_build_with_restart( dockerfile_contents="""FROM golang:1.20.8 RUN apt-get -q update && apt-get install -qyy curl jq RUN go install github.com/go-delve/delve/cmd/dlv@latest -COPY bin/pocketd /usr/local/bin/pocketd +COPY bin/poktrolld /usr/local/bin/pocketd WORKDIR / """, - only=["./bin/pocketd"], + only=["./bin/poktrolld"], entrypoint=["/bin/sh", "/scripts/pocket.sh"], - live_update=[sync("bin/pocketd", "/usr/local/bin/pocketd")], + live_update=[sync("bin/poktrolld", "/usr/local/bin/pocketd")], ) # Run celestia and anvil nodes diff --git a/e2e/tests/node.go b/e2e/tests/node.go index 4e34fa827..e46ad2889 100644 --- a/e2e/tests/node.go +++ b/e2e/tests/node.go @@ -67,7 +67,7 @@ func (p *pocketdBin) RunCommandOnHost(rpcUrl string, args ...string) (*commandRe func (p *pocketdBin) runCmd(args ...string) (*commandResult, error) { base := []string{"--home", defaultHome} args = append(base, args...) - cmd := exec.Command("pocketd", args...) + cmd := exec.Command("poktrolld", args...) r := &commandResult{} out, err := cmd.Output() if err != nil { diff --git a/x/application/client/cli/tx_delegate_to_gateway.go b/x/application/client/cli/tx_delegate_to_gateway.go index 324f88622..ea251e6cd 100644 --- a/x/application/client/cli/tx_delegate_to_gateway.go +++ b/x/application/client/cli/tx_delegate_to_gateway.go @@ -22,7 +22,7 @@ that delegates authority to the gateway specified to sign relays requests for th act on the behalf of the application during a session. Example: -$ pocketd --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { gatewayAddress := args[0] diff --git a/x/application/client/cli/tx_stake_application.go b/x/application/client/cli/tx_stake_application.go index 4b077e6c2..510cfd648 100644 --- a/x/application/client/cli/tx_stake_application.go +++ b/x/application/client/cli/tx_stake_application.go @@ -27,7 +27,7 @@ func CmdStakeApplication() *cobra.Command { will stake the tokens and serviceIds and associate them with the application specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt svc1,svc2,svc3 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx application stake-application 1000upokt svc1,svc2,svc3 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { stakeString := args[0] diff --git a/x/application/client/cli/tx_undelegate_from_gateway.go b/x/application/client/cli/tx_undelegate_from_gateway.go index 95a770baa..308a5d8a0 100644 --- a/x/application/client/cli/tx_undelegate_from_gateway.go +++ b/x/application/client/cli/tx_undelegate_from_gateway.go @@ -22,7 +22,7 @@ that removes the authority from the gateway specified to sign relays requests fo act on the behalf of the application during a session. Example: -$ pocketd --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { gatewayAddress := args[0] diff --git a/x/application/client/cli/tx_unstake_application.go b/x/application/client/cli/tx_unstake_application.go index ebf720a82..bfbf10e32 100644 --- a/x/application/client/cli/tx_unstake_application.go +++ b/x/application/client/cli/tx_unstake_application.go @@ -22,7 +22,7 @@ func CmdUnstakeApplication() *cobra.Command { the application specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) (err error) { diff --git a/x/gateway/client/cli/tx_stake_gateway.go b/x/gateway/client/cli/tx_stake_gateway.go index 2104b2523..2c363b43b 100644 --- a/x/gateway/client/cli/tx_stake_gateway.go +++ b/x/gateway/client/cli/tx_stake_gateway.go @@ -21,7 +21,7 @@ func CmdStakeGateway() *cobra.Command { Long: `Stake a gateway with the provided parameters. This is a broadcast operation that will stake the tokens and associate them with the gateway specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { clientCtx, err := client.GetClientTxContext(cmd) diff --git a/x/gateway/client/cli/tx_unstake_gateway.go b/x/gateway/client/cli/tx_unstake_gateway.go index b57fd9eb7..e417b7540 100644 --- a/x/gateway/client/cli/tx_unstake_gateway.go +++ b/x/gateway/client/cli/tx_unstake_gateway.go @@ -21,7 +21,7 @@ func CmdUnstakeGateway() *cobra.Command { Long: `Unstake a gateway. This is a broadcast operation that will unstake the gateway specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, _ []string) (err error) { clientCtx, err := client.GetClientTxContext(cmd) diff --git a/x/supplier/client/cli/tx_stake_supplier.go b/x/supplier/client/cli/tx_stake_supplier.go index f223799cf..eac4b4044 100644 --- a/x/supplier/client/cli/tx_stake_supplier.go +++ b/x/supplier/client/cli/tx_stake_supplier.go @@ -32,7 +32,7 @@ of comma separated values of the form 'service;url' where 'service' is the servi For example, an application that stakes for 'anvil' could be matched with a supplier staking for 'anvil;http://anvil:8547'. Example: -$ pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt anvil;http://anvil:8547 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt anvil;http://anvil:8547 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { stakeString := args[0] diff --git a/x/supplier/client/cli/tx_unstake_supplier.go b/x/supplier/client/cli/tx_unstake_supplier.go index 40ac4a83f..2daf7c00a 100644 --- a/x/supplier/client/cli/tx_unstake_supplier.go +++ b/x/supplier/client/cli/tx_unstake_supplier.go @@ -17,7 +17,7 @@ func CmdUnstakeSupplier() *cobra.Command { Long: `Unstake an supplier with the provided parameters. This is a broadcast operation that will unstake the supplier specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) (err error) { From 1974c8a864816eb8461f9982c0b0cd251151683d Mon Sep 17 00:00:00 2001 From: Bryan White Date: Tue, 7 Nov 2023 09:25:20 +0100 Subject: [PATCH 26/28] [Miner] feat: add `TxClient` (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add `TxClient` interface * chore: add option support to `ReplayObservable` * feat: add `txClient` implementation * test: `txClient` * test: tx client integration * chore: s/tx/transaction/g * chore: update pkg README.md template * wip: client pkg README * docs: fix client pkg godoc comment * fix: flakey test * chore: dial back godoc comments 😅 * chore: revise (and move to godoc.go) `testblock` & `testeventsquery` pkg godoc comment * chore: update go.mod * chore: refactor & condense godoc comments * chore: fix import paths post-update * chore: review feedback improvements * docs: update client README.md * docs: add `tx query` usage association between `txContext` & `Blockchain` * docs: add TOC * chore: review feedback improvements Co-authored-by: Daniel Olshansky * docs: improve godoc comments & client README.md --------- Co-authored-by: Daniel Olshansky --- docs/pkg/client/README.md | 147 +++++ docs/template/pkg/README.md | 21 +- go.mod | 5 +- go.sum | 3 +- internal/testclient/testblock/client.go | 73 +++ internal/testclient/testblock/godoc.go | 4 + internal/testclient/testeventsquery/client.go | 61 +- internal/testclient/testeventsquery/godoc.go | 5 + internal/testclient/testtx/context.go | 113 +--- pkg/client/godoc.go | 12 + pkg/client/interface.go | 56 +- pkg/client/services.go | 19 + pkg/client/tx/client.go | 567 ++++++++++++++++++ pkg/client/tx/client_integration_test.go | 65 ++ pkg/client/tx/client_test.go | 413 +++++++++++++ pkg/client/tx/context.go | 13 +- pkg/client/tx/encoding.go | 18 + pkg/client/tx/errors.go | 53 ++ pkg/client/tx/options.go | 22 + pkg/observable/channel/replay.go | 3 +- 20 files changed, 1517 insertions(+), 156 deletions(-) create mode 100644 docs/pkg/client/README.md create mode 100644 internal/testclient/testblock/godoc.go create mode 100644 internal/testclient/testeventsquery/godoc.go create mode 100644 pkg/client/godoc.go create mode 100644 pkg/client/services.go create mode 100644 pkg/client/tx/client.go create mode 100644 pkg/client/tx/client_integration_test.go create mode 100644 pkg/client/tx/client_test.go create mode 100644 pkg/client/tx/encoding.go create mode 100644 pkg/client/tx/errors.go create mode 100644 pkg/client/tx/options.go diff --git a/docs/pkg/client/README.md b/docs/pkg/client/README.md new file mode 100644 index 000000000..6f4032800 --- /dev/null +++ b/docs/pkg/client/README.md @@ -0,0 +1,147 @@ +# Package `client` + +## Table of Contents + +- [Overview](#overview) +- [Features](#features) +- [Architecture Overview](#architecture-overview) + - [Component Diagram Legend](#component-diagram-legend) + - [Clients Dependency Tree](#clients-dependency-tree) + - [Network Interaction](#network-interaction) +- [Installation](#installation) +- [Usage](#usage) + - [Basic Example](#basic-example) + - [Advanced Usage](#advanced-usage) + - [Configuration](#configuration) +- [API Reference](#api-reference) +- [Best Practices](#best-practices) +- [FAQ](#faq) + + +## Overview + +The `client` package exposes go APIs to facilitate interactions with the Pocket network. +It includes lower-level interfaces for working with transactions and subscribing to events generally, as well as higher-level interfaces for tracking blocks and broadcasting protocol-specific transactions. + +## Features + +| Interface | Description | +|-------------------------|----------------------------------------------------------------------------------------------------| +| **`SupplierClient`** | A high-level client for use by the "supplier" actor. | +| **`TxClient`** | A high-level client used to build, sign, and broadcast transaction from cosmos-sdk messages. | +| **`TxContext`** | Abstracts and encapsulates the transaction building, signing, encoding, and broadcasting concerns. | +| **`BlockClient`** | Exposes methods for receiving notifications about newly committed blocks. | +| **`EventsQueryClient`** | Encapsulates blockchain event subscriptions. | +| **`Connection`** | A transport agnostic communication channel for sending and receiving messages. | +| **`Dialer`** | Abstracts the establishment of connections. | + +## Architecture Overview + +```mermaid +--- +title: Component Diagram Legend +--- +flowchart + +c[Component] +d[Dependency Component] +s[[Subcomponent]] +r[Remote Component] + +c --"direct usage via #DependencyMethod()"--> d +c -."usage via network I/O".-> r +c --> s +``` + +> **Figure 1**: A legend for the component diagrams in this document. + +```mermaid +--- +title: Clients Dependency Tree +--- +flowchart + +sup[SupplierClient] +tx[TxClient] +txctx[[TxContext]] +bl[BlockClient] +evt[EventsQueryClient] +conn[[Connection]] +dial[[Dialer]] + +sup --"#SignAndBroadcast()"--> tx + +tx --"#CommittedBlocksSequence()"--> bl +tx --"#BroadcastTx"--> txctx +tx --"#EventsBytes()"--> evt +bl --"#EventsBytes()"--> evt +evt --> conn +evt --"#DialContext()"--> dial +dial --"(returns)"--> conn +``` + +> **Figure 2**: An overview which articulates the dependency relationships between the various client interfaces and their subcompnents. + +```mermaid +--- +title: Network Interaction +--- +flowchart + +txctx[[TxContext]] +conn[[Connection]] +dial[[Dialer]] + +chain[Blockchain] + +conn <-."subscribed events".-> chain +dial -."RPC subscribe".-> chain +txctx -."tx broadcast".-> chain +txctx -."tx query".-> chain +``` + +> **Figure 3**: An overview of how client subcomponents interact with the network. + +## Installation + +```bash +go get github.com/pokt-network/poktroll/pkg/client +``` + +## Usage + +### Basic Example + +```go +// TODO: Code example showcasing the use of TxClient or any other primary interface. +``` + +### Advanced Usage + +```go +// TODO: Example illustrating advanced features or edge cases of the package. +``` + +### Configuration + +- **TxClientOption**: Function type that modifies the `TxClient` allowing for flexible and optional configurations. +- **EventsQueryClientOption**: Modifies the `EventsQueryClient` to apply custom behaviors or configurations. + +## API Reference + +For the complete API details, see the [godoc](https://pkg.go.dev/github.com/pokt-network/poktroll/pkg/client). + +## Best Practices + +- **Use Abstractions**: Instead of directly communicating with blockchain platforms, leverage the provided interfaces for consistent and error-free interactions. +- **Stay Updated**: With evolving blockchain technologies, ensure to keep the package updated for any new features or security patches. + +## FAQ + +#### How does the `TxClient` interface differ from `TxContext`? + +While `TxClient` is centered around signing and broadcasting transactions, `TxContext` consolidates operational dependencies for the transaction lifecycle, like building, encoding, and querying. + +#### Can I extend or customize the provided interfaces? + +Yes, the package is designed with modularity in mind. You can either implement the interfaces based on your requirements or extend them for additional functionalities. \ No newline at end of file diff --git a/docs/template/pkg/README.md b/docs/template/pkg/README.md index 44f41885a..10fdc2755 100644 --- a/docs/template/pkg/README.md +++ b/docs/template/pkg/README.md @@ -70,12 +70,7 @@ If the package can be configured in some way, describe it here: ## API Reference -While `godoc` will provide the detailed API reference, you can highlight or briefly describe key functions, types, or methods here. - -- `FunctionOrType1()`: A short description of its purpose. -- `FunctionOrType2(param Type)`: Another brief description. - -For the complete API details, see the [godoc](https://pkg.go.dev/github.com/yourusername/yourproject/[PackageName]). +For the complete API details, see the [godoc](https://pkg.go.dev/github.com/pokt-network/poktroll/[PackageName]). ## Best Practices @@ -90,16 +85,4 @@ Answer for question 1. #### Question 2? -Answer for question 2. - -## Contributing - -Briefly describe how others can contribute to this package. Link to the main contributing guide if you have one. - -## Changelog - -For detailed release notes, see the [CHANGELOG](../CHANGELOG.md) at the root level or link to a separate CHANGELOG specific to this package. - -## License - -This package is released under the XYZ License. For more information, see the [LICENSE](../LICENSE) file at the root level. \ No newline at end of file +Answer for question 2. \ No newline at end of file diff --git a/go.mod b/go.mod index 95c298124..2d699d1cf 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( cosmossdk.io/math v1.0.1 github.com/cometbft/cometbft v0.37.2 github.com/cometbft/cometbft-db v0.8.0 - github.com/cosmos/cosmos-proto v1.0.0-beta.2 github.com/cosmos/cosmos-sdk v0.47.3 github.com/cosmos/gogoproto v1.4.10 github.com/cosmos/ibc-go/v7 v7.1.0 @@ -27,7 +26,6 @@ require ( go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.12.0 golang.org/x/sync v0.3.0 - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 google.golang.org/grpc v1.56.1 gopkg.in/yaml.v2 v2.4.0 ) @@ -71,6 +69,7 @@ require ( github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.0 // indirect @@ -135,7 +134,6 @@ require ( github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect @@ -266,6 +264,7 @@ require ( gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/api v0.122.0 // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 48ddd50c7..ef5829bd5 100644 --- a/go.sum +++ b/go.sum @@ -933,9 +933,8 @@ github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoD github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= diff --git a/internal/testclient/testblock/client.go b/internal/testclient/testblock/client.go index 0918ee64f..ebd2ebcd7 100644 --- a/internal/testclient/testblock/client.go +++ b/internal/testclient/testblock/client.go @@ -5,14 +5,19 @@ import ( "testing" "cosmossdk.io/depinject" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + "github.com/pokt-network/poktroll/internal/mocks/mockclient" "github.com/pokt-network/poktroll/internal/testclient" "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" "github.com/pokt-network/poktroll/pkg/client" "github.com/pokt-network/poktroll/pkg/client/block" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) +// NewLocalnetClient creates and returns a new BlockClient that's configured for +// use with the localnet sequencer. func NewLocalnetClient(ctx context.Context, t *testing.T) client.BlockClient { t.Helper() @@ -25,3 +30,71 @@ func NewLocalnetClient(ctx context.Context, t *testing.T) client.BlockClient { return bClient } + +// NewOneTimeCommittedBlocksSequenceBlockClient creates a new mock BlockClient. +// This mock BlockClient will expect a call to CommittedBlocksSequence, and +// when that call is made, it returns a new BlocksObservable that is notified of +// blocks sent on the given blocksPublishCh. +// blocksPublishCh is the channel the caller can use to publish blocks the observable. +func NewOneTimeCommittedBlocksSequenceBlockClient( + t *testing.T, + blocksPublishCh chan client.Block, +) *mockclient.MockBlockClient { + t.Helper() + + // Create a mock for the block client which expects the LatestBlock method to be called any number of times. + blockClientMock := NewAnyTimeLatestBlockBlockClient(t, nil, 0) + + // Set up the mock expectation for the CommittedBlocksSequence method. When + // the method is called, it returns a new replay observable that publishes + // blocks sent on the given blocksPublishCh. + blockClientMock.EXPECT().CommittedBlocksSequence( + gomock.AssignableToTypeOf(context.Background()), + ).DoAndReturn(func(ctx context.Context) client.BlocksObservable { + // Create a new replay observable with a replay buffer size of 1. Blocks + // are published to this observable via the provided blocksPublishCh. + withPublisherOpt := channel.WithPublisher(blocksPublishCh) + obs, _ := channel.NewReplayObservable[client.Block]( + ctx, 1, withPublisherOpt, + ) + return obs + }) + + return blockClientMock +} + +// NewAnyTimeLatestBlockBlockClient creates a mock BlockClient that expects +// calls to the LatestBlock method any number of times. When the LatestBlock +// method is called, it returns a mock Block with the provided hash and height. +func NewAnyTimeLatestBlockBlockClient( + t *testing.T, + hash []byte, + height int64, +) *mockclient.MockBlockClient { + t.Helper() + ctrl := gomock.NewController(t) + + // Create a mock block that returns the provided hash and height. + blockMock := NewAnyTimesBlock(t, hash, height) + // Create a mock block client that expects calls to LatestBlock method and + // returns the mock block. + blockClientMock := mockclient.NewMockBlockClient(ctrl) + blockClientMock.EXPECT().LatestBlock(gomock.Any()).Return(blockMock).AnyTimes() + + return blockClientMock +} + +// NewAnyTimesBlock creates a mock Block that expects calls to Height and Hash +// methods any number of times. When the methods are called, they return the +// provided height and hash respectively. +func NewAnyTimesBlock(t *testing.T, hash []byte, height int64) *mockclient.MockBlock { + t.Helper() + ctrl := gomock.NewController(t) + + // Create a mock block that returns the provided hash and height AnyTimes. + blockMock := mockclient.NewMockBlock(ctrl) + blockMock.EXPECT().Height().Return(height).AnyTimes() + blockMock.EXPECT().Hash().Return(hash).AnyTimes() + + return blockMock +} diff --git a/internal/testclient/testblock/godoc.go b/internal/testclient/testblock/godoc.go new file mode 100644 index 000000000..866bb4f70 --- /dev/null +++ b/internal/testclient/testblock/godoc.go @@ -0,0 +1,4 @@ +// Package testblock provides helper functions for constructing real (e.g. localnet) +// and mock BlockClient objects with pre-configured and/or parameterized call +// arguments, return value(s), and/or expectations thereof. Intended for use in tests. +package testblock diff --git a/internal/testclient/testeventsquery/client.go b/internal/testclient/testeventsquery/client.go index 0aa618fe9..2c68606ce 100644 --- a/internal/testclient/testeventsquery/client.go +++ b/internal/testclient/testeventsquery/client.go @@ -2,10 +2,13 @@ package testeventsquery import ( "context" + "fmt" "testing" "time" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" "github.com/pokt-network/poktroll/internal/mocks/mockclient" "github.com/pokt-network/poktroll/internal/testclient" @@ -15,14 +18,68 @@ import ( "github.com/pokt-network/poktroll/pkg/observable/channel" ) -// NewLocalnetClient returns a new events query client which is configured to -// connect to the localnet sequencer. +// NewLocalnetClient creates and returns a new events query client that's configured +// for use with the localnet sequencer. Any options provided are applied to the client. func NewLocalnetClient(t *testing.T, opts ...client.EventsQueryClientOption) client.EventsQueryClient { t.Helper() return eventsquery.NewEventsQueryClient(testclient.CometLocalWebsocketURL, opts...) } +// NewOneTimeEventsQuery creates a mock of the EventsQueryClient which expects +// a single call to the EventsBytes method. query is the query string which is +// expected to be received by that call. +// It returns a mock client whose event bytes method constructs a new observable. +// The caller can simulate blockchain events by sending on the value publishCh +// points to, which is set by this helper function. +func NewOneTimeEventsQuery( + ctx context.Context, + t *testing.T, + query string, + publishCh *chan<- either.Bytes, +) *mockclient.MockEventsQueryClient { + t.Helper() + ctrl := gomock.NewController(t) + + eventsQueryClient := mockclient.NewMockEventsQueryClient(ctrl) + eventsQueryClient.EXPECT().EventsBytes(gomock.Eq(ctx), gomock.Eq(query)). + DoAndReturn(func( + ctx context.Context, + query string, + ) (eventsBzObservable client.EventsBytesObservable, err error) { + eventsBzObservable, *publishCh = channel.NewObservable[either.Bytes]() + return eventsBzObservable, nil + }).Times(1) + return eventsQueryClient +} + +// NewOneTimeTxEventsQueryClient creates a mock of the Events that expects +// a single call to the EventsBytes method where the query is for transaction +// events for sender address matching that of the given key. +// The caller can simulate blockchain events by sending on the value publishCh +// points to, which is set by this helper function. +func NewOneTimeTxEventsQueryClient( + ctx context.Context, + t *testing.T, + key *cosmoskeyring.Record, + publishCh *chan<- either.Bytes, +) *mockclient.MockEventsQueryClient { + t.Helper() + + signingAddr, err := key.GetAddress() + require.NoError(t, err) + + expectedEventsQuery := fmt.Sprintf( + "tm.event='Tx' AND message.sender='%s'", + signingAddr, + ) + return NewOneTimeEventsQuery( + ctx, t, + expectedEventsQuery, + publishCh, + ) +} + // NewAnyTimesEventsBytesEventsQueryClient returns a new events query client which // is configured to return the expected event bytes when queried with the expected // query, any number of times. The returned client also expects to be closed once. diff --git a/internal/testclient/testeventsquery/godoc.go b/internal/testclient/testeventsquery/godoc.go new file mode 100644 index 000000000..0caa02997 --- /dev/null +++ b/internal/testclient/testeventsquery/godoc.go @@ -0,0 +1,5 @@ +// Package testeventsquery provides helper functions for constructing real +// (e.g. localnet) and mock EventsQueryClient objects with pre-configured and/or +// parameterized call arguments, return value(s), and/or expectations thereof. +// Intended for use in tests. +package testeventsquery diff --git a/internal/testclient/testtx/context.go b/internal/testclient/testtx/context.go index e7d1f8446..fa25494e7 100644 --- a/internal/testclient/testtx/context.go +++ b/internal/testclient/testtx/context.go @@ -30,25 +30,10 @@ import ( // correlations between these "times" values and the contexts in which the expected // methods may be called. -// NewOneTimeErrTxTimeoutTxContext creates a mock transaction context designed to simulate a specific -// timeout error scenario during transaction broadcasting. -// -// Parameters: -// - t: The testing.T instance for the current test. -// - keyring: The Cosmos SDK keyring containing the signer's cryptographic keys. -// - signingKeyName: The name of the key within the keyring to use for signing. -// - expectedTx: A pointer whose value will be set to the expected transaction -// bytes (in hexadecimal format). -// - expectedErrMsg: A pointer whose value will be set to the expected error -// message string. -// -// The function performs the following actions: -// 1. It retrieves the signer's cryptographic key from the provided keyring using the signingKeyName. -// 2. It computes the corresponding address of the signer's key. -// 3. It then formats an error message indicating that the fee payer's address does not exist. -// 4. It creates a base mock transaction context using NewBaseTxContext. -// 5. It sets up the mock behavior for the BroadcastTxSync method to return a specific preset response. -// 6. It also sets up the mock behavior for the QueryTx method to return a specific error response. +// NewOneTimeErrTxTimeoutTxContext creates a mock transaction context designed to +// simulate a specific timeout error scenario during transaction broadcasting. +// expectedErrMsg is populated with the same error message which is presented in +// the result from the QueryTx method so that it can be asserted against. func NewOneTimeErrTxTimeoutTxContext( t *testing.T, keyring cosmoskeyring.Keyring, @@ -116,24 +101,8 @@ func NewOneTimeErrTxTimeoutTxContext( // NewOneTimeErrCheckTxTxContext creates a mock transaction context to simulate // a specific error scenario during the ABCI check-tx phase (i.e., during initial // validation before the transaction is included in the block). -// -// Parameters: -// - t: The testing.T instance for the current test. -// - keyring: The Cosmos SDK keyring containing the signer's cryptographic keys. -// - signingKeyName: The name of the key within the keyring to be used for signing. -// - expectedTx: A pointer whose value will be set to the expected transaction -// bytes (in hexadecimal format). -// - expectedErrMsg: A pointer whose value will be set to the expected error -// message string. -// -// The function operates as follows: -// 1. Retrieves the signer's cryptographic key from the provided keyring based on -// the signingKeyName. -// 2. Determines the corresponding address of the signer's key. -// 3. Composes an error message suggesting that the fee payer's address is unrecognized. -// 4. Creates a base mock transaction context using the NewBaseTxContext function. -// 5. Sets up the mock behavior for the BroadcastTxSync method to return a specific -// error response related to the check phase of the transaction. +// expectedErrMsg is populated with the same error message which is presented in +// the result from the QueryTx method so that it can be asserted against. func NewOneTimeErrCheckTxTxContext( t *testing.T, keyring cosmoskeyring.Keyring, @@ -179,22 +148,7 @@ func NewOneTimeErrCheckTxTxContext( } // NewOneTimeTxTxContext creates a mock transaction context primed to respond with -// a single successful transaction response. This function facilitates testing by -// ensuring that the BroadcastTxSync method will return a specific, controlled response -// without actually broadcasting the transaction to the network. -// -// Parameters: -// - t: The testing.T instance used for the current test, typically passed from -// the calling test function. -// - keyring: The Cosmos SDK keyring containing the available cryptographic keys. -// - signingKeyName: The name of the key within the keyring used for transaction signing. -// - expectedTx: A pointer whose value will be set to the expected transaction -// bytes (in hexadecimal format). -// -// The function operates as follows: -// 1. Constructs a base mock transaction context using the NewBaseTxContext function. -// 2. Configures the mock behavior for the BroadcastTxSync method to return a pre-defined -// successful transaction response, ensuring that this behavior will only be triggered once. +// a single successful transaction response. func NewOneTimeTxTxContext( t *testing.T, keyring cosmoskeyring.Keyring, @@ -224,30 +178,11 @@ func NewOneTimeTxTxContext( return txCtxMock } -// NewBaseTxContext establishes a foundational mock transaction context with -// predefined behaviors suitable for a broad range of testing scenarios. It ensures -// that when interactions like transaction building, signing, and encoding occur -// in the test environment, they produce predictable and controlled outcomes. -// -// Parameters: -// - t: The testing.T instance used for the current test, typically passed from -// the calling test function. -// - signingKeyName: The name of the key within the keyring to be used for -// transaction signing. -// - keyring: The Cosmos SDK keyring containing the available cryptographic keys. -// - expectedTx: A pointer whose value will be set to the expected transaction -// bytes (in hexadecimal format). -// - expectedErrMsg: A pointer whose value will be set to the expected error -// message string. -// -// The function works as follows: -// 1. Invokes the NewAnyTimesTxTxContext to create a base mock transaction context. -// 2. Sets the expectation that NewTxBuilder method will be called exactly once. -// 3. Configures the mock behavior for the SignTx method to utilize the context's -// signing logic. -// 4. Overrides the EncodeTx method's behavior to intercept the encoding operation, -// capture the encoded transaction bytes, compute the transaction hash, and populate -// the expectedTx and expectedTxHash parameters accordingly. +// NewBaseTxContext creates a mock transaction context that's configured to expect +// calls to NewTxBuilder, SignTx, and EncodeTx methods, any number of times. +// EncodeTx is used to intercept the encoded transaction bytes and store them in +// the expectedTx output parameter. Each of these methods proxies to the corresponding +// method on a real transaction context. func NewBaseTxContext( t *testing.T, signingKeyName string, @@ -281,30 +216,6 @@ func NewBaseTxContext( // NewAnyTimesTxTxContext initializes a mock transaction context that's configured to allow // arbitrary calls to certain predefined interactions, primarily concerning the retrieval // of account numbers and sequences. -// -// Parameters: -// - t: The testing.T instance used for the current test, typically passed from the calling test function. -// - keyring: The Cosmos SDK keyring containing the available cryptographic keys. -// -// The function operates in the following manner: -// 1. Establishes a new gomock controller for setting up mock expectations and behaviors. -// 2. Prepares a set of flags suitable for localnet testing environments. -// 3. Sets up a mock behavior to intercept the GetAccountNumberSequence method calls, -// ensuring that whenever this method is invoked, it consistently returns an account number -// and sequence of 1, without making real queries to the underlying infrastructure. -// 4. Constructs a client context tailored for localnet testing with the provided keyring -// and the mocked account retriever. -// 5. Initializes a transaction factory from the client context and validates its integrity. -// 6. Injects the transaction factory and client context dependencies to create a new transaction context. -// 7. Creates a mock transaction context that always returns the provided keyring when the GetKeyring method is called. -// -// This setup aids tests by facilitating the creation of mock transaction contexts that have predictable -// and controlled outcomes for account number and sequence retrieval operations. -// -// Returns: -// - A mock transaction context suitable for setting additional expectations in tests. -// - A real transaction context initialized with the supplied dependencies. - func NewAnyTimesTxTxContext( t *testing.T, keyring cosmoskeyring.Keyring, diff --git a/pkg/client/godoc.go b/pkg/client/godoc.go new file mode 100644 index 000000000..66da550dd --- /dev/null +++ b/pkg/client/godoc.go @@ -0,0 +1,12 @@ +// Package client defines interfaces and types that facilitate interactions +// with blockchain functionalities, both transactional and observational. It is +// built to provide an abstraction layer for sending, receiving, and querying +// blockchain data, thereby offering a standardized way of integrating with +// various blockchain platforms. +// +// The client package leverages external libraries like cosmos-sdk and cometbft, +// but there is a preference to minimize direct dependencies on these external +// libraries, when defining interfaces, aiming for a cleaner decoupling. +// It seeks to provide a flexible and comprehensive interface layer, adaptable to +// different blockchain configurations and requirements. +package client diff --git a/pkg/client/interface.go b/pkg/client/interface.go index 2d67c90c0..32dab250c 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -1,4 +1,5 @@ //go:generate mockgen -destination=../../internal/mocks/mockclient/events_query_client_mock.go -package=mockclient . Dialer,Connection,EventsQueryClient +//go:generate mockgen -destination=../../internal/mocks/mockclient/block_client_mock.go -package=mockclient . Block,BlockClient //go:generate mockgen -destination=../../internal/mocks/mockclient/tx_client_mock.go -package=mockclient . TxContext //go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_tx_builder_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/client TxBuilder //go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_keyring_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/crypto/keyring Keyring @@ -18,9 +19,18 @@ import ( "github.com/pokt-network/poktroll/pkg/observable" ) +// TxClient provides a synchronous interface initiating and waiting for transactions +// derived from cosmos-sdk messages, in a cosmos-sdk based blockchain network. +type TxClient interface { + SignAndBroadcast( + ctx context.Context, + msgs ...cosmostypes.Msg, + ) either.AsyncError +} + // TxContext provides an interface which consolidates the operational dependencies -// required to facilitate the sender side of the tx lifecycle: build, sign, encode, -// broadcast, query (optional). +// required to facilitate the sender side of the transaction lifecycle: build, sign, +// encode, broadcast, and query (optional). // // TODO_IMPROVE: Avoid depending on cosmos-sdk structs or interfaces; add Pocket // interface types to substitute: @@ -29,13 +39,13 @@ import ( // - Keyring // - TxBuilder type TxContext interface { - // GetKeyring returns the associated key management mechanism for the tx context. + // GetKeyring returns the associated key management mechanism for the transaction context. GetKeyring() cosmoskeyring.Keyring - // NewTxBuilder creates and returns a new tx builder instance. + // NewTxBuilder creates and returns a new transaction builder instance. NewTxBuilder() cosmosclient.TxBuilder - // SignTx signs a tx using the specified key name. It can operate in offline mode, + // SignTx signs a transaction using the specified key name. It can operate in offline mode, // and can overwrite any existing signatures based on the provided flags. SignTx( keyName string, @@ -43,14 +53,14 @@ type TxContext interface { offline, overwriteSig bool, ) error - // EncodeTx takes a tx builder and encodes it, returning its byte representation. + // EncodeTx takes a transaction builder and encodes it, returning its byte representation. EncodeTx(txBuilder cosmosclient.TxBuilder) ([]byte, error) - // BroadcastTx broadcasts the given tx to the network. + // BroadcastTx broadcasts the given transaction to the network. BroadcastTx(txBytes []byte) (*cosmostypes.TxResponse, error) - // QueryTx retrieves a tx status based on its hash and optionally provides - // proof of the tx. + // QueryTx retrieves a transaction status based on its hash and optionally provides + // proof of the transaction. QueryTx( ctx context.Context, txHash []byte, @@ -60,6 +70,7 @@ type TxContext interface { // BlocksObservable is an observable which is notified with an either // value which contains either an error or the event message bytes. +// // TODO_HACK: The purpose of this type is to work around gomock's lack of // support for generic types. For the same reason, this type cannot be an // alias (i.e. EventsBytesObservable = observable.Observable[either.Either[[]byte]]). @@ -84,23 +95,24 @@ type Block interface { Hash() []byte } -// TODO_CONSIDERATION: the cosmos-sdk CLI code seems to use a cometbft RPC client -// which includes a `#Subscribe()` method for a similar purpose. Perhaps we could -// replace this custom websocket client with that. -// (see: https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110) -// (see: https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114) -// -// NOTE: a branch which attempts this is available at: -// https://github.com/pokt-network/poktroll/pull/74 - // EventsBytesObservable is an observable which is notified with an either // value which contains either an error or the event message bytes. +// // TODO_HACK: The purpose of this type is to work around gomock's lack of // support for generic types. For the same reason, this type cannot be an // alias (i.e. EventsBytesObservable = observable.Observable[either.Bytes]). type EventsBytesObservable observable.Observable[either.Bytes] // EventsQueryClient is used to subscribe to chain event messages matching the given query, +// +// TODO_CONSIDERATION: the cosmos-sdk CLI code seems to use a cometbft RPC client +// which includes a `#Subscribe()` method for a similar purpose. Perhaps we could +// replace our custom implementation with one which wraps that. +// (see: https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110) +// (see: https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114) +// +// NOTE: a branch which attempts this is available at: +// https://github.com/pokt-network/poktroll/pull/74 type EventsQueryClient interface { // EventsBytes returns an observable which is notified about chain event messages // matching the given query. It receives an either value which contains either an @@ -131,8 +143,8 @@ type Dialer interface { DialContext(ctx context.Context, urlStr string) (Connection, error) } -// EventsQueryClientOption is an interface-wide type which can be implemented to use or modify the -// query client during construction. This would likely be done in an -// implementation-specific way; e.g. using a type assertion to assign to an -// implementation struct field(s). +// EventsQueryClientOption defines a function type that modifies the EventsQueryClient. type EventsQueryClientOption func(EventsQueryClient) + +// TxClientOption defines a function type that modifies the TxClient. +type TxClientOption func(TxClient) diff --git a/pkg/client/services.go b/pkg/client/services.go new file mode 100644 index 000000000..0d2ca060d --- /dev/null +++ b/pkg/client/services.go @@ -0,0 +1,19 @@ +package client + +import ( + "fmt" + + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +// NewTestApplicationServiceConfig returns a slice of application service configs for testing. +func NewTestApplicationServiceConfig(prefix string, count int) []*sharedtypes.ApplicationServiceConfig { + appSvcCfg := make([]*sharedtypes.ApplicationServiceConfig, count) + for i, _ := range appSvcCfg { + serviceId := fmt.Sprintf("%s%d", prefix, i) + appSvcCfg[i] = &sharedtypes.ApplicationServiceConfig{ + ServiceId: &sharedtypes.ServiceId{Id: serviceId}, + } + } + return appSvcCfg +} diff --git a/pkg/client/tx/client.go b/pkg/client/tx/client.go new file mode 100644 index 000000000..805443eb4 --- /dev/null +++ b/pkg/client/tx/client.go @@ -0,0 +1,567 @@ +package tx + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "sync" + + "cosmossdk.io/depinject" + abciTypes "github.com/cometbft/cometbft/abci/types" + comettypes "github.com/cometbft/cometbft/types" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "go.uber.org/multierr" + + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" +) + +const ( + // DefaultCommitTimeoutHeightOffset is the default number of blocks after the + // latest block (when broadcasting) that a transactions should be considered + // errored if it has not been committed. + DefaultCommitTimeoutHeightOffset = 5 + // txWithSenderAddrQueryFmt is the query used to subscribe to cometbft transactions + // events where the sender address matches the interpolated address. + // (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) + txWithSenderAddrQueryFmt = "tm.event='Tx' AND message.sender='%s'" +) + +var _ client.TxClient = (*txClient)(nil) + +// txClient orchestrates building, signing, broadcasting, and querying of +// transactions. It maintains a single events query subscription to its own +// transactions (via the EventsQueryClient) in order to receive notifications +// regarding their status. +// It also depends on the BlockClient as a timer, synchronized to block height, +// to facilitate transaction timeout logic. If a transaction doesn't appear to +// have been committed by commitTimeoutHeightOffset number of blocks have elapsed, +// it is considered as timed out. Upon timeout, the client queries the network for +// the last status of the transaction, which is used to derive the asynchronous +// error that's populated in the either.AsyncError. +type txClient struct { + // TODO_TECHDEBT: this should be configurable & integrated w/ viper, flags, etc. + // commitTimeoutHeightOffset is the number of blocks after the latest block + // that a transactions should be considered errored if it has not been committed. + commitTimeoutHeightOffset int64 + // signingKeyName is the name of the key in the keyring to use for signing + // transactions. + signingKeyName string + // signingAddr is the address of the signing key referenced by signingKeyName. + // It is hydrated from the keyring by calling Keyring#Key() with signingKeyName. + signingAddr cosmostypes.AccAddress + // txCtx is the transactions context which encapsulates transactions building, signing, + // broadcasting, and querying, as well as keyring access. + txCtx client.TxContext + // eventsQueryClient is the client used to subscribe to transactions events from this + // sender. It is used to receive notifications about transactions events corresponding + // to transactions which it has constructed, signed, and broadcast. + eventsQueryClient client.EventsQueryClient + // blockClient is the client used to query for the latest block height. + // It is used to implement timout logic for transactions which weren't committed. + blockClient client.BlockClient + + // txsMutex protects txErrorChans and txTimeoutPool maps. + txsMutex sync.Mutex + // txErrorChans maps tx_hash->channel which will receive an error or nil, + // and close, when the transactions with the given hash is committed. + txErrorChans txErrorChansByHash + // txTimeoutPool maps timeout_block_height->map_of_txsByHash. It + // is used to ensure that transactions error channels receive and close in the event + // that they have not already by the given timeout height. + txTimeoutPool txTimeoutPool +} + +type ( + txTimeoutPool map[height]txErrorChansByHash + txErrorChansByHash map[txHash]chan error + height = int64 + txHash = string +) + +// TxEvent is used to deserialize incoming websocket messages from +// the transactions subscription. +type TxEvent struct { + // Tx is the binary representation of the tx hash. + Tx []byte `json:"tx"` + Events []abciTypes.Event `json:"events"` +} + +// NewTxClient attempts to construct a new TxClient using the given dependencies +// and options. +// +// It performs the following steps: +// 1. Initializes a default txClient with the default commit timeout height +// offset, an empty error channel map, and an empty transaction timeout pool. +// 2. Injects the necessary dependencies using depinject. +// 3. Applies any provided options to customize the client. +// 4. Validates and sets any missing default configurations using the +// validateConfigAndSetDefaults method. +// 5. Subscribes the client to its own transactions. This step might be +// reconsidered for relocation to a potential Start() method in the future. +func NewTxClient( + ctx context.Context, + deps depinject.Config, + opts ...client.TxClientOption, +) (client.TxClient, error) { + tClient := &txClient{ + commitTimeoutHeightOffset: DefaultCommitTimeoutHeightOffset, + txErrorChans: make(txErrorChansByHash), + txTimeoutPool: make(txTimeoutPool), + } + + if err := depinject.Inject( + deps, + &tClient.txCtx, + &tClient.eventsQueryClient, + &tClient.blockClient, + ); err != nil { + return nil, err + } + + for _, opt := range opts { + opt(tClient) + } + + if err := tClient.validateConfigAndSetDefaults(); err != nil { + return nil, err + } + + // Start an events query subscription for transactions originating from this + // client's signing address. + // TODO_CONSIDERATION: move this into a #Start() method + if err := tClient.subscribeToOwnTxs(ctx); err != nil { + return nil, err + } + + // Launch a separate goroutine to handle transaction timeouts. + // TODO_CONSIDERATION: move this into a #Start() method + go tClient.goTimeoutPendingTransactions(ctx) + + return tClient, nil +} + +// SignAndBroadcast signs a set of Cosmos SDK messages, constructs a transaction, +// and broadcasts it to the network. The function performs several steps to +// ensure the messages and the resultant transaction are valid: +// +// 1. Validates each message in the provided set. +// 2. Constructs the transaction using the Cosmos SDK's transaction builder. +// 3. Calculates and sets the transaction's timeout height. +// 4. Sets a default gas limit (note: this will be made configurable in the future). +// 5. Signs the transaction. +// 6. Validates the constructed transaction. +// 7. Serializes and broadcasts the transaction. +// 8. Checks the broadcast response for errors. +// 9. If all the above steps are successful, the function registers the +// transaction as pending. +// +// If any step encounters an error, it returns an either.AsyncError populated with +// the synchronous error. If the function completes successfully, it returns an +// either.AsyncError populated with the error channel which will receive if the +// transaction results in an asynchronous error or times out. +func (tClient *txClient) SignAndBroadcast( + ctx context.Context, + msgs ...cosmostypes.Msg, +) either.AsyncError { + var validationErrs error + for i, msg := range msgs { + if err := msg.ValidateBasic(); err != nil { + validationErr := ErrInvalidMsg.Wrapf("in msg with index %d: %s", i, err) + validationErrs = multierr.Append(validationErrs, validationErr) + } + } + if validationErrs != nil { + return either.SyncErr(validationErrs) + } + + // Construct the transactions using cosmos' transactions builder. + txBuilder := tClient.txCtx.NewTxBuilder() + if err := txBuilder.SetMsgs(msgs...); err != nil { + // return synchronous error + return either.SyncErr(err) + } + + // Calculate timeout height + timeoutHeight := tClient.blockClient.LatestBlock(ctx). + Height() + tClient.commitTimeoutHeightOffset + + // TODO_TECHDEBT: this should be configurable + txBuilder.SetGasLimit(200000) + txBuilder.SetTimeoutHeight(uint64(timeoutHeight)) + + // sign transactions + err := tClient.txCtx.SignTx( + tClient.signingKeyName, + txBuilder, + false, false, + ) + if err != nil { + return either.SyncErr(err) + } + + // ensure transactions is valid + // NOTE: this makes the transactions valid; i.e. it is *REQUIRED* + if err := txBuilder.GetTx().ValidateBasic(); err != nil { + return either.SyncErr(err) + } + + // serialize transactions + txBz, err := tClient.txCtx.EncodeTx(txBuilder) + if err != nil { + return either.SyncErr(err) + } + + txResponse, err := tClient.txCtx.BroadcastTx(txBz) + if err != nil { + return either.SyncErr(err) + } + + if txResponse.Code != 0 { + return either.SyncErr(ErrCheckTx.Wrapf(txResponse.RawLog)) + } + + return tClient.addPendingTransactions(normalizeTxHashHex(txResponse.TxHash), timeoutHeight) +} + +// validateConfigAndSetDefaults ensures that the necessary configurations for the +// txClient are set, and populates any missing defaults. +// +// 1. It checks if the signing key name is set and returns an error if it's empty. +// 2. It then retrieves the key record from the keyring using the signing key name +// and checks its existence. +// 3. The address of the signing key is computed and assigned to txClient#signgingAddr. +// 4. Lastly, it ensures that commitTimeoutHeightOffset has a valid value, setting +// it to DefaultCommitTimeoutHeightOffset if it's zero or negative. +// +// Returns: +// - ErrEmptySigningKeyName if the signing key name is not provided. +// - ErrNoSuchSigningKey if the signing key is not found in the keyring. +// - ErrSigningKeyAddr if there's an issue retrieving the address for the signing key. +// - nil if validation is successful and defaults are set appropriately. +func (tClient *txClient) validateConfigAndSetDefaults() error { + if tClient.signingKeyName == "" { + return ErrEmptySigningKeyName + } + + keyRecord, err := tClient.txCtx.GetKeyring().Key(tClient.signingKeyName) + if err != nil { + return ErrNoSuchSigningKey.Wrapf("name %q: %s", tClient.signingKeyName, err) + } + signingAddr, err := keyRecord.GetAddress() + if err != nil { + return ErrSigningKeyAddr.Wrapf("name %q: %s", tClient.signingKeyName, err) + } + tClient.signingAddr = signingAddr + + if tClient.commitTimeoutHeightOffset <= 0 { + tClient.commitTimeoutHeightOffset = DefaultCommitTimeoutHeightOffset + } + return nil +} + +// addPendingTransactions registers a new pending transaction for monitoring and +// notification of asynchronous errors. It accomplishes the following: +// +// 1. Creates an error notification channel (if one doesn't already exist) and associates +// it with the provided transaction hash in the txErrorChans map. +// +// 2. Ensures that there's an initialized map of transactions by hash for the +// given timeout height in the txTimeoutPool. The same error notification channel +// is also associated with the transaction hash in this map. +// +// Both txErrorChans and txTimeoutPool store references to the same error notification +// channel for a given transaction hash. This ensures idempotency of error handling +// for any given transaction between asynchronous, transaction-specific errors and +// transaction timeout logic. +// +// Note: The error channels are buffered to prevent blocking on send operations and +// are intended to convey a single error event. +// +// Returns: +// - An either.AsyncError populated with the error notification channel for the +// provided transaction hash. +func (tClient *txClient) addPendingTransactions( + txHash string, + timeoutHeight int64, +) either.AsyncError { + tClient.txsMutex.Lock() + defer tClient.txsMutex.Unlock() + + // Initialize txTimeoutPool map if necessary. + txsByHash, ok := tClient.txTimeoutPool[timeoutHeight] + if !ok { + txsByHash = make(map[string]chan error) + tClient.txTimeoutPool[timeoutHeight] = txsByHash + } + + // Initialize txErrorChans map in txTimeoutPool map if necessary. + errCh, ok := txsByHash[txHash] + if !ok { + // NB: intentionally buffered to avoid blocking on send. Only intended + // to send/receive a single error. + errCh = make(chan error, 1) + txsByHash[txHash] = errCh + } + + // Initialize txErrorChans map if necessary. + if _, ok := tClient.txErrorChans[txHash]; !ok { + // NB: both maps hold a reference to the same channel so that we can check + // if the channel has already been closed when timing out. + tClient.txErrorChans[txHash] = errCh + } + + return either.AsyncErr(errCh) +} + +// subscribeToOwnTxs establishes an event query subscription to monitor transactions +// originating from this client's signing address. +// +// It performs the following steps: +// +// 1. Forms a query to fetch transaction events specific to the client's signing address. +// 2. Maps raw event bytes observable notifications to a new transaction event objects observable. +// 3. Handle each transaction event. +// +// Important considerations: +// There's uncertainty surrounding the potential for asynchronous errors post transaction broadcast. +// Current implementation and observations suggest that errors might be returned synchronously, +// even when using Cosmos' BroadcastTxAsync method. Further investigation is required. +// +// This function also spawns a goroutine to handle transaction timeouts via goTimeoutPendingTransactions. +// +// Parameters: +// - ctx: Context for managing the function's lifecycle and child operations. +// +// Returns: +// - An error if there's a failure during the event query or subscription process. +func (tClient *txClient) subscribeToOwnTxs(ctx context.Context) error { + // Form a query based on the client's signing address. + query := fmt.Sprintf(txWithSenderAddrQueryFmt, tClient.signingAddr) + + // Fetch transaction events matching the query. + eventsBz, err := tClient.eventsQueryClient.EventsBytes(ctx, query) + if err != nil { + return err + } + + // Convert raw event data into a stream of transaction events. + txEventsObservable := channel.Map[ + either.Bytes, either.Either[*TxEvent], + ](ctx, eventsBz, tClient.txEventFromEventBz) + txEventsObserver := txEventsObservable.Subscribe(ctx) + + // Handle transaction events asynchronously. + go tClient.goHandleTxEvents(txEventsObserver) + + return nil +} + +// goHandleTxEvents ranges over the transaction events observable, performing +// the following steps on each: +// +// 1. Normalize hexadeimal transaction hash. +// 2. Retrieves the transaction's error channel from txErrorChans. +// 3. Closes and removes it from txErrorChans. +// 4. Removes the transaction error channel from txTimeoutPool. +// +// It is intended to be called in a goroutine. +func (tClient *txClient) goHandleTxEvents( + txEventsObserver observable.Observer[either.Either[*TxEvent]], +) { + for eitherTxEvent := range txEventsObserver.Ch() { + txEvent, err := eitherTxEvent.ValueOrError() + if err != nil { + return + } + + // Convert transaction hash into its normalized hex form. + txHashHex := txHashBytesToNormalizedHex(comettypes.Tx(txEvent.Tx).Hash()) + + tClient.txsMutex.Lock() + + // Check for a corresponding error channel in the map. + txErrCh, ok := tClient.txErrorChans[txHashHex] + if !ok { + panic("Received tx event without an associated error channel.") + } + + // TODO_INVESTIGATE: it seems like it may not be possible for the + // txEvent to represent an error. Cosmos' #BroadcastTxSync() is being + // called internally, which will return an error if the transaction + // is not accepted by the mempool. + // + // It's unclear if a cosmos chain is capable of returning an async + // error for a transaction at this point; even when substituting + // #BroadcastTxAsync(), the error is returned synchronously: + // + // > error in json rpc client, with http response metadata: (Status: + // > 200 OK, Protocol HTTP/1.1). RPC error -32000 - tx added to local + // > mempool but failed to gossip: validation failed + // + // Potential parse and send transaction error on txErrCh here. + + // Close and remove from txErrChans + close(txErrCh) + delete(tClient.txErrorChans, txHashHex) + + // Remove from the txTimeoutPool. + for timeoutHeight, txErrorChans := range tClient.txTimeoutPool { + // Handled transaction isn't in this timeout height. + if _, ok := txErrorChans[txHashHex]; !ok { + continue + } + + delete(txErrorChans, txHashHex) + if len(txErrorChans) == 0 { + delete(tClient.txTimeoutPool, timeoutHeight) + } + } + + tClient.txsMutex.Unlock() + } +} + +// goTimeoutPendingTransactions monitors blocks and handles transaction timeouts. +// For each block observed, it checks if there are transactions associated with that +// block's height in the txTimeoutPool. If transactions are found, the function +// evaluates whether they have already been processed by the transaction events +// query subscription logic. If not, a timeout error is generated and sent on the +// transaction's error channel. Finally, the error channel is closed and removed +// from the txTimeoutPool. +func (tClient *txClient) goTimeoutPendingTransactions(ctx context.Context) { + // Subscribe to a sequence of committed blocks. + blockCh := tClient.blockClient.CommittedBlocksSequence(ctx).Subscribe(ctx).Ch() + + // Iterate over each incoming block. + for block := range blockCh { + select { + case <-ctx.Done(): + // Exit if the context signals done. + return + default: + } + + tClient.txsMutex.Lock() + + // Retrieve transactions associated with the current block's height. + txsByHash, ok := tClient.txTimeoutPool[block.Height()] + if !ok { + // If no transactions are found for the current block height, continue. + tClient.txsMutex.Unlock() + continue + } + + // Process each transaction for the current block height. + for txHash, txErrCh := range txsByHash { + select { + // Check if the transaction was processed by its subscription. + case err, ok := <-txErrCh: + if ok { + // Unexpected state: error channel should be closed after processing. + panic(fmt.Errorf("Expected txErrCh to be closed; received err: %w", err)) + } + // Remove the processed transaction. + delete(txsByHash, txHash) + tClient.txsMutex.Unlock() + continue + default: + } + + // Transaction was not processed by its subscription: handle timeout. + txErrCh <- tClient.getTxTimeoutError(ctx, txHash) // Send a timeout error. + close(txErrCh) // Close the error channel. + delete(txsByHash, txHash) // Remove the transaction. + } + + // Clean up the txTimeoutPool for the current block height. + delete(tClient.txTimeoutPool, block.Height()) + tClient.txsMutex.Unlock() + } +} + +// txEventFromEventBz deserializes a binary representation of a transaction event +// into a TxEvent structure. +// +// Parameters: +// - eitherEventBz: Binary data of the event, potentially encapsulating an error. +// +// Returns: +// - eitherTxEvent: The TxEvent or an encapsulated error, facilitating clear +// error management in the caller's context. +// - skip: A flag denoting if the event should be bypassed. A value of true +// suggests the event be disregarded, progressing to the succeeding message. +func (tClient *txClient) txEventFromEventBz( + eitherEventBz either.Bytes, +) (eitherTxEvent either.Either[*TxEvent], skip bool) { + + // Extract byte data from the given event. In case of failure, wrap the error + // and denote the event for skipping. + eventBz, err := eitherEventBz.ValueOrError() + if err != nil { + return either.Error[*TxEvent](err), true + } + + // Unmarshal byte data into a TxEvent object. + txEvt, err := tClient.unmarshalTxEvent(eventBz) + switch { + // If the error indicates a non-transactional event, return the TxEvent and + // signal for skipping. + case errors.Is(err, ErrNonTxEventBytes): + return either.Success(txEvt), true + // For other errors, wrap them and flag the event to be skipped. + case err != nil: + return either.Error[*TxEvent](ErrUnmarshalTx.Wrapf("%s", err)), true + } + + // For successful unmarshalling, return the TxEvent. + return either.Success(txEvt), false +} + +// unmarshalTxEvent attempts to deserialize a slice of bytes into a TxEvent. +// It checks if the given bytes correspond to a valid transaction event. +// If the resulting TxEvent has empty transaction bytes, it assumes that +// the message was not a transaction event and returns an ErrNonTxEventBytes error. +func (tClient *txClient) unmarshalTxEvent(eventBz []byte) (*TxEvent, error) { + txEvent := new(TxEvent) + + // Try to deserialize the provided bytes into a TxEvent. + if err := json.Unmarshal(eventBz, txEvent); err != nil { + return nil, err + } + + // Check if the TxEvent has empty transaction bytes, which indicates + // the message might not be a valid transaction event. + if bytes.Equal(txEvent.Tx, []byte{}) { + return nil, ErrNonTxEventBytes.Wrapf("%s", string(eventBz)) + } + + return txEvent, nil +} + +// getTxTimeoutError checks if a transaction with the specified hash has timed out. +// The function decodes the provided hexadecimal hash into bytes and queries the +// transaction using the byte hash. If any error occurs during this process, +// appropriate wrapped errors are returned for easier debugging. +func (tClient *txClient) getTxTimeoutError(ctx context.Context, txHashHex string) error { + + // Decode the provided hex hash into bytes. + txHash, err := hex.DecodeString(txHashHex) + if err != nil { + return ErrInvalidTxHash.Wrapf("%s", txHashHex) + } + + // Query the transaction using the decoded byte hash. + txResponse, err := tClient.txCtx.QueryTx(ctx, txHash, false) + if err != nil { + return ErrQueryTx.Wrapf("with hash: %s: %s", txHashHex, err) + } + + // Return a timeout error with details about the transaction. + return ErrTxTimeout.Wrapf("with hash %s: %s", txHashHex, txResponse.TxResult.Log) +} diff --git a/pkg/client/tx/client_integration_test.go b/pkg/client/tx/client_integration_test.go new file mode 100644 index 000000000..c2d5db6e5 --- /dev/null +++ b/pkg/client/tx/client_integration_test.go @@ -0,0 +1,65 @@ +//go:build integration + +package tx_test + +import ( + "context" + "testing" + + "cosmossdk.io/depinject" + "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/pkg/client/tx" + + "github.com/pokt-network/poktroll/internal/testclient/testblock" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/internal/testclient/testtx" + "github.com/pokt-network/poktroll/pkg/client" + apptypes "github.com/pokt-network/poktroll/x/application/types" +) + +func TestTxClient_SignAndBroadcast_Integration(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test depends on some setup which is currently not implemented in this test: staked application and servicer with matching services") + + var ctx = context.Background() + + keyring, signingKey := newTestKeyringWithKey(t) + + eventsQueryClient := testeventsquery.NewLocalnetClient(t) + + _, txCtx := testtx.NewAnyTimesTxTxContext(t, keyring) + + // Construct a new mock block client because it is a required dependency. Since + // we're not exercising transactions timeouts in this test, we don't need to set any + // particular expectations on it, nor do we care about the value of blockHash + // argument. + blockClientMock := testblock.NewLocalnetClient(ctx, t) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtx, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient(ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName)) + require.NoError(t, err) + + signingKeyAddr, err := signingKey.GetAddress() + require.NoError(t, err) + + // Construct a valid (arbitrary) message to sign, encode, and broadcast. + appStake := types.NewCoin("upokt", types.NewInt(1000000)) + appStakeMsg := &apptypes.MsgStakeApplication{ + Address: signingKeyAddr.String(), + Stake: &appStake, + Services: client.NewTestApplicationServiceConfig(testServiceIdPrefix, 2), + } + + // Sign and broadcast the message. + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, _ = eitherErr.SyncOrAsyncError() + require.NoError(t, err) +} diff --git a/pkg/client/tx/client_test.go b/pkg/client/tx/client_test.go new file mode 100644 index 000000000..6cb5d33da --- /dev/null +++ b/pkg/client/tx/client_test.go @@ -0,0 +1,413 @@ +package tx_test + +import ( + "context" + "encoding/json" + "testing" + "time" + + "cosmossdk.io/depinject" + cometbytes "github.com/cometbft/cometbft/libs/bytes" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/internal/testclient/testblock" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/internal/testclient/testtx" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/tx" + "github.com/pokt-network/poktroll/pkg/either" + apptypes "github.com/pokt-network/poktroll/x/application/types" +) + +const ( + testSigningKeyName = "test_signer" + // NB: testServiceIdPrefix must not be longer than 7 characters due to + // maxServiceIdLen. + testServiceIdPrefix = "testsvc" + txCommitTimeout = 10 * time.Millisecond +) + +// TODO_TECHDEBT: add coverage for the transactions client handling an events bytes error either. + +func TestTxClient_SignAndBroadcast_Succeeds(t *testing.T) { + var ( + // expectedTx is the expected transactions bytes that will be signed and broadcast + // by the transaction client. It is computed and assigned in the + // testtx.NewOneTimeTxTxContext helper function. The same reference needs + // to be used across the expectations that are set on the transactions context mock. + expectedTx cometbytes.HexBytes + // eventsBzPublishCh is the channel that the mock events query client + // will use to publish the transactions event bytes. It is used near the end of + // the test to mock the network signaling that the transactions was committed. + eventsBzPublishCh chan<- either.Bytes + blocksPublishCh chan client.Block + ctx = context.Background() + ) + + keyring, signingKey := newTestKeyringWithKey(t) + + eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( + ctx, t, signingKey, &eventsBzPublishCh, + ) + + txCtxMock := testtx.NewOneTimeTxTxContext( + t, keyring, + testSigningKeyName, + &expectedTx, + ) + + // Construct a new mock block client because it is a required dependency. + // Since we're not exercising transactions timeouts in this test, we don't need to + // set any particular expectations on it, nor do we care about the contents + // of the latest block. + blockClientMock := testblock.NewOneTimeCommittedBlocksSequenceBlockClient( + t, blocksPublishCh, + ) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient( + ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName), + ) + require.NoError(t, err) + + signingKeyAddr, err := signingKey.GetAddress() + require.NoError(t, err) + + // Construct a valid (arbitrary) message to sign, encode, and broadcast. + appStake := types.NewCoin("upokt", types.NewInt(1000000)) + appStakeMsg := &apptypes.MsgStakeApplication{ + Address: signingKeyAddr.String(), + Stake: &appStake, + Services: client.NewTestApplicationServiceConfig(testServiceIdPrefix, 2), + } + + // Sign and broadcast the message. + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, errCh := eitherErr.SyncOrAsyncError() + require.NoError(t, err) + + // Construct the expected transaction event bytes from the expected transaction bytes. + txEventBz, err := json.Marshal(&tx.TxEvent{Tx: expectedTx}) + require.NoError(t, err) + + // Publish the transaction event bytes to the events query client so that the transaction client + // registers the transactions as committed (i.e. removes it from the timeout pool). + eventsBzPublishCh <- either.Success[[]byte](txEventBz) + + // Assert that the error channel was closed without receiving. + select { + case err, ok := <-errCh: + require.NoError(t, err) + require.Falsef(t, ok, "expected errCh to be closed") + case <-time.After(txCommitTimeout): + t.Fatal("test timed out waiting for errCh to receive") + } +} + +func TestTxClient_NewTxClient_Error(t *testing.T) { + // Construct an empty in-memory keyring. + keyring := cosmoskeyring.NewInMemory(testclient.EncodingConfig.Marshaler) + + tests := []struct { + name string + signingKeyName string + expectedErr error + }{ + { + name: "empty signing key name", + signingKeyName: "", + expectedErr: tx.ErrEmptySigningKeyName, + }, + { + name: "signing key does not exist", + signingKeyName: "nonexistent", + expectedErr: tx.ErrNoSuchSigningKey, + }, + // TODO_TECHDEBT: add coverage for this error case + // { + // name: "failed to get address", + // testSigningKeyName: "incompatible", + // expectedErr: tx.ErrSigningKeyAddr, + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + ctrl = gomock.NewController(t) + ctx = context.Background() + ) + + // Construct a new mock events query client. Since we expect the + // NewTxClient call to fail, we don't need to set any expectations + // on this mock. + eventsQueryClient := mockclient.NewMockEventsQueryClient(ctrl) + + // Construct a new mock transactions context. + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, keyring) + + // Construct a new mock block client. Since we expect the NewTxClient + // call to fail, we don't need to set any expectations on this mock. + blockClientMock := mockclient.NewMockBlockClient(ctrl) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct a signing key option using the test signing key name. + signingKeyOpt := tx.WithSigningKeyName(tt.signingKeyName) + + // Attempt to create the transactions client. + txClient, err := tx.NewTxClient(ctx, txClientDeps, signingKeyOpt) + require.ErrorIs(t, err, tt.expectedErr) + require.Nil(t, txClient) + }) + } +} + +func TestTxClient_SignAndBroadcast_SyncError(t *testing.T) { + var ( + // eventsBzPublishCh is the channel that the mock events query client + // will use to publish the transactions event bytes. It is not used in + // this test but is required to use the NewOneTimeTxEventsQueryClient + // helper. + eventsBzPublishCh chan<- either.Bytes + // blocksPublishCh is the channel that the mock block client will use + // to publish the latest block. It is not used in this test but is + // required to use the NewOneTimeCommittedBlocksSequenceBlockClient + // helper. + blocksPublishCh chan client.Block + ctx = context.Background() + ) + + keyring, signingKey := newTestKeyringWithKey(t) + + // Construct a new mock events query client. Since we expect the + // NewTxClient call to fail, we don't need to set any expectations + // on this mock. + eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( + ctx, t, signingKey, &eventsBzPublishCh, + ) + + // Construct a new mock transaction context. + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, keyring) + + // Construct a new mock block client because it is a required dependency. + // Since we're not exercising transactions timeouts in this test, we don't need to + // set any particular expectations on it, nor do we care about the contents + // of the latest block. + blockClientMock := testblock.NewOneTimeCommittedBlocksSequenceBlockClient( + t, blocksPublishCh, + ) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient( + ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName), + ) + require.NoError(t, err) + + // Construct an invalid (arbitrary) message to sign, encode, and broadcast. + signingAddr, err := signingKey.GetAddress() + require.NoError(t, err) + appStakeMsg := &apptypes.MsgStakeApplication{ + // Providing address to avoid panic from #GetSigners(). + Address: signingAddr.String(), + Stake: nil, + // NB: explicitly omitting required fields + } + + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, _ = eitherErr.SyncOrAsyncError() + require.ErrorIs(t, err, tx.ErrInvalidMsg) + + time.Sleep(10 * time.Millisecond) +} + +// TODO_INCOMPLETE: add coverage for async error; i.e. insufficient gas or on-chain error +func TestTxClient_SignAndBroadcast_CheckTxError(t *testing.T) { + var ( + // expectedErrMsg is the expected error message that will be returned + // by the transaction client. It is computed and assigned in the + // testtx.NewOneTimeErrCheckTxTxContext helper function. + expectedErrMsg string + // eventsBzPublishCh is the channel that the mock events query client + // will use to publish the transactions event bytes. It is used near the end of + // the test to mock the network signaling that the transactions was committed. + eventsBzPublishCh chan<- either.Bytes + blocksPublishCh chan client.Block + ctx = context.Background() + ) + + keyring, signingKey := newTestKeyringWithKey(t) + + eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( + ctx, t, signingKey, &eventsBzPublishCh, + ) + + txCtxMock := testtx.NewOneTimeErrCheckTxTxContext( + t, keyring, + testSigningKeyName, + &expectedErrMsg, + ) + + // Construct a new mock block client because it is a required dependency. + // Since we're not exercising transactions timeouts in this test, we don't need to + // set any particular expectations on it, nor do we care about the contents + // of the latest block. + blockClientMock := testblock.NewOneTimeCommittedBlocksSequenceBlockClient( + t, blocksPublishCh, + ) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient(ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName)) + require.NoError(t, err) + + signingKeyAddr, err := signingKey.GetAddress() + require.NoError(t, err) + + // Construct a valid (arbitrary) message to sign, encode, and broadcast. + appStake := types.NewCoin("upokt", types.NewInt(1000000)) + appStakeMsg := &apptypes.MsgStakeApplication{ + Address: signingKeyAddr.String(), + Stake: &appStake, + Services: client.NewTestApplicationServiceConfig(testServiceIdPrefix, 2), + } + + // Sign and broadcast the message. + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, _ = eitherErr.SyncOrAsyncError() + require.ErrorIs(t, err, tx.ErrCheckTx) + require.ErrorContains(t, err, expectedErrMsg) +} + +func TestTxClient_SignAndBroadcast_Timeout(t *testing.T) { + var ( + // expectedErrMsg is the expected error message that will be returned + // by the transaction client. It is computed and assigned in the + // testtx.NewOneTimeErrCheckTxTxContext helper function. + expectedErrMsg string + // eventsBzPublishCh is the channel that the mock events query client + // will use to publish the transaction event bytes. It is used near the end of + // the test to mock the network signaling that the transaction was committed. + eventsBzPublishCh chan<- either.Bytes + blocksPublishCh = make(chan client.Block, tx.DefaultCommitTimeoutHeightOffset) + ctx = context.Background() + ) + + keyring, signingKey := newTestKeyringWithKey(t) + + eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( + ctx, t, signingKey, &eventsBzPublishCh, + ) + + txCtxMock := testtx.NewOneTimeErrTxTimeoutTxContext( + t, keyring, + testSigningKeyName, + &expectedErrMsg, + ) + + // Construct a new mock block client because it is a required dependency. + // Since we're not exercising transaction timeouts in this test, we don't need to + // set any particular expectations on it, nor do we care about the contents + // of the latest block. + blockClientMock := testblock.NewOneTimeCommittedBlocksSequenceBlockClient( + t, blocksPublishCh, + ) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient( + ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName), + ) + require.NoError(t, err) + + signingKeyAddr, err := signingKey.GetAddress() + require.NoError(t, err) + + // Construct a valid (arbitrary) message to sign, encode, and broadcast. + appStake := types.NewCoin("upokt", types.NewInt(1000000)) + appStakeMsg := &apptypes.MsgStakeApplication{ + Address: signingKeyAddr.String(), + Stake: &appStake, + Services: client.NewTestApplicationServiceConfig(testServiceIdPrefix, 2), + } + + // Sign and broadcast the message in a transaction. + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, errCh := eitherErr.SyncOrAsyncError() + require.NoError(t, err) + + for i := 0; i < tx.DefaultCommitTimeoutHeightOffset; i++ { + blocksPublishCh <- testblock.NewAnyTimesBlock(t, []byte{}, int64(i+1)) + } + + // Assert that we receive the expected error type & message. + select { + case err := <-errCh: + require.ErrorIs(t, err, tx.ErrTxTimeout) + require.ErrorContains(t, err, expectedErrMsg) + // NB: wait 110% of txCommitTimeout; a bit longer than strictly necessary in + // order to mitigate flakiness. + case <-time.After(txCommitTimeout * 110 / 100): + t.Fatal("test timed out waiting for errCh to receive") + } + + // Assert that the error channel was closed. + select { + case err, ok := <-errCh: + require.Falsef(t, ok, "expected errCh to be closed") + require.NoError(t, err) + // NB: Give the error channel some time to be ready to receive in order to + // mitigate flakiness. + case <-time.After(50 * time.Millisecond): + t.Fatal("expected errCh to be closed") + } +} + +// TODO_TECHDEBT: add coverage for sending multiple messages simultaneously +func TestTxClient_SignAndBroadcast_MultipleMsgs(t *testing.T) { + t.SkipNow() +} + +// newTestKeyringWithKey creates a new in-memory keyring with a test key +// with testSigningKeyName as its name. +func newTestKeyringWithKey(t *testing.T) (cosmoskeyring.Keyring, *cosmoskeyring.Record) { + keyring := cosmoskeyring.NewInMemory(testclient.EncodingConfig.Marshaler) + key, _ := testclient.NewKey(t, testSigningKeyName, keyring) + return keyring, key +} diff --git a/pkg/client/tx/context.go b/pkg/client/tx/context.go index 5865ae526..eca32f943 100644 --- a/pkg/client/tx/context.go +++ b/pkg/client/tx/context.go @@ -22,7 +22,7 @@ type cosmosTxContext struct { // Holds cosmos-sdk client context. // (see: https://pkg.go.dev/github.com/cosmos/cosmos-sdk@v0.47.5/client#Context) clientCtx cosmosclient.Context - // Holds the cosmos-sdk tx factory. + // Holds the cosmos-sdk transaction factory. // (see: https://pkg.go.dev/github.com/cosmos/cosmos-sdk@v0.47.5/client/tx#Factory) txFactory cosmostx.Factory } @@ -67,7 +67,7 @@ func (txCtx cosmosTxContext) SignTx( ) } -// NewTxBuilder returns a new tx builder instance using the cosmos-sdk client tx config. +// NewTxBuilder returns a new transaction builder instance using the cosmos-sdk client transaction config. func (txCtx cosmosTxContext) NewTxBuilder() cosmosclient.TxBuilder { return txCtx.clientCtx.TxConfig.NewTxBuilder() } @@ -77,14 +77,15 @@ func (txCtx cosmosTxContext) EncodeTx(txBuilder cosmosclient.TxBuilder) ([]byte, return txCtx.clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) } -// BroadcastTx broadcasts the given tx to the network, blocking until the check-tx -// ABCI operation completes and returns a TxResponse of the tx status at that point in time. +// BroadcastTx broadcasts the given transaction to the network, blocking until the check-tx +// ABCI operation completes and returns a TxResponse of the transaction status at that point in time. func (txCtx cosmosTxContext) BroadcastTx(txBytes []byte) (*cosmostypes.TxResponse, error) { - return txCtx.clientCtx.BroadcastTxSync(txBytes) + return txCtx.clientCtx.BroadcastTxAsync(txBytes) + //return txCtx.clientCtx.BroadcastTxSync(txBytes) } // QueryTx queries the transaction based on its hash and optionally provides proof -// of the transaction. It returns the tx query result. +// of the transaction. It returns the transaction query result. func (txCtx cosmosTxContext) QueryTx( ctx context.Context, txHash []byte, diff --git a/pkg/client/tx/encoding.go b/pkg/client/tx/encoding.go new file mode 100644 index 000000000..78612e7b7 --- /dev/null +++ b/pkg/client/tx/encoding.go @@ -0,0 +1,18 @@ +package tx + +import ( + "fmt" + "strings" +) + +// normalizeTxHashHex defines canonical and unambiguous representation for a +// transaction hash hexadecimal string; lower-case. +func normalizeTxHashHex(txHash string) string { + return strings.ToLower(txHash) +} + +// txHashBytesToNormalizedHex converts a transaction hash bytes to a normalized +// hexadecimal string representation. +func txHashBytesToNormalizedHex(txHash []byte) string { + return normalizeTxHashHex(fmt.Sprintf("%x", txHash)) +} diff --git a/pkg/client/tx/errors.go b/pkg/client/tx/errors.go new file mode 100644 index 000000000..474f2ac19 --- /dev/null +++ b/pkg/client/tx/errors.go @@ -0,0 +1,53 @@ +package tx + +import errorsmod "cosmossdk.io/errors" + +var ( + // ErrEmptySigningKeyName represents an error which indicates that the + // provided signing key name is empty or unspecified. + ErrEmptySigningKeyName = errorsmod.Register(codespace, 1, "empty signing key name") + + // ErrNoSuchSigningKey represents an error signifying that the requested + // signing key does not exist or could not be located. + ErrNoSuchSigningKey = errorsmod.Register(codespace, 2, "signing key does not exist") + + // ErrSigningKeyAddr is raised when there's a failure in retrieving the + // associated address for the provided signing key. + ErrSigningKeyAddr = errorsmod.Register(codespace, 3, "failed to get address for signing key") + + // ErrInvalidMsg signifies that there was an issue in validating the + // transaction message. This could be due to format, content, or other + // constraints imposed on the message. + ErrInvalidMsg = errorsmod.Register(codespace, 4, "failed to validate tx message") + + // ErrCheckTx indicates an error occurred during the ABCI check transaction + // process, which verifies the transaction's integrity before it is added + // to the mempool. + ErrCheckTx = errorsmod.Register(codespace, 5, "error during ABCI check tx") + + // ErrTxTimeout is raised when a transaction has taken too long to + // complete, surpassing a predefined threshold. + ErrTxTimeout = errorsmod.Register(codespace, 6, "tx timed out") + + // ErrQueryTx indicates an error occurred while trying to query for the status + // of a specific transaction, likely due to issues with the query parameters + // or the state of the blockchain network. + ErrQueryTx = errorsmod.Register(codespace, 7, "error encountered while querying for tx") + + // ErrInvalidTxHash represents an error which is triggered when the + // transaction hash provided does not adhere to the expected format or + // constraints, implying it may be corrupted or tampered with. + ErrInvalidTxHash = errorsmod.Register(codespace, 8, "invalid tx hash") + + // ErrNonTxEventBytes indicates an attempt to deserialize bytes that do not + // correspond to a transaction event. This error is triggered when the provided + // byte data isn't recognized as a valid transaction event representation. + ErrNonTxEventBytes = errorsmod.Register(codespace, 9, "attempted to deserialize non-tx event bytes") + + // ErrUnmarshalTx signals a failure in the unmarshalling process of a transaction. + // This error is triggered when the system encounters issues translating a set of + // bytes into the corresponding Tx structure or object. + ErrUnmarshalTx = errorsmod.Register(codespace, 10, "failed to unmarshal tx") + + codespace = "tx_client" +) diff --git a/pkg/client/tx/options.go b/pkg/client/tx/options.go new file mode 100644 index 000000000..34e782b6d --- /dev/null +++ b/pkg/client/tx/options.go @@ -0,0 +1,22 @@ +package tx + +import ( + "github.com/pokt-network/poktroll/pkg/client" +) + +// WithCommitTimeoutBlocks sets the timeout duration in terms of number of blocks +// for the client to wait for broadcast transactions to be committed before +// returning a timeout error. +func WithCommitTimeoutBlocks(timeout int64) client.TxClientOption { + return func(client client.TxClient) { + client.(*txClient).commitTimeoutHeightOffset = timeout + } +} + +// WithSigningKeyName sets the name of the key which should be retrieved from the +// keyring and used for signing transactions. +func WithSigningKeyName(keyName string) client.TxClientOption { + return func(client client.TxClient) { + client.(*txClient).signingKeyName = keyName + } +} diff --git a/pkg/observable/channel/replay.go b/pkg/observable/channel/replay.go index 583edb4e5..a3935543d 100644 --- a/pkg/observable/channel/replay.go +++ b/pkg/observable/channel/replay.go @@ -38,8 +38,9 @@ type replayObservable[V any] struct { func NewReplayObservable[V any]( ctx context.Context, replayBufferSize int, + opts ...option[V], ) (observable.ReplayObservable[V], chan<- V) { - obsvbl, publishCh := NewObservable[V]() + obsvbl, publishCh := NewObservable[V](opts...) return ToReplayObservable[V](ctx, replayBufferSize, obsvbl), publishCh } From aecdf18c007e94e51092009c77e02cbd2bcd4bb7 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Tue, 7 Nov 2023 09:44:14 +0100 Subject: [PATCH 27/28] [Off-chain] refactor: keyring errors & helpers (#131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add `TxClient` interface * chore: add option support to `ReplayObservable` * feat: add `txClient` implementation * test: `txClient` * test: tx client integration * chore: s/tx/transaction/g * chore: update pkg README.md template * wip: client pkg README * docs: fix client pkg godoc comment * refactor: consolidate keyring errors & helpers * refactor: keyring test helpers * fix: flakey test * chore: dial back godoc comments 😅 * chore: revise (and move to godoc.go) `testblock` & `testeventsquery` pkg godoc comment * chore: update go.mod * chore: refactor & condense godoc comments * chore: fix import paths post-update --- internal/testclient/testeventsquery/client.go | 18 +++++++----- internal/testclient/testkeyring/keyring.go | 17 +++++++++++ pkg/client/keyring/errors.go | 19 ++++++++++++ pkg/client/keyring/keyring.go | 29 +++++++++++++++++++ pkg/client/tx/client.go | 17 +++++------ pkg/client/tx/client_integration_test.go | 3 +- pkg/client/tx/client_test.go | 26 +++++++---------- pkg/client/tx/errors.go | 12 -------- 8 files changed, 94 insertions(+), 47 deletions(-) create mode 100644 internal/testclient/testkeyring/keyring.go create mode 100644 pkg/client/keyring/errors.go create mode 100644 pkg/client/keyring/keyring.go diff --git a/internal/testclient/testeventsquery/client.go b/internal/testclient/testeventsquery/client.go index 2c68606ce..fbf7daeb1 100644 --- a/internal/testclient/testeventsquery/client.go +++ b/internal/testclient/testeventsquery/client.go @@ -27,11 +27,12 @@ func NewLocalnetClient(t *testing.T, opts ...client.EventsQueryClientOption) cli } // NewOneTimeEventsQuery creates a mock of the EventsQueryClient which expects -// a single call to the EventsBytes method. query is the query string which is -// expected to be received by that call. -// It returns a mock client whose event bytes method constructs a new observable. -// The caller can simulate blockchain events by sending on the value publishCh -// points to, which is set by this helper function. +// a single call to the EventsBytes method. It returns a mock client whose event +// bytes method always constructs a new observable. query is the query string +// for which event bytes subscription is expected to be for. +// The caller can simulate blockchain events by sending on publishCh, the value +// of which is set to the publish channel of the events bytes observable publish +// channel. func NewOneTimeEventsQuery( ctx context.Context, t *testing.T, @@ -53,11 +54,12 @@ func NewOneTimeEventsQuery( return eventsQueryClient } -// NewOneTimeTxEventsQueryClient creates a mock of the Events that expects +// NewOneTimeTxEventsQueryClient creates a mock of the Events that expects to to // a single call to the EventsBytes method where the query is for transaction // events for sender address matching that of the given key. -// The caller can simulate blockchain events by sending on the value publishCh -// points to, which is set by this helper function. +// The caller can simulate blockchain events by sending on publishCh, the value +// of which is set to the publish channel of the events bytes observable publish +// channel. func NewOneTimeTxEventsQueryClient( ctx context.Context, t *testing.T, diff --git a/internal/testclient/testkeyring/keyring.go b/internal/testclient/testkeyring/keyring.go new file mode 100644 index 000000000..40fbc64c8 --- /dev/null +++ b/internal/testclient/testkeyring/keyring.go @@ -0,0 +1,17 @@ +package testkeyring + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + + "github.com/pokt-network/poktroll/internal/testclient" +) + +// NewTestKeyringWithKey creates a new in-memory keyring with a test key +// with testSigningKeyName as its name. +func NewTestKeyringWithKey(t *testing.T, keyName string) (keyring.Keyring, *keyring.Record) { + keyring := keyring.NewInMemory(testclient.EncodingConfig.Marshaler) + key, _ := testclient.NewKey(t, keyName, keyring) + return keyring, key +} diff --git a/pkg/client/keyring/errors.go b/pkg/client/keyring/errors.go new file mode 100644 index 000000000..7be8a677a --- /dev/null +++ b/pkg/client/keyring/errors.go @@ -0,0 +1,19 @@ +package keyring + +import "cosmossdk.io/errors" + +var ( + // ErrEmptySigningKeyName represents an error which indicates that the + // provided signing key name is empty or unspecified. + ErrEmptySigningKeyName = errors.Register(codespace, 1, "empty signing key name") + + // ErrNoSuchSigningKey represents an error signifying that the requested + // signing key does not exist or could not be located. + ErrNoSuchSigningKey = errors.Register(codespace, 2, "signing key does not exist") + + // ErrSigningKeyAddr is raised when there's a failure in retrieving the + // associated address for the provided signing key. + ErrSigningKeyAddr = errors.Register(codespace, 3, "failed to get address for signing key") + + codespace = "keyring" +) diff --git a/pkg/client/keyring/keyring.go b/pkg/client/keyring/keyring.go new file mode 100644 index 000000000..a77d35b6e --- /dev/null +++ b/pkg/client/keyring/keyring.go @@ -0,0 +1,29 @@ +package keyring + +import ( + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmostypes "github.com/cosmos/cosmos-sdk/types" +) + +// KeyNameToAddr attempts to retrieve the key with the given name from the +// given keyring and compute its address. +func KeyNameToAddr( + keyName string, + keyring cosmoskeyring.Keyring, +) (cosmostypes.AccAddress, error) { + if keyName == "" { + return nil, ErrEmptySigningKeyName + } + + keyRecord, err := keyring.Key(keyName) + if err != nil { + return nil, ErrNoSuchSigningKey.Wrapf("name %q: %s", keyName, err) + } + + signingAddr, err := keyRecord.GetAddress() + if err != nil { + return nil, ErrSigningKeyAddr.Wrapf("name %q: %s", keyName, err) + } + + return signingAddr, nil +} diff --git a/pkg/client/tx/client.go b/pkg/client/tx/client.go index 805443eb4..1c083559b 100644 --- a/pkg/client/tx/client.go +++ b/pkg/client/tx/client.go @@ -16,6 +16,7 @@ import ( "go.uber.org/multierr" "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/keyring" "github.com/pokt-network/poktroll/pkg/either" "github.com/pokt-network/poktroll/pkg/observable" "github.com/pokt-network/poktroll/pkg/observable/channel" @@ -245,18 +246,14 @@ func (tClient *txClient) SignAndBroadcast( // - ErrSigningKeyAddr if there's an issue retrieving the address for the signing key. // - nil if validation is successful and defaults are set appropriately. func (tClient *txClient) validateConfigAndSetDefaults() error { - if tClient.signingKeyName == "" { - return ErrEmptySigningKeyName - } - - keyRecord, err := tClient.txCtx.GetKeyring().Key(tClient.signingKeyName) - if err != nil { - return ErrNoSuchSigningKey.Wrapf("name %q: %s", tClient.signingKeyName, err) - } - signingAddr, err := keyRecord.GetAddress() + signingAddr, err := keyring.KeyNameToAddr( + tClient.signingKeyName, + tClient.txCtx.GetKeyring(), + ) if err != nil { - return ErrSigningKeyAddr.Wrapf("name %q: %s", tClient.signingKeyName, err) + return err } + tClient.signingAddr = signingAddr if tClient.commitTimeoutHeightOffset <= 0 { diff --git a/pkg/client/tx/client_integration_test.go b/pkg/client/tx/client_integration_test.go index c2d5db6e5..737c8a628 100644 --- a/pkg/client/tx/client_integration_test.go +++ b/pkg/client/tx/client_integration_test.go @@ -10,6 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + "github.com/pokt-network/poktroll/internal/testclient/testkeyring" "github.com/pokt-network/poktroll/pkg/client/tx" "github.com/pokt-network/poktroll/internal/testclient/testblock" @@ -24,7 +25,7 @@ func TestTxClient_SignAndBroadcast_Integration(t *testing.T) { var ctx = context.Background() - keyring, signingKey := newTestKeyringWithKey(t) + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) eventsQueryClient := testeventsquery.NewLocalnetClient(t) diff --git a/pkg/client/tx/client_test.go b/pkg/client/tx/client_test.go index 6cb5d33da..f6f1d08a9 100644 --- a/pkg/client/tx/client_test.go +++ b/pkg/client/tx/client_test.go @@ -17,8 +17,10 @@ import ( "github.com/pokt-network/poktroll/internal/testclient" "github.com/pokt-network/poktroll/internal/testclient/testblock" "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/internal/testclient/testkeyring" "github.com/pokt-network/poktroll/internal/testclient/testtx" "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/keyring" "github.com/pokt-network/poktroll/pkg/client/tx" "github.com/pokt-network/poktroll/pkg/either" apptypes "github.com/pokt-network/poktroll/x/application/types" @@ -49,7 +51,7 @@ func TestTxClient_SignAndBroadcast_Succeeds(t *testing.T) { ctx = context.Background() ) - keyring, signingKey := newTestKeyringWithKey(t) + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( ctx, t, signingKey, &eventsBzPublishCh, @@ -118,7 +120,7 @@ func TestTxClient_SignAndBroadcast_Succeeds(t *testing.T) { func TestTxClient_NewTxClient_Error(t *testing.T) { // Construct an empty in-memory keyring. - keyring := cosmoskeyring.NewInMemory(testclient.EncodingConfig.Marshaler) + memKeyring := cosmoskeyring.NewInMemory(testclient.EncodingConfig.Marshaler) tests := []struct { name string @@ -128,12 +130,12 @@ func TestTxClient_NewTxClient_Error(t *testing.T) { { name: "empty signing key name", signingKeyName: "", - expectedErr: tx.ErrEmptySigningKeyName, + expectedErr: keyring.ErrEmptySigningKeyName, }, { name: "signing key does not exist", signingKeyName: "nonexistent", - expectedErr: tx.ErrNoSuchSigningKey, + expectedErr: keyring.ErrNoSuchSigningKey, }, // TODO_TECHDEBT: add coverage for this error case // { @@ -156,7 +158,7 @@ func TestTxClient_NewTxClient_Error(t *testing.T) { eventsQueryClient := mockclient.NewMockEventsQueryClient(ctrl) // Construct a new mock transactions context. - txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, keyring) + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, memKeyring) // Construct a new mock block client. Since we expect the NewTxClient // call to fail, we don't need to set any expectations on this mock. @@ -195,7 +197,7 @@ func TestTxClient_SignAndBroadcast_SyncError(t *testing.T) { ctx = context.Background() ) - keyring, signingKey := newTestKeyringWithKey(t) + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) // Construct a new mock events query client. Since we expect the // NewTxClient call to fail, we don't need to set any expectations @@ -260,7 +262,7 @@ func TestTxClient_SignAndBroadcast_CheckTxError(t *testing.T) { ctx = context.Background() ) - keyring, signingKey := newTestKeyringWithKey(t) + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( ctx, t, signingKey, &eventsBzPublishCh, @@ -323,7 +325,7 @@ func TestTxClient_SignAndBroadcast_Timeout(t *testing.T) { ctx = context.Background() ) - keyring, signingKey := newTestKeyringWithKey(t) + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( ctx, t, signingKey, &eventsBzPublishCh, @@ -403,11 +405,3 @@ func TestTxClient_SignAndBroadcast_Timeout(t *testing.T) { func TestTxClient_SignAndBroadcast_MultipleMsgs(t *testing.T) { t.SkipNow() } - -// newTestKeyringWithKey creates a new in-memory keyring with a test key -// with testSigningKeyName as its name. -func newTestKeyringWithKey(t *testing.T) (cosmoskeyring.Keyring, *cosmoskeyring.Record) { - keyring := cosmoskeyring.NewInMemory(testclient.EncodingConfig.Marshaler) - key, _ := testclient.NewKey(t, testSigningKeyName, keyring) - return keyring, key -} diff --git a/pkg/client/tx/errors.go b/pkg/client/tx/errors.go index 474f2ac19..1e43f1d05 100644 --- a/pkg/client/tx/errors.go +++ b/pkg/client/tx/errors.go @@ -3,18 +3,6 @@ package tx import errorsmod "cosmossdk.io/errors" var ( - // ErrEmptySigningKeyName represents an error which indicates that the - // provided signing key name is empty or unspecified. - ErrEmptySigningKeyName = errorsmod.Register(codespace, 1, "empty signing key name") - - // ErrNoSuchSigningKey represents an error signifying that the requested - // signing key does not exist or could not be located. - ErrNoSuchSigningKey = errorsmod.Register(codespace, 2, "signing key does not exist") - - // ErrSigningKeyAddr is raised when there's a failure in retrieving the - // associated address for the provided signing key. - ErrSigningKeyAddr = errorsmod.Register(codespace, 3, "failed to get address for signing key") - // ErrInvalidMsg signifies that there was an issue in validating the // transaction message. This could be due to format, content, or other // constraints imposed on the message. From 5b3fd9513d103a7d87252f3a0563624feac14ec4 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Tue, 7 Nov 2023 09:55:19 +0100 Subject: [PATCH 28/28] [Miner] feat: add supplier client (#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add `TxClient` interface * chore: add option support to `ReplayObservable` * feat: add `txClient` implementation * test: `txClient` * test: tx client integration * chore: s/tx/transaction/g * chore: update pkg README.md template * wip: client pkg README * docs: fix client pkg godoc comment * refactor: consolidate keyring errors & helpers * refactor: keyring test helpers * fix: flakey test * chore: dial back godoc comments 😅 * chore: add `SupplierClient` interface * feat: add supplier client implementation * test: supplier test helpers * test: supplier client tests * test: supplier client integration test * chore: update go.mod * trigger CI * chore: revise (and move to godoc.go) `testblock` & `testeventsquery` pkg godoc comment * chore: update go.mod * chore: refactor & condense godoc comments * chore: fix import paths post-update * chore: add godoc comment --- go.mod | 2 + go.sum | 4 + internal/testclient/keyring.go | 2 + internal/testclient/testsupplier/client.go | 37 ++++ internal/testclient/testtx/client.go | 93 +++++++++ internal/testclient/testtx/context.go | 22 ++ pkg/client/interface.go | 32 ++- pkg/client/supplier/client.go | 127 ++++++++++++ .../supplier/client_integration_test.go | 36 ++++ pkg/client/supplier/client_test.go | 190 ++++++++++++++++++ pkg/client/supplier/options.go | 14 ++ 11 files changed, 557 insertions(+), 2 deletions(-) create mode 100644 internal/testclient/testsupplier/client.go create mode 100644 internal/testclient/testtx/client.go create mode 100644 pkg/client/supplier/client.go create mode 100644 pkg/client/supplier/client_integration_test.go create mode 100644 pkg/client/supplier/client_test.go create mode 100644 pkg/client/supplier/options.go diff --git a/go.mod b/go.mod index 2d699d1cf..a8ef04265 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 + github.com/pokt-network/smt v0.7.1 github.com/regen-network/gocuke v0.6.2 github.com/spf13/cast v1.5.1 github.com/spf13/cobra v1.7.0 @@ -86,6 +87,7 @@ require ( github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/badger/v3 v3.2103.5 // indirect + github.com/dgraph-io/badger/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/docker/go-units v0.5.0 // indirect diff --git a/go.sum b/go.sum index ef5829bd5..64f7566f0 100644 --- a/go.sum +++ b/go.sum @@ -520,6 +520,8 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= @@ -1582,6 +1584,8 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pokt-network/smt v0.7.1 h1:WHcZeMLe+9U1/kCAhdbssdyTYzYxxb74sf8MCvG34M8= +github.com/pokt-network/smt v0.7.1/go.mod h1:K7BLEOWoZGZmY5USQuYvTkZ3qXjE6m39BMufBvVo3U8= github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= github.com/polyfloyd/go-errorlint v1.0.0/go.mod h1:KZy4xxPJyy88/gldCe5OdW6OQRtNO3EZE7hXzmnebgA= diff --git a/internal/testclient/keyring.go b/internal/testclient/keyring.go index a3c8dc14c..59d54f908 100644 --- a/internal/testclient/keyring.go +++ b/internal/testclient/keyring.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/require" ) +// NewKey creates a new Secp256k1 key and mnemonic for the given name within +// the provided keyring. func NewKey( t *testing.T, name string, diff --git a/internal/testclient/testsupplier/client.go b/internal/testclient/testsupplier/client.go new file mode 100644 index 000000000..be5df6507 --- /dev/null +++ b/internal/testclient/testsupplier/client.go @@ -0,0 +1,37 @@ +package testsupplier + +import ( + "testing" + + "cosmossdk.io/depinject" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/testclient/testtx" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/supplier" + "github.com/pokt-network/poktroll/pkg/client/tx" +) + +// NewLocalnetClient creates and returns a new supplier client that connects to +// the localnet sequencer. +func NewLocalnetClient( + t *testing.T, + signingKeyName string, +) client.SupplierClient { + t.Helper() + + txClientOpt := tx.WithSigningKeyName(signingKeyName) + supplierClientOpt := supplier.WithSigningKeyName(signingKeyName) + + txCtx := testtx.NewLocalnetContext(t) + txClient := testtx.NewLocalnetClient(t, txClientOpt) + + deps := depinject.Supply( + txCtx, + txClient, + ) + + supplierClient, err := supplier.NewSupplierClient(deps, supplierClientOpt) + require.NoError(t, err) + return supplierClient +} diff --git a/internal/testclient/testtx/client.go b/internal/testclient/testtx/client.go new file mode 100644 index 000000000..3e843dd91 --- /dev/null +++ b/internal/testclient/testtx/client.go @@ -0,0 +1,93 @@ +package testtx + +import ( + "context" + "testing" + "time" + + "cosmossdk.io/depinject" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient/testblock" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/tx" + "github.com/pokt-network/poktroll/pkg/either" +) + +type signAndBroadcastFn func(context.Context, cosmostypes.Msg) either.AsyncError + +// TODO_CONSIDERATION: functions like these (NewLocalnetXXX) could probably accept +// and return depinject.Config arguments to support shared dependencies. + +// NewLocalnetClient creates and returns a new client for use with the localnet +// sequencer. +func NewLocalnetClient(t *testing.T, opts ...client.TxClientOption) client.TxClient { + t.Helper() + + ctx := context.Background() + txCtx := NewLocalnetContext(t) + eventsQueryClient := testeventsquery.NewLocalnetClient(t) + blockClient := testblock.NewLocalnetClient(ctx, t) + + deps := depinject.Supply( + txCtx, + eventsQueryClient, + blockClient, + ) + + txClient, err := tx.NewTxClient(ctx, deps, opts...) + require.NoError(t, err) + + return txClient +} + +// NewOneTimeDelayedSignAndBroadcastTxClient constructs a mock TxClient with the +// expectation to perform a SignAndBroadcast operation with a specified delay. +func NewOneTimeDelayedSignAndBroadcastTxClient( + t *testing.T, + delay time.Duration, +) *mockclient.MockTxClient { + t.Helper() + + signAndBroadcast := newSignAndBroadcastSucceedsDelayed(delay) + return NewOneTimeSignAndBroadcastTxClient(t, signAndBroadcast) +} + +// NewOneTimeSignAndBroadcastTxClient constructs a mock TxClient with the +// expectation to perform a SignAndBroadcast operation, which will call and receive +// the return from the given signAndBroadcast function. +func NewOneTimeSignAndBroadcastTxClient( + t *testing.T, + signAndBroadcast signAndBroadcastFn, +) *mockclient.MockTxClient { + t.Helper() + + var ctrl = gomock.NewController(t) + + txClient := mockclient.NewMockTxClient(ctrl) + txClient.EXPECT().SignAndBroadcast( + gomock.AssignableToTypeOf(context.Background()), + gomock.Any(), + ).DoAndReturn(signAndBroadcast).Times(1) + + return txClient +} + +// newSignAndBroadcastSucceedsDelayed returns a signAndBroadcastFn that succeeds +// after the given delay. +func newSignAndBroadcastSucceedsDelayed(delay time.Duration) signAndBroadcastFn { + return func(ctx context.Context, msg cosmostypes.Msg) either.AsyncError { + errCh := make(chan error) + + go func() { + time.Sleep(delay) + close(errCh) + }() + + return either.AsyncErr(errCh) + } +} diff --git a/internal/testclient/testtx/context.go b/internal/testclient/testtx/context.go index fa25494e7..134d7b2c4 100644 --- a/internal/testclient/testtx/context.go +++ b/internal/testclient/testtx/context.go @@ -23,6 +23,28 @@ import ( "github.com/pokt-network/poktroll/pkg/client/tx" ) +// NewLocalnetContext creates and returns a new transaction context configured +// for use with the localnet sequencer. +func NewLocalnetContext(t *testing.T) client.TxContext { + t.Helper() + + flagSet := testclient.NewLocalnetFlagSet(t) + clientCtx := testclient.NewLocalnetClientCtx(t, flagSet) + txFactory, err := cosmostx.NewFactoryCLI(*clientCtx, flagSet) + require.NoError(t, err) + require.NotEmpty(t, txFactory) + + deps := depinject.Supply( + *clientCtx, + txFactory, + ) + + txCtx, err := tx.NewTxContext(deps) + require.NoError(t, err) + + return txCtx +} + // TODO_IMPROVE: these mock constructor helpers could include parameters for the // "times" (e.g. exact, min, max) values which are passed to their respective // gomock.EXPECT() method calls (i.e. Times(), MinTimes(), MaxTimes()). diff --git a/pkg/client/interface.go b/pkg/client/interface.go index 32dab250c..c088df35c 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -1,6 +1,6 @@ //go:generate mockgen -destination=../../internal/mocks/mockclient/events_query_client_mock.go -package=mockclient . Dialer,Connection,EventsQueryClient //go:generate mockgen -destination=../../internal/mocks/mockclient/block_client_mock.go -package=mockclient . Block,BlockClient -//go:generate mockgen -destination=../../internal/mocks/mockclient/tx_client_mock.go -package=mockclient . TxContext +//go:generate mockgen -destination=../../internal/mocks/mockclient/tx_client_mock.go -package=mockclient . TxContext,TxClient //go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_tx_builder_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/client TxBuilder //go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_keyring_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/crypto/keyring Keyring //go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_client_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/client AccountRetriever @@ -14,11 +14,36 @@ import ( cosmosclient "github.com/cosmos/cosmos-sdk/client" cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/pokt-network/smt" "github.com/pokt-network/poktroll/pkg/either" "github.com/pokt-network/poktroll/pkg/observable" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" ) +// SupplierClient is an interface for sufficient for a supplier operator to be +// able to construct blockchain transactions from pocket protocol-specific messages +// related to its role. +type SupplierClient interface { + // CreateClaim sends a claim message which creates an on-chain commitment by + // calling supplier to the given smt.SparseMerkleSumTree root hash of the given + // session's mined relays. + CreateClaim( + ctx context.Context, + sessionHeader sessiontypes.SessionHeader, + rootHash []byte, + ) error + // SubmitProof sends a proof message which contains the + // smt.SparseMerkleClosestProof, corresponding to some previously created claim + // for the same session. The proof is validated on-chain as part of the pocket + // protocol. + SubmitProof( + ctx context.Context, + sessionHeader sessiontypes.SessionHeader, + proof *smt.SparseMerkleClosestProof, + ) error +} + // TxClient provides a synchronous interface initiating and waiting for transactions // derived from cosmos-sdk messages, in a cosmos-sdk based blockchain network. type TxClient interface { @@ -79,7 +104,7 @@ type BlocksObservable observable.ReplayObservable[Block] // BlockClient is an interface which provides notifications about newly committed // blocks as well as direct access to the latest block via some blockchain API. type BlockClient interface { - // Blocks returns an observable which emits newly committed blocks. + // CommittedBlocksSequence returns an observable which emits newly committed blocks. CommittedBlocksSequence(context.Context) BlocksObservable // LatestBlock returns the latest block that has been committed. LatestBlock(context.Context) Block @@ -148,3 +173,6 @@ type EventsQueryClientOption func(EventsQueryClient) // TxClientOption defines a function type that modifies the TxClient. type TxClientOption func(TxClient) + +// SupplierClientOption defines a function type that modifies the SupplierClient. +type SupplierClientOption func(SupplierClient) diff --git a/pkg/client/supplier/client.go b/pkg/client/supplier/client.go new file mode 100644 index 000000000..a0172c3c5 --- /dev/null +++ b/pkg/client/supplier/client.go @@ -0,0 +1,127 @@ +package supplier + +import ( + "context" + + "cosmossdk.io/depinject" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/pokt-network/smt" + + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/keyring" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" +) + +var _ client.SupplierClient = (*supplierClient)(nil) + +// supplierClient +type supplierClient struct { + signingKeyName string + signingKeyAddr cosmostypes.AccAddress + + txClient client.TxClient + txCtx client.TxContext +} + +// NewSupplierClient constructs a new SupplierClient with the given dependencies +// and options. If a signingKeyName is not configured, an error will be returned. +// +// Required dependencies: +// - client.TxClient +// - client.TxContext +// +// Available options: +// - WithSigningKeyName +func NewSupplierClient( + deps depinject.Config, + opts ...client.SupplierClientOption, +) (*supplierClient, error) { + sClient := &supplierClient{} + + if err := depinject.Inject( + deps, + &sClient.txClient, + &sClient.txCtx, + ); err != nil { + return nil, err + } + + for _, opt := range opts { + opt(sClient) + } + + if err := sClient.validateConfigAndSetDefaults(); err != nil { + return nil, err + } + + return sClient, nil +} + +// SubmitProof constructs a submit proof message then signs and broadcasts it +// to the network via #txClient. It blocks until the transaction is included in +// a block or times out. +func (sClient *supplierClient) SubmitProof( + ctx context.Context, + sessionHeader sessiontypes.SessionHeader, + proof *smt.SparseMerkleClosestProof, +) error { + proofBz, err := proof.Marshal() + if err != nil { + return err + } + + msg := &suppliertypes.MsgSubmitProof{ + SupplierAddress: sClient.signingKeyAddr.String(), + SessionHeader: &sessionHeader, + Proof: proofBz, + } + eitherErr := sClient.txClient.SignAndBroadcast(ctx, msg) + err, errCh := eitherErr.SyncOrAsyncError() + if err != nil { + return err + } + + return <-errCh +} + +// CreateClaim constructs a creates claim message then signs and broadcasts it +// to the network via #txClient. It blocks until the transaction is included in +// a block or times out. +func (sClient *supplierClient) CreateClaim( + ctx context.Context, + sessionHeader sessiontypes.SessionHeader, + rootHash []byte, +) error { + msg := &suppliertypes.MsgCreateClaim{ + SupplierAddress: sClient.signingKeyAddr.String(), + SessionHeader: &sessionHeader, + RootHash: rootHash, + } + eitherErr := sClient.txClient.SignAndBroadcast(ctx, msg) + err, errCh := eitherErr.SyncOrAsyncError() + if err != nil { + return err + } + + err = <-errCh + return err +} + +// validateConfigAndSetDefaults attempts to get the address from the keyring +// corresponding to the key whose name matches the configured signingKeyName. +// If signingKeyName is empty or the keyring does not contain the corresponding +// key, an error is returned. +func (sClient *supplierClient) validateConfigAndSetDefaults() error { + signingAddr, err := keyring.KeyNameToAddr( + sClient.signingKeyName, + sClient.txCtx.GetKeyring(), + ) + if err != nil { + return err + } + + sClient.signingKeyAddr = signingAddr + + return nil +} diff --git a/pkg/client/supplier/client_integration_test.go b/pkg/client/supplier/client_integration_test.go new file mode 100644 index 000000000..8af759186 --- /dev/null +++ b/pkg/client/supplier/client_integration_test.go @@ -0,0 +1,36 @@ +//go:build integration + +package supplier_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/testclient/testsupplier" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" +) + +func TestNewSupplierClient_Localnet(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test depends on some setup which is currently not implemented in this test: staked application and servicer with matching services") + + var ( + signingKeyName = "app1" + ctx = context.Background() + ) + + supplierClient := testsupplier.NewLocalnetClient(t, signingKeyName) + require.NotNil(t, supplierClient) + + var rootHash []byte + sessionHeader := sessiontypes.SessionHeader{ + ApplicationAddress: "", + SessionStartBlockHeight: 0, + SessionId: "", + } + err := supplierClient.CreateClaim(ctx, sessionHeader, rootHash) + require.NoError(t, err) + + require.True(t, false) +} diff --git a/pkg/client/supplier/client_test.go b/pkg/client/supplier/client_test.go new file mode 100644 index 000000000..9cb6e85c6 --- /dev/null +++ b/pkg/client/supplier/client_test.go @@ -0,0 +1,190 @@ +package supplier_test + +import ( + "context" + "crypto/sha256" + "testing" + "time" + + "cosmossdk.io/depinject" + "github.com/golang/mock/gomock" + "github.com/pokt-network/smt" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient/testkeyring" + "github.com/pokt-network/poktroll/internal/testclient/testtx" + "github.com/pokt-network/poktroll/pkg/client/keyring" + "github.com/pokt-network/poktroll/pkg/client/supplier" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" +) + +var testSigningKeyName = "test_signer" + +func TestNewSupplierClient(t *testing.T) { + ctrl := gomock.NewController(t) + + memKeyring, _ := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, memKeyring) + txClientMock := mockclient.NewMockTxClient(ctrl) + + deps := depinject.Supply( + txCtxMock, + txClientMock, + ) + + tests := []struct { + name string + signingKeyName string + expectedErr error + }{ + { + name: "valid signing key name", + signingKeyName: testSigningKeyName, + expectedErr: nil, + }, + { + name: "empty signing key name", + signingKeyName: "", + expectedErr: keyring.ErrEmptySigningKeyName, + }, + { + name: "no such signing key name", + signingKeyName: "nonexistent", + expectedErr: keyring.ErrNoSuchSigningKey, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + signingKeyOpt := supplier.WithSigningKeyName(tt.signingKeyName) + + supplierClient, err := supplier.NewSupplierClient(deps, signingKeyOpt) + if tt.expectedErr != nil { + require.ErrorIs(t, err, tt.expectedErr) + require.Nil(t, supplierClient) + } else { + require.NoError(t, err) + require.NotNil(t, supplierClient) + } + }) + } +} + +func TestSupplierClient_CreateClaim(t *testing.T) { + var ( + signAndBroadcastDelay = 50 * time.Millisecond + doneCh = make(chan struct{}, 1) + ctx = context.Background() + ) + + keyring, testAppKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + + testAppAddr, err := testAppKey.GetAddress() + require.NoError(t, err) + + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, keyring) + txClientMock := testtx.NewOneTimeDelayedSignAndBroadcastTxClient(t, signAndBroadcastDelay) + + signingKeyOpt := supplier.WithSigningKeyName(testAppKey.Name) + deps := depinject.Supply( + txCtxMock, + txClientMock, + ) + + supplierClient, err := supplier.NewSupplierClient(deps, signingKeyOpt) + require.NoError(t, err) + require.NotNil(t, supplierClient) + + var rootHash []byte + sessionHeader := sessiontypes.SessionHeader{ + ApplicationAddress: testAppAddr.String(), + SessionStartBlockHeight: 0, + SessionId: "", + } + + go func() { + err = supplierClient.CreateClaim(ctx, sessionHeader, rootHash) + require.NoError(t, err) + close(doneCh) + }() + + // TODO_IMPROVE: this could be rewritten to record the times at which + // things happen and then compare them to the expected times. + + select { + case <-doneCh: + t.Fatal("expected CreateClaim to block for signAndBroadcastDelay") + case <-time.After(signAndBroadcastDelay * 95 / 100): + t.Log("OK: CreateClaim blocked for at least 95% of signAndBroadcastDelay") + } + + select { + case <-time.After(signAndBroadcastDelay): + t.Fatal("expected CreateClaim to unblock after signAndBroadcastDelay") + case <-doneCh: + t.Log("OK: CreateClaim unblocked after signAndBroadcastDelay") + } +} + +func TestSupplierClient_SubmitProof(t *testing.T) { + var ( + signAndBroadcastDelay = 50 * time.Millisecond + doneCh = make(chan struct{}, 1) + ctx = context.Background() + ) + + keyring, testAppKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + + testAppAddr, err := testAppKey.GetAddress() + require.NoError(t, err) + + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, keyring) + txClientMock := testtx.NewOneTimeDelayedSignAndBroadcastTxClient(t, signAndBroadcastDelay) + + signingKeyOpt := supplier.WithSigningKeyName(testAppKey.Name) + deps := depinject.Supply( + txCtxMock, + txClientMock, + ) + + supplierClient, err := supplier.NewSupplierClient(deps, signingKeyOpt) + require.NoError(t, err) + require.NotNil(t, supplierClient) + + sessionHeader := sessiontypes.SessionHeader{ + ApplicationAddress: testAppAddr.String(), + SessionStartBlockHeight: 0, + SessionId: "", + } + + kvStore, err := smt.NewKVStore("") + require.NoError(t, err) + + tree := smt.NewSparseMerkleSumTree(kvStore, sha256.New()) + proof, err := tree.ProveClosest([]byte{1}) + require.NoError(t, err) + + go func() { + err = supplierClient.SubmitProof(ctx, sessionHeader, proof) + require.NoError(t, err) + close(doneCh) + }() + + // TODO_IMPROVE: this could be rewritten to record the times at which + // things happen and then compare them to the expected times. + + select { + case <-doneCh: + t.Fatal("expected SubmitProof to block for signAndBroadcastDelay") + case <-time.After(signAndBroadcastDelay * 95 / 100): + t.Log("OK: SubmitProof blocked for at least 95% of signAndBroadcastDelay") + } + + select { + case <-time.After(signAndBroadcastDelay): + t.Fatal("expected SubmitProof to unblock after signAndBroadcastDelay") + case <-doneCh: + t.Log("OK: SubmitProof unblocked after signAndBroadcastDelay") + } +} diff --git a/pkg/client/supplier/options.go b/pkg/client/supplier/options.go new file mode 100644 index 000000000..f4460c8c9 --- /dev/null +++ b/pkg/client/supplier/options.go @@ -0,0 +1,14 @@ +package supplier + +import ( + "github.com/pokt-network/poktroll/pkg/client" +) + +// WithSigningKeyName sets the name of the key which the supplier client should +// retrieve from the keyring to use for authoring and signing CreateClaim and +// SubmitProof messages. +func WithSigningKeyName(keyName string) client.SupplierClientOption { + return func(sClient client.SupplierClient) { + sClient.(*supplierClient).signingKeyName = keyName + } +}