From d7d87eb357ffa60a3bf6848d2b89969743af9d10 Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Thu, 26 Oct 2023 15:34:31 -0700 Subject: [PATCH] [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) {}