diff --git a/.gitignore b/.gitignore index 8fe75f2aa..88b925c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,12 @@ localnet_config.yaml # Relase artifacts produced by `ignite chain build --release` release +# Only keep one go module in our codebase +go.work.sum + +# Avoid accidentally committing gomock artifacts +**/gomock_reflect_* + # SMT KVStore files # TODO_TECHDEBT(#126, @red-0ne): Rename `smt` to `smt_stores` and make it configurable so it can be stored anywhere on this smt diff --git a/Makefile b/Makefile index 19120c578..dc7666200 100644 --- a/Makefile +++ b/Makefile @@ -451,6 +451,45 @@ acc_balance_query_app1: ## Query the balance of app1 acc_balance_total_supply: ## Query the total supply of the network poktrolld --home=$(POKTROLLD_HOME) q bank total --node $(POCKET_NODE) +############## +### Claims ### +############## + +# These encoded values were generated using the `encodeSessionHeader` helpers in `query_claim_test.go` as dummy values. +ENCODED_SESSION_HEADER = "eyJhcHBsaWNhdGlvbl9hZGRyZXNzIjoicG9rdDFleXJuNDUwa3JoZnpycmVyemd0djd2c3J4bDA5NDN0dXN4azRhayIsInNlcnZpY2UiOnsiaWQiOiJhbnZpbCIsIm5hbWUiOiIifSwic2Vzc2lvbl9zdGFydF9ibG9ja19oZWlnaHQiOiI1Iiwic2Vzc2lvbl9pZCI6InNlc3Npb25faWQxIiwic2Vzc2lvbl9lbmRfYmxvY2tfaGVpZ2h0IjoiOSJ9" +ENCODED_ROOT_HASH = "cm9vdF9oYXNo" +.PHONY: claim_create_dummy +claim_create_dummy: ## Create a dummy claim by supplier1 + poktrolld --home=$(POKTROLLD_HOME) tx supplier create-claim \ + $(ENCODED_SESSION_HEADER) \ + $(ENCODED_ROOT_HASH) \ + --from supplier1 --node $(POCKET_NODE) + +.PHONY: claims_list +claim_list: ## List all the claims + poktrolld --home=$(POKTROLLD_HOME) q supplier list-claims --node $(POCKET_NODE) + +.PHONY: claims_list_address +claim_list_address: ## List all the claims for a specific address (specified via ADDR variable) + poktrolld --home=$(POKTROLLD_HOME) q supplier list-claims --supplier-address $(ADDR) --node $(POCKET_NODE) + +.PHONY: claims_list_address_supplier1 +claim_list_address_supplier1: ## List all the claims for supplier1 + SUPPLIER1=$$(make poktrolld_addr ACC_NAME=supplier1) && \ + ADDR=$$SUPPLIER1 make claim_list_address + +.PHONY: claim_list_height +claim_list_height: ## List all the claims ending at a specific height (specified via HEIGHT variable) + poktrolld --home=$(POKTROLLD_HOME) q supplier list-claims --session-end-height $(HEIGHT) --node $(POCKET_NODE) + +.PHONY: claim_list_height_5 +claim_list_height_5: ## List all the claims at height 5 + HEIGHT=5 make claim_list_height + +.PHONY: claim_list_session +claim_list_session: ## List all the claims ending at a specific session (specified via SESSION variable) + poktrolld --home=$(POKTROLLD_HOME) q supplier list-claims --session-id $(SESSION) --node $(POCKET_NODE) + ###################### ### Ignite Helpers ### ###################### @@ -487,4 +526,4 @@ openapi_gen: ## Generate the OpenAPI spec for the Ignite API .PHONY: poktrolld_addr poktrolld_addr: ## Retrieve the address for an account by ACC_NAME - @echo $(shell poktrolld keys show -a $(ACC_NAME)) + @echo $(shell poktrolld --home=$(POKTROLLD_HOME) keys show -a $(ACC_NAME)) diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index f55a851f2..467f7783e 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -47427,9 +47427,10 @@ paths: additionalProperties: {} tags: - Query - /pocket/supplier/claim: + /pocket/supplier/claim/{session_id}/{supplier_address}: get: - operationId: PocketSupplierAllClaims + summary: Queries a list of Claim items. + operationId: PocketSupplierClaim responses: '200': description: A successful response. @@ -47437,23 +47438,85 @@ paths: type: object properties: claim: + type: object + properties: + supplier_address: + type: string + title: the address of the supplier that submitted this claim + session_id: + type: string + title: session id from the SessionHeader + session_end_block_height: + type: string + format: uint64 + title: session end block height from the SessionHeader + root_hash: + type: string + format: byte + title: smt.SMST#Root() + title: >- + Claim is the serialized object stored on-chain for claims + pending to be proven + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: type: array items: type: object properties: - index: + '@type': type: string + additionalProperties: {} + parameters: + - name: session_id + in: path + required: true + type: string + - name: supplier_address + in: path + required: true + type: string + tags: + - Query + /pocket/supplier/claims: + get: + operationId: PocketSupplierAllClaims + responses: + '200': + description: A successful response. + schema: + type: object + properties: + claim: + type: array + items: + type: object + properties: supplier_address: type: string + title: the address of the supplier that submitted this claim session_id: type: string + title: session id from the SessionHeader + session_end_block_height: + type: string + format: uint64 + title: session end block height from the SessionHeader root_hash: type: string - description: >- - TODO_UPNEXT(@Olshansk): The structure below is the default - (untouched) scaffolded type. Update - - and productionize it for our use case. + format: byte + title: smt.SMST#Root() + title: >- + Claim is the serialized object stored on-chain for claims + pending to be proven pagination: type: object properties: @@ -47557,34 +47620,37 @@ paths: in: query required: false type: boolean + - name: supplier_address + in: query + required: false + type: string + - name: session_id + in: query + required: false + type: string + - name: session_end_height + in: query + required: false + type: string + format: uint64 tags: - Query - /pocket/supplier/claim/{index}: + /pocket/supplier/params: get: - summary: Queries a list of Claim items. - operationId: PocketSupplierClaim + summary: Parameters queries the parameters of the module. + operationId: PocketSupplierParams responses: '200': description: A successful response. schema: type: object properties: - claim: + params: + description: params holds all the parameters of this module. type: object - properties: - index: - type: string - supplier_address: - type: string - session_id: - type: string - root_hash: - type: string - description: >- - TODO_UPNEXT(@Olshansk): The structure below is the default - (untouched) scaffolded type. Update - - and productionize it for our use case. + description: >- + QueryParamsResponse is response type for the Query/Params RPC + method. default: description: An unexpected error response. schema: @@ -47603,29 +47669,131 @@ paths: '@type': type: string additionalProperties: {} - parameters: - - name: index - in: path - required: true - type: string tags: - Query - /pocket/supplier/params: + /pocket/supplier/supplier/{address}: get: - summary: Parameters queries the parameters of the module. - operationId: PocketSupplierParams + summary: Queries a list of Supplier items. + operationId: PocketSupplierSupplier responses: '200': description: A successful response. schema: type: object properties: - params: - description: params holds all the parameters of this module. + supplier: type: object - description: >- - QueryParamsResponse is response type for the Query/Params RPC - method. + 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: + title: The Service for which the supplier is configured + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + 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. default: description: An unexpected error response. schema: @@ -47644,9 +47812,14 @@ paths: '@type': type: string additionalProperties: {} + parameters: + - name: address + in: path + required: true + type: string tags: - Query - /pocket/supplier/supplier: + /pocket/supplier/suppliers: get: operationId: PocketSupplierSupplierAll responses: @@ -47875,154 +48048,6 @@ paths: type: boolean tags: - Query - /pocket/supplier/supplier/{address}: - get: - summary: Queries a list of Supplier items. - operationId: PocketSupplierSupplier - responses: - '200': - description: A successful response. - schema: - type: object - properties: - supplier: - 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: - title: The Service for which the supplier is configured - type: object - properties: - id: - type: string - description: Unique identifier for the service - title: >- - 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. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: address - in: path - required: true - type: string - tags: - - Query definitions: cosmos.auth.v1beta1.AddressBytesToStringResponse: type: object @@ -78019,19 +78044,23 @@ definitions: pocket.supplier.Claim: type: object properties: - index: - type: string supplier_address: type: string + title: the address of the supplier that submitted this claim session_id: type: string + title: session id from the SessionHeader + session_end_block_height: + type: string + format: uint64 + title: session end block height from the SessionHeader root_hash: type: string - description: >- - TODO_UPNEXT(@Olshansk): The structure below is the default (untouched) - scaffolded type. Update - - and productionize it for our use case. + format: byte + title: smt.SMST#Root() + title: >- + Claim is the serialized object stored on-chain for claims pending to be + proven pocket.supplier.MsgCreateClaimResponse: type: object pocket.supplier.MsgStakeSupplierResponse: @@ -78051,19 +78080,23 @@ definitions: items: type: object properties: - index: - type: string supplier_address: type: string + title: the address of the supplier that submitted this claim session_id: type: string + title: session id from the SessionHeader + session_end_block_height: + type: string + format: uint64 + title: session end block height from the SessionHeader root_hash: type: string - description: >- - TODO_UPNEXT(@Olshansk): The structure below is the default - (untouched) scaffolded type. Update - - and productionize it for our use case. + format: byte + title: smt.SMST#Root() + title: >- + Claim is the serialized object stored on-chain for claims pending to + be proven pagination: type: object properties: @@ -78238,19 +78271,23 @@ definitions: claim: type: object properties: - index: - type: string supplier_address: type: string + title: the address of the supplier that submitted this claim session_id: type: string + title: session id from the SessionHeader + session_end_block_height: + type: string + format: uint64 + title: session end block height from the SessionHeader root_hash: type: string - description: >- - TODO_UPNEXT(@Olshansk): The structure below is the default (untouched) - scaffolded type. Update - - and productionize it for our use case. + format: byte + title: smt.SMST#Root() + title: >- + Claim is the serialized object stored on-chain for claims pending to + be proven pocket.supplier.QueryGetSupplierResponse: type: object properties: diff --git a/proto/pocket/supplier/claim.proto b/proto/pocket/supplier/claim.proto index c0c293083..5b8121855 100644 --- a/proto/pocket/supplier/claim.proto +++ b/proto/pocket/supplier/claim.proto @@ -3,12 +3,12 @@ package pocket.supplier; option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; -// TODO_UPNEXT(@Olshansk): The structure below is the default (untouched) scaffolded type. Update -// and productionize it for our use case. -message Claim { - string index = 1; - string supplier_address = 2; - string session_id = 3; - string root_hash = 4; -} +import "cosmos_proto/cosmos.proto"; +// Claim is the serialized object stored on-chain for claims pending to be proven +message Claim { + string supplier_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // the address of the supplier that submitted this claim + string session_id = 2; // session id from the SessionHeader + uint64 session_end_block_height = 3; // session end block height from the SessionHeader + bytes root_hash = 4; // smt.SMST#Root() +} \ No newline at end of file diff --git a/proto/pocket/supplier/genesis.proto b/proto/pocket/supplier/genesis.proto index b8f184ef9..5bba7baea 100644 --- a/proto/pocket/supplier/genesis.proto +++ b/proto/pocket/supplier/genesis.proto @@ -5,7 +5,6 @@ package pocket.supplier; import "gogoproto/gogo.proto"; import "pocket/supplier/params.proto"; import "pocket/shared/supplier.proto"; -import "pocket/supplier/claim.proto"; option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; @@ -13,7 +12,5 @@ option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; message GenesisState { Params params = 1 [(gogoproto.nullable) = false]; repeated pocket.shared.Supplier supplierList = 2 [(gogoproto.nullable) = false]; - // TODO_UPNEXT(@Olshansk): Delete `claimList` from the genesis state. - repeated Claim claimList = 3 [(gogoproto.nullable) = false]; } diff --git a/proto/pocket/supplier/query.proto b/proto/pocket/supplier/query.proto index 8e15e041a..fdc90a4c2 100644 --- a/proto/pocket/supplier/query.proto +++ b/proto/pocket/supplier/query.proto @@ -24,15 +24,15 @@ service Query { option (google.api.http).get = "/pocket/supplier/supplier/{address}"; } rpc SupplierAll (QueryAllSupplierRequest) returns (QueryAllSupplierResponse) { - option (google.api.http).get = "/pocket/supplier/supplier"; + option (google.api.http).get = "/pocket/supplier/suppliers"; } // Queries a list of Claim items. rpc Claim (QueryGetClaimRequest) returns (QueryGetClaimResponse) { - option (google.api.http).get = "/pocket/supplier/claim/{index}"; + option (google.api.http).get = "/pocket/supplier/claim/{session_id}/{supplier_address}"; } rpc AllClaims (QueryAllClaimsRequest) returns (QueryAllClaimsResponse) { - option (google.api.http).get = "/pocket/supplier/claim"; + option (google.api.http).get = "/pocket/supplier/claims"; } } // QueryParamsRequest is request type for the Query/Params RPC method. @@ -62,7 +62,8 @@ message QueryAllSupplierResponse { } message QueryGetClaimRequest { - string index = 1; + string session_id = 1; + string supplier_address = 2; } message QueryGetClaimResponse { @@ -71,6 +72,11 @@ message QueryGetClaimResponse { message QueryAllClaimsRequest { cosmos.base.query.v1beta1.PageRequest pagination = 1; + oneof filter { + string supplier_address = 2; + string session_id = 3; + uint64 session_end_height = 4; + } } message QueryAllClaimsResponse { diff --git a/testutil/network/network.go b/testutil/network/network.go index 28f4eaa2f..d05c283be 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -1,6 +1,7 @@ package network import ( + "encoding/json" "fmt" "testing" "time" @@ -172,6 +173,32 @@ func DefaultSupplierModuleGenesisState(t *testing.T, n int) *suppliertypes.Genes return state } +// SupplierModuleGenesisStateWithAccount generates a GenesisState object with a single supplier with the given address. +func SupplierModuleGenesisStateWithAccounts(t *testing.T, addresses []string) *suppliertypes.GenesisState { + t.Helper() + state := suppliertypes.DefaultGenesis() + for _, addr := range addresses { + supplier := sharedtypes.Supplier{ + Address: addr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(10000)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + Service: &sharedtypes.Service{Id: "svc1"}, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:1", + RpcType: sharedtypes.RPCType_JSON_RPC, + }, + }, + }, + }, + } + state.SupplierList = append(state.SupplierList, supplier) + } + + return state +} + // Initialize an Account by sending it some funds from the validator in the network to the address provided func InitAccount(t *testing.T, net *Network, addr sdk.AccAddress) { t.Helper() @@ -184,6 +211,40 @@ func InitAccount(t *testing.T, net *Network, addr sdk.AccAddress) { fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(net.Config.BondDenom, sdkmath.NewInt(10))).String()), } amount := sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(200))) - _, err := clitestutil.MsgSendExec(ctx, val.Address, addr, amount, args...) + responseRaw, err := clitestutil.MsgSendExec(ctx, val.Address, addr, amount, args...) + require.NoError(t, err) + var responseJson map[string]interface{} + err = json.Unmarshal(responseRaw.Bytes(), &responseJson) + require.NoError(t, err) + require.Equal(t, float64(0), responseJson["code"], "code is not 0 in the response: %v", responseJson) +} + +// Initialize an Account by sending it some funds from the validator in the network to the address provided +func InitAccountWithSequence( + t *testing.T, + net *Network, + addr sdk.AccAddress, + signatureSequencerNumber int, +) { + t.Helper() + val := net.Validators[0] + signerAccountNumber := 0 + ctx := val.ClientCtx + args := []string{ + fmt.Sprintf("--%s=true", flags.FlagOffline), + fmt.Sprintf("--%s=%d", flags.FlagAccountNumber, signerAccountNumber), + fmt.Sprintf("--%s=%d", flags.FlagSequence, signatureSequencerNumber), + + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(net.Config.BondDenom, sdkmath.NewInt(10))).String()), + } + amount := sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(200))) + responseRaw, err := clitestutil.MsgSendExec(ctx, val.Address, addr, amount, args...) + require.NoError(t, err) + var responseJson map[string]interface{} + err = json.Unmarshal(responseRaw.Bytes(), &responseJson) require.NoError(t, err) + require.Equal(t, float64(0), responseJson["code"], "code is not 0 in the response: %v", responseJson) } diff --git a/x/application/client/cli/tx_delegate_to_gateway_test.go b/x/application/client/cli/tx_delegate_to_gateway_test.go index 97ad29a91..22d2630b2 100644 --- a/x/application/client/cli/tx_delegate_to_gateway_test.go +++ b/x/application/client/cli/tx_delegate_to_gateway_test.go @@ -78,8 +78,8 @@ func TestCLI_DelegateToGateway(t *testing.T) { } // Initialize the App and Gateway Accounts by sending it some funds from the validator account that is part of genesis - network.InitAccount(t, net, appAccount.Address) - network.InitAccount(t, net, gatewayAccount.Address) + network.InitAccountWithSequence(t, net, appAccount.Address, 1) + network.InitAccountWithSequence(t, net, gatewayAccount.Address, 2) // Run the tests for _, tt := range tests { diff --git a/x/application/client/cli/tx_undelegate_from_gateway_test.go b/x/application/client/cli/tx_undelegate_from_gateway_test.go index a1c57973d..a5d942268 100644 --- a/x/application/client/cli/tx_undelegate_from_gateway_test.go +++ b/x/application/client/cli/tx_undelegate_from_gateway_test.go @@ -78,8 +78,8 @@ func TestCLI_UndelegateFromGateway(t *testing.T) { } // Initialize the App and Gateway Accounts by sending it some funds from the validator account that is part of genesis - network.InitAccount(t, net, appAccount.Address) - network.InitAccount(t, net, gatewayAccount.Address) + network.InitAccountWithSequence(t, net, appAccount.Address, 1) + network.InitAccountWithSequence(t, net, gatewayAccount.Address, 2) // Run the tests for _, tt := range tests { diff --git a/x/application/keeper/query_application.go b/x/application/keeper/query_application.go index 1a6b2a1b7..c08c3c925 100644 --- a/x/application/keeper/query_application.go +++ b/x/application/keeper/query_application.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "fmt" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" @@ -51,7 +52,7 @@ func (k Keeper) Application(goCtx context.Context, req *types.QueryGetApplicatio req.Address, ) if !found { - return nil, status.Error(codes.NotFound, "not found") + return nil, status.Error(codes.NotFound, fmt.Sprintf("application not found: address %s", req.Address)) } return &types.QueryGetApplicationResponse{Application: val}, nil diff --git a/x/application/keeper/query_application_test.go b/x/application/keeper/query_application_test.go index feb0fef55..0cc9c35e9 100644 --- a/x/application/keeper/query_application_test.go +++ b/x/application/keeper/query_application_test.go @@ -47,7 +47,7 @@ func TestApplicationQuerySingle(t *testing.T) { request: &types.QueryGetApplicationRequest{ Address: strconv.Itoa(100000), }, - err: status.Error(codes.NotFound, "not found"), + err: status.Error(codes.NotFound, "application not found"), }, { desc: "InvalidRequest", @@ -58,7 +58,7 @@ func TestApplicationQuerySingle(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { response, err := keeper.Application(wctx, tc.request) if tc.err != nil { - require.ErrorIs(t, err, tc.err) + require.ErrorContains(t, err, tc.err.Error()) } else { require.NoError(t, err) require.Equal(t, diff --git a/x/gateway/keeper/query_gateway.go b/x/gateway/keeper/query_gateway.go index bd0576a2d..7e18dea34 100644 --- a/x/gateway/keeper/query_gateway.go +++ b/x/gateway/keeper/query_gateway.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "fmt" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" @@ -51,7 +52,7 @@ func (k Keeper) Gateway(goCtx context.Context, req *types.QueryGetGatewayRequest req.Address, ) if !found { - return nil, status.Error(codes.NotFound, "not found") + return nil, status.Error(codes.NotFound, fmt.Sprintf("gateway not found: address %s", req.Address)) } return &types.QueryGetGatewayResponse{Gateway: val}, nil diff --git a/x/gateway/keeper/query_gateway_test.go b/x/gateway/keeper/query_gateway_test.go index 9d44de980..4c289e6ae 100644 --- a/x/gateway/keeper/query_gateway_test.go +++ b/x/gateway/keeper/query_gateway_test.go @@ -47,7 +47,7 @@ func TestGatewayQuerySingle(t *testing.T) { request: &types.QueryGetGatewayRequest{ Address: strconv.Itoa(100000), }, - err: status.Error(codes.NotFound, "not found"), + err: status.Error(codes.NotFound, "gateway not found"), }, { desc: "InvalidRequest", @@ -58,7 +58,7 @@ func TestGatewayQuerySingle(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { response, err := keeper.Gateway(wctx, tc.request) if tc.err != nil { - require.ErrorIs(t, err, tc.err) + require.ErrorContains(t, err, tc.err.Error()) } else { require.NoError(t, err) require.Equal(t, diff --git a/x/supplier/client/cli/query.go b/x/supplier/client/cli/query.go index f70e996ba..ab7697ac9 100644 --- a/x/supplier/client/cli/query.go +++ b/x/supplier/client/cli/query.go @@ -27,7 +27,7 @@ func GetQueryCmd(queryRoute string) *cobra.Command { cmd.AddCommand(CmdQueryParams()) cmd.AddCommand(CmdListSupplier()) cmd.AddCommand(CmdShowSupplier()) - cmd.AddCommand(CmdListClaim()) + cmd.AddCommand(CmdListClaims()) cmd.AddCommand(CmdShowClaim()) // this line is used by starport scaffolding # 1 diff --git a/x/supplier/client/cli/query_claim.go b/x/supplier/client/cli/query_claim.go index a71bdd246..26a667a3b 100644 --- a/x/supplier/client/cli/query_claim.go +++ b/x/supplier/client/cli/query_claim.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "strconv" "github.com/cosmos/cosmos-sdk/client" @@ -13,37 +14,64 @@ import ( // Prevent strconv unused error var _ = strconv.IntSize -func CmdListClaim() *cobra.Command { +const ( + FlagSessionEndHeight = "session-end-height" + FlagSessionId = "session-id" + FlagSupplierAddress = "supplier-address" +) + +// AddPaginationFlagsToCmd adds common pagination flags to cmd +func AddClaimFilterFlags(cmd *cobra.Command) { + cmd.Flags().Uint64(FlagSessionEndHeight, 0, "claims whose session ends at this height will be returned") + cmd.Flags().String(FlagSessionId, "", "claims matching this session id will be returned") + cmd.Flags().String(FlagSupplierAddress, "", "claims submitted by suppliers matching this address will be returned") +} + +func CmdListClaims() *cobra.Command { cmd := &cobra.Command{ Use: "list-claims", Short: "list all claims", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - clientCtx, err := client.GetClientQueryContext(cmd) - if err != nil { - return err - } + Long: `List all the claims that the node being queried has in its state. + +The claims can be optionally filtered by one of --session-end-height --session-id or --supplier-address flags +Example: +$ poktrolld --home=$(POKTROLLD_HOME) q claim list-claims --node $(POCKET_NODE) +$ poktrolld --home=$(POKTROLLD_HOME) q claim list-claims --session-id --node $(POCKET_NODE) +$ poktrolld --home=$(POKTROLLD_HOME) q claim list-claims --session-end-height --node $(POCKET_NODE) +$ poktrolld --home=$(POKTROLLD_HOME) q claim list-claims --supplier-address --node $(POCKET_NODE)`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { pageReq, err := client.ReadPageRequest(cmd.Flags()) if err != nil { return err } - queryClient := types.NewQueryClient(clientCtx) - - params := &types.QueryAllClaimsRequest{ + req := &types.QueryAllClaimsRequest{ Pagination: pageReq, } + if err := updateClaimsFilter(cmd, req); err != nil { + return err + } + if err := req.ValidateBasic(); err != nil { + return err + } - res, err := queryClient.AllClaims(cmd.Context(), params) + clientCtx, err := client.GetClientQueryContext(cmd) if err != nil { return err } + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.AllClaims(cmd.Context(), req) + if err != nil { + return err + } return clientCtx.PrintProto(res) }, } + AddClaimFilterFlags(cmd) flags.AddPaginationFlagsToCmd(cmd, cmd.Use) flags.AddQueryFlagsToCmd(cmd) @@ -52,10 +80,27 @@ func CmdListClaim() *cobra.Command { func CmdShowClaim() *cobra.Command { cmd := &cobra.Command{ - Use: "show-claim ", - Short: "shows a claim", - Args: cobra.ExactArgs(1), + Use: "show-claim ", + Short: "shows a specific claim", + Long: `List a specific claim that the node being queried has access to (if it still exists) + +A unique claim can be defined via a session_id that a supplier participated in + +Example: +$ poktrolld --home=$(POKTROLLD_HOME) q claim show-claims --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { + sessionId := args[0] + supplierAddr := args[1] + + getClaimRequest := &types.QueryGetClaimRequest{ + SessionId: sessionId, + SupplierAddress: supplierAddr, + } + if err := getClaimRequest.ValidateBasic(); err != nil { + return err + } + clientCtx, err := client.GetClientQueryContext(cmd) if err != nil { return err @@ -63,13 +108,7 @@ func CmdShowClaim() *cobra.Command { queryClient := types.NewQueryClient(clientCtx) - argIndex := args[0] - - params := &types.QueryGetClaimRequest{ - Index: argIndex, - } - - res, err := queryClient.Claim(cmd.Context(), params) + res, err := queryClient.Claim(cmd.Context(), getClaimRequest) if err != nil { return err } @@ -82,3 +121,54 @@ func CmdShowClaim() *cobra.Command { return cmd } + +// updateClaimsFilter updates the claims filter request based on the flags set provided +func updateClaimsFilter(cmd *cobra.Command, req *types.QueryAllClaimsRequest) error { + sessionId, _ := cmd.Flags().GetString(FlagSessionId) + supplierAddr, _ := cmd.Flags().GetString(FlagSupplierAddress) + sessionEndHeight, _ := cmd.Flags().GetUint64(FlagSessionEndHeight) + + // Preparing a shared error in case more than one flag was set + err := fmt.Errorf("can only specify one flag filter but got sessionId (%s), supplierAddr (%s) and sessionEngHeight (%d)", sessionId, supplierAddr, sessionEndHeight) + + // Use the session id as the filter + if sessionId != "" { + // If the session id is set, then the other flags must not be set + if supplierAddr != "" || sessionEndHeight > 0 { + return err + } + // Set the session id filter + req.Filter = &types.QueryAllClaimsRequest_SessionId{ + SessionId: sessionId, + } + return nil + } + + // Use the supplier address as the filter + if supplierAddr != "" { + // If the supplier address is set, then the other flags must not be set + if sessionId != "" || sessionEndHeight > 0 { + return err + } + // Set the supplier address filter + req.Filter = &types.QueryAllClaimsRequest_SupplierAddress{ + SupplierAddress: supplierAddr, + } + return nil + } + + // Use the session end height as the filter + if sessionEndHeight > 0 { + // If the session end height is set, then the other flags must not be set + if sessionId != "" || supplierAddr != "" { + return err + } + // Set the session end height filter + req.Filter = &types.QueryAllClaimsRequest_SessionEndHeight{ + SessionEndHeight: sessionEndHeight, + } + return nil + } + + return nil +} diff --git a/x/supplier/client/cli/query_claim_test.go b/x/supplier/client/cli/query_claim_test.go index 3c42d23de..c36abf15a 100644 --- a/x/supplier/client/cli/query_claim_test.go +++ b/x/supplier/client/cli/query_claim_test.go @@ -1,65 +1,181 @@ package cli_test import ( + "encoding/base64" + "encoding/json" "fmt" - "strconv" "testing" + sdkmath "cosmossdk.io/math" tmcli "github.com/cometbft/cometbft/libs/cli" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/testutil" clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/pokt-network/poktroll/testutil/network" "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" "github.com/pokt-network/poktroll/x/supplier/client/cli" "github.com/pokt-network/poktroll/x/supplier/types" ) -func networkWithClaimObjects(t *testing.T, n int) (*network.Network, []types.Claim) { +// TODO_TECHDEBT: This should not be hardcoded once the num blocks per session is configurable +const numBlocksPerSession = 4 + +func encodeSessionHeader(t *testing.T, sessionId string, sessionEndHeight int64) string { + t.Helper() + + argSessionHeader := &sessiontypes.SessionHeader{ + ApplicationAddress: sample.AccAddress(), + SessionStartBlockHeight: sessionEndHeight - numBlocksPerSession, + SessionId: sessionId, + SessionEndBlockHeight: sessionEndHeight, + Service: &sharedtypes.Service{Id: "anvil"}, // hardcoded for simplicity + } + cdc := codec.NewProtoCodec(cdctypes.NewInterfaceRegistry()) + sessionHeaderBz := cdc.MustMarshalJSON(argSessionHeader) + return base64.StdEncoding.EncodeToString(sessionHeaderBz) +} + +func createClaim( + t *testing.T, + net *network.Network, + ctx client.Context, + supplierAddr string, + sessionId string, + sessionEndHeight int64, +) *types.Claim { + t.Helper() + + rootHash := []byte("root_hash") + sessionHeaderEncoded := encodeSessionHeader(t, sessionId, sessionEndHeight) + rootHashEncoded := base64.StdEncoding.EncodeToString(rootHash) + + args := []string{ + sessionHeaderEncoded, + rootHashEncoded, + fmt.Sprintf("--%s=%s", flags.FlagFrom, supplierAddr), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(net.Config.BondDenom, sdkmath.NewInt(10))).String()), + } + + responseRaw, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdCreateClaim(), args) + require.NoError(t, err) + var responseJson map[string]interface{} + err = json.Unmarshal(responseRaw.Bytes(), &responseJson) + require.NoError(t, err) + require.Equal(t, float64(0), responseJson["code"], "code is not 0 in the response: %v", responseJson) + + return &types.Claim{ + SupplierAddress: supplierAddr, + SessionId: sessionId, + SessionEndBlockHeight: uint64(sessionEndHeight), + RootHash: rootHash, + } +} + +func networkWithClaimObjects( + t *testing.T, + numSessions int, + numClaimsPerSession int, +) (net *network.Network, claims []types.Claim) { t.Helper() + + // Prepare the network cfg := network.DefaultConfig() - state := types.GenesisState{} - for i := 0; i < n; i++ { - claim := types.Claim{ - Index: strconv.Itoa(i), - } - nullify.Fill(&claim) - state.ClaimList = append(state.ClaimList, claim) + net = network.New(t, cfg) + ctx := net.Validators[0].ClientCtx + + // Prepare the keyring for the supplier account + kr := ctx.Keyring + accounts := testutil.CreateKeyringAccounts(t, kr, numClaimsPerSession) + ctx = ctx.WithKeyring(kr) + + // Initialize all the accounts + for i, account := range accounts { + signatureSequenceNumber := i + 1 + network.InitAccountWithSequence(t, net, account.Address, signatureSequenceNumber) } - buf, err := cfg.Codec.MarshalJSON(&state) + // need to wait for the account to be initialized in the next block + require.NoError(t, net.WaitForNextBlock()) + + addresses := make([]string, len(accounts)) + for i, account := range accounts { + addresses[i] = account.Address.String() + } + + // Create one supplier + supplierGenesisState := network.SupplierModuleGenesisStateWithAccounts(t, addresses) + buf, err := cfg.Codec.MarshalJSON(supplierGenesisState) require.NoError(t, err) cfg.GenesisState[types.ModuleName] = buf - return network.New(t, cfg), state.ClaimList + + // Create numSessions * numClaimsPerSession claims for the supplier + sessionEndHeight := int64(1) + for sessionNum := 0; sessionNum < numSessions; sessionNum++ { + sessionEndHeight += numBlocksPerSession + sessionId := fmt.Sprintf("session_id%d", sessionNum) + for claimNum := 0; claimNum < numClaimsPerSession; claimNum++ { + supplierAddr := addresses[claimNum] + claim := createClaim(t, net, ctx, supplierAddr, sessionId, sessionEndHeight) + claims = append(claims, *claim) + // TODO_TECHDEBT(#196): Move this outside of the forloop so that the test iteration is faster + require.NoError(t, net.WaitForNextBlock()) + } + } + + return net, claims } -func TestShowClaim(t *testing.T) { - net, objs := networkWithClaimObjects(t, 2) +func TestClaim_Show(t *testing.T) { + numSessions := 1 + numClaimsPerSession := 2 + + net, claims := networkWithClaimObjects(t, numSessions, numClaimsPerSession) ctx := net.Validators[0].ClientCtx common := []string{ fmt.Sprintf("--%s=json", tmcli.OutputFlag), } tests := []struct { - desc string - idIndex string + desc string + sessionId string + supplierAddr string args []string err error obj types.Claim }{ { - desc: "found", - idIndex: objs[0].Index, + desc: "claim found", + sessionId: claims[0].SessionId, + supplierAddr: claims[0].SupplierAddress, + + args: common, + obj: claims[0], + }, + { + desc: "claim not found (wrong session ID)", + sessionId: "wrong_session_id", + supplierAddr: claims[0].SupplierAddress, args: common, - obj: objs[0], + err: status.Error(codes.NotFound, "not found"), }, { - desc: "not found", - idIndex: strconv.Itoa(100000), + desc: "claim not found (wrong supplier address)", + sessionId: claims[0].SessionId, + supplierAddr: "wrong_supplier_address", args: common, err: status.Error(codes.NotFound, "not found"), @@ -68,7 +184,8 @@ func TestShowClaim(t *testing.T) { for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { args := []string{ - tc.idIndex, + tc.sessionId, + tc.supplierAddr, } args = append(args, tc.args...) out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdShowClaim(), args) @@ -90,11 +207,15 @@ func TestShowClaim(t *testing.T) { } } -func TestListClaim(t *testing.T) { - net, objs := networkWithClaimObjects(t, 5) +func TestClaim_List(t *testing.T) { + numSessions := 2 + numClaimsPerSession := 5 + totalClaims := numSessions * numClaimsPerSession + + net, claims := networkWithClaimObjects(t, numSessions, numClaimsPerSession) ctx := net.Validators[0].ClientCtx - request := func(next []byte, offset, limit uint64, total bool) []string { + prepareArgs := func(next []byte, offset, limit uint64, total bool) []string { args := []string{ fmt.Sprintf("--%s=json", tmcli.OutputFlag), } @@ -109,48 +230,131 @@ func TestListClaim(t *testing.T) { } return args } + t.Run("ByOffset", func(t *testing.T) { step := 2 - for i := 0; i < len(objs); i += step { - args := request(nil, uint64(i), uint64(step), false) - out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListClaim(), args) + for i := 0; i < totalClaims; i += step { + args := prepareArgs(nil, uint64(i), uint64(step), false) + out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListClaims(), args) require.NoError(t, err) + var resp types.QueryAllClaimsResponse require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) + require.LessOrEqual(t, len(resp.Claim), step) require.Subset(t, - nullify.Fill(objs), + nullify.Fill(claims), nullify.Fill(resp.Claim), ) } }) + t.Run("ByKey", func(t *testing.T) { step := 2 var next []byte - for i := 0; i < len(objs); i += step { - args := request(next, 0, uint64(step), false) - out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListClaim(), args) + for i := 0; i < totalClaims; i += step { + args := prepareArgs(next, 0, uint64(step), false) + out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListClaims(), args) require.NoError(t, err) + var resp types.QueryAllClaimsResponse require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) + require.LessOrEqual(t, len(resp.Claim), step) require.Subset(t, - nullify.Fill(objs), + nullify.Fill(claims), nullify.Fill(resp.Claim), ) next = resp.Pagination.NextKey } }) - t.Run("Total", func(t *testing.T) { - args := request(nil, 0, uint64(len(objs)), true) - out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListClaim(), args) + + t.Run("ByAddress", func(t *testing.T) { + supplierAddr := claims[0].SupplierAddress + args := prepareArgs(nil, 0, uint64(totalClaims), true) + args = append(args, fmt.Sprintf("--%s=%s", cli.FlagSupplierAddress, supplierAddr)) + + expectedClaims := make([]types.Claim, 0) + for _, claim := range claims { + if claim.SupplierAddress == supplierAddr { + expectedClaims = append(expectedClaims, claim) + } + } + + out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListClaims(), args) + require.NoError(t, err) + + var resp types.QueryAllClaimsResponse + require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) + + require.Equal(t, numSessions, int(resp.Pagination.Total)) + require.ElementsMatch(t, + nullify.Fill(expectedClaims), + nullify.Fill(resp.Claim), + ) + }) + + t.Run("BySession", func(t *testing.T) { + sessionId := claims[0].SessionId + args := prepareArgs(nil, 0, uint64(totalClaims), true) + args = append(args, fmt.Sprintf("--%s=%s", cli.FlagSessionId, sessionId)) + + expectedClaims := make([]types.Claim, 0) + for _, claim := range claims { + if claim.SessionId == sessionId { + expectedClaims = append(expectedClaims, claim) + } + } + + out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListClaims(), args) + require.NoError(t, err) + + var resp types.QueryAllClaimsResponse + require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) + + require.Equal(t, numClaimsPerSession, int(resp.Pagination.Total)) + require.ElementsMatch(t, + nullify.Fill(expectedClaims), + nullify.Fill(resp.Claim), + ) + }) + + t.Run("ByHeight", func(t *testing.T) { + sessionEndHeight := claims[0].SessionEndBlockHeight + args := prepareArgs(nil, 0, uint64(totalClaims), true) + args = append(args, fmt.Sprintf("--%s=%d", cli.FlagSessionEndHeight, sessionEndHeight)) + + expectedClaims := make([]types.Claim, 0) + for _, claim := range claims { + if claim.SessionEndBlockHeight == sessionEndHeight { + expectedClaims = append(expectedClaims, claim) + } + } + + out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListClaims(), args) require.NoError(t, err) + var resp types.QueryAllClaimsResponse require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) + + require.Equal(t, numClaimsPerSession, int(resp.Pagination.Total)) + require.ElementsMatch(t, + nullify.Fill(expectedClaims), + nullify.Fill(resp.Claim), + ) + }) + + t.Run("Total", func(t *testing.T) { + args := prepareArgs(nil, 0, uint64(totalClaims), true) + out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListClaims(), args) require.NoError(t, err) - require.Equal(t, len(objs), int(resp.Pagination.Total)) + + var resp types.QueryAllClaimsResponse + require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) + + require.Equal(t, totalClaims, int(resp.Pagination.Total)) require.ElementsMatch(t, - nullify.Fill(objs), + nullify.Fill(claims), nullify.Fill(resp.Claim), ) }) diff --git a/x/supplier/client/cli/query_supplier_test.go b/x/supplier/client/cli/query_supplier_test.go index 378811a2f..b8a4781a9 100644 --- a/x/supplier/client/cli/query_supplier_test.go +++ b/x/supplier/client/cli/query_supplier_test.go @@ -19,7 +19,7 @@ import ( ) func TestShowSupplier(t *testing.T) { - net, objs := networkWithSupplierObjects(t, 2) + net, suppliers := networkWithSupplierObjects(t, 2) ctx := net.Validators[0].ClientCtx common := []string{ @@ -34,14 +34,14 @@ func TestShowSupplier(t *testing.T) { obj sharedtypes.Supplier }{ { - desc: "found", - idAddress: objs[0].Address, + desc: "supplier found", + idAddress: suppliers[0].Address, args: common, - obj: objs[0], + obj: suppliers[0], }, { - desc: "not found", + desc: "supplier not found", idAddress: strconv.Itoa(100000), args: common, @@ -74,7 +74,7 @@ func TestShowSupplier(t *testing.T) { } func TestListSupplier(t *testing.T) { - net, objs := networkWithSupplierObjects(t, 5) + net, suppliers := networkWithSupplierObjects(t, 5) ctx := net.Validators[0].ClientCtx request := func(next []byte, offset, limit uint64, total bool) []string { @@ -94,7 +94,7 @@ func TestListSupplier(t *testing.T) { } t.Run("ByOffset", func(t *testing.T) { step := 2 - for i := 0; i < len(objs); i += step { + for i := 0; i < len(suppliers); i += step { args := request(nil, uint64(i), uint64(step), false) out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListSupplier(), args) require.NoError(t, err) @@ -102,7 +102,7 @@ func TestListSupplier(t *testing.T) { require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.LessOrEqual(t, len(resp.Supplier), step) require.Subset(t, - nullify.Fill(objs), + nullify.Fill(suppliers), nullify.Fill(resp.Supplier), ) } @@ -110,7 +110,7 @@ func TestListSupplier(t *testing.T) { t.Run("ByKey", func(t *testing.T) { step := 2 var next []byte - for i := 0; i < len(objs); i += step { + for i := 0; i < len(suppliers); i += step { args := request(next, 0, uint64(step), false) out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListSupplier(), args) require.NoError(t, err) @@ -118,22 +118,22 @@ func TestListSupplier(t *testing.T) { require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.LessOrEqual(t, len(resp.Supplier), step) require.Subset(t, - nullify.Fill(objs), + nullify.Fill(suppliers), nullify.Fill(resp.Supplier), ) next = resp.Pagination.NextKey } }) t.Run("Total", func(t *testing.T) { - args := request(nil, 0, uint64(len(objs)), true) + args := request(nil, 0, uint64(len(suppliers)), true) out, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdListSupplier(), args) require.NoError(t, err) var resp types.QueryAllSupplierResponse require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.NoError(t, err) - require.Equal(t, len(objs), int(resp.Pagination.Total)) + require.Equal(t, len(suppliers), int(resp.Pagination.Total)) require.ElementsMatch(t, - nullify.Fill(objs), + nullify.Fill(suppliers), nullify.Fill(resp.Supplier), ) }) diff --git a/x/supplier/client/cli/tx_create_claim.go b/x/supplier/client/cli/tx_create_claim.go index 2869ce95d..9fd509e55 100644 --- a/x/supplier/client/cli/tx_create_claim.go +++ b/x/supplier/client/cli/tx_create_claim.go @@ -2,12 +2,13 @@ package cli import ( "encoding/base64" - "encoding/json" "strconv" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/spf13/cobra" sessiontypes "github.com/pokt-network/poktroll/x/session/types" @@ -24,12 +25,20 @@ func CmdCreateClaim() *cobra.Command { Short: "Broadcast message create-claim", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { - argSessionHeader := new(sessiontypes.SessionHeader) - err = json.Unmarshal([]byte(args[0]), argSessionHeader) + sessionHeaderEncodedStr := args[0] + rootHashEncodedStr := args[1] + + // Get the session header + cdc := codec.NewProtoCodec(cdctypes.NewInterfaceRegistry()) + sessionHeaderBz, err := base64.StdEncoding.DecodeString(sessionHeaderEncodedStr) if err != nil { return err } - argRootHash, err := base64.StdEncoding.DecodeString(args[1]) + sessionHeader := sessiontypes.SessionHeader{} + cdc.MustUnmarshalJSON(sessionHeaderBz, &sessionHeader) + + // Get the root hash + rootHash, err := base64.StdEncoding.DecodeString(rootHashEncodedStr) if err != nil { return err } @@ -38,15 +47,17 @@ func CmdCreateClaim() *cobra.Command { if err != nil { return err } + supplierAddress := clientCtx.GetFromAddress().String() msg := types.NewMsgCreateClaim( - clientCtx.GetFromAddress().String(), - argSessionHeader, - argRootHash, + supplierAddress, + &sessionHeader, + rootHash, ) if err := msg.ValidateBasic(); err != nil { return err } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } diff --git a/x/supplier/genesis.go b/x/supplier/genesis.go index bda688fb9..156d80118 100644 --- a/x/supplier/genesis.go +++ b/x/supplier/genesis.go @@ -14,10 +14,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) for _, supplier := range genState.SupplierList { k.SetSupplier(ctx, supplier) } - // Set all the claim - for _, elem := range genState.ClaimList { - k.SetClaim(ctx, elem) - } // this line is used by starport scaffolding # genesis/module/init k.SetParams(ctx, genState.Params) } @@ -28,7 +24,6 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis.Params = k.GetParams(ctx) genesis.SupplierList = k.GetAllSupplier(ctx) - genesis.ClaimList = k.GetAllClaims(ctx) // this line is used by starport scaffolding # genesis/module/export return genesis diff --git a/x/supplier/genesis_test.go b/x/supplier/genesis_test.go index 3afdd0a8a..ba9b521f8 100644 --- a/x/supplier/genesis_test.go +++ b/x/supplier/genesis_test.go @@ -56,14 +56,6 @@ func TestGenesis(t *testing.T) { }, }, }, - ClaimList: []types.Claim{ - { - Index: "0", - }, - { - Index: "1", - }, - }, // this line is used by starport scaffolding # genesis/test/state } @@ -76,6 +68,5 @@ func TestGenesis(t *testing.T) { nullify.Fill(got) require.ElementsMatch(t, genesisState.SupplierList, got.SupplierList) - require.ElementsMatch(t, genesisState.ClaimList, got.ClaimList) // this line is used by starport scaffolding # genesis/test/assert } diff --git a/x/supplier/keeper/claim.go b/x/supplier/keeper/claim.go index db6f627b4..511d8f7da 100644 --- a/x/supplier/keeper/claim.go +++ b/x/supplier/keeper/claim.go @@ -1,64 +1,156 @@ package keeper import ( + "encoding/binary" + "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/pokt-network/poktroll/x/supplier/types" ) -// SetClaim set a specific claim in the store from its index -func (k Keeper) SetClaim(ctx sdk.Context, claim types.Claim) { - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimKeyPrefix)) - b := k.cdc.MustMarshal(&claim) - store.Set(types.ClaimKey( - claim.Index, - ), b) +// InsertClaim adds a claim to the store +func (k Keeper) InsertClaim(ctx sdk.Context, claim types.Claim) { + logger := k.Logger(ctx).With("method", "InsertClaim") + + claimBz := k.cdc.MustMarshal(&claim) + parentStore := ctx.KVStore(k.storeKey) + + // Update the primary store: ClaimPrimaryKey -> ClaimObject + primaryStore := prefix.NewStore(parentStore, types.KeyPrefix(types.ClaimPrimaryKeyPrefix)) + primaryKey := types.ClaimPrimaryKey(claim.SessionId, claim.SupplierAddress) + primaryStore.Set(primaryKey, claimBz) + + logger.Info("inserted claim for supplier %s with primaryKey %s", claim.SupplierAddress, primaryKey) + + // Update the address index: supplierAddress -> [ClaimPrimaryKey] + addressStoreIndex := prefix.NewStore(parentStore, types.KeyPrefix(types.ClaimSupplierAddressPrefix)) + addressKey := types.ClaimSupplierAddressKey(claim.SupplierAddress, primaryKey) + addressStoreIndex.Set(addressKey, primaryKey) + + logger.Info("indexed claim for supplier %s with primaryKey %s", claim.SupplierAddress, primaryKey) + + // Update the session end height index: sessionEndHeight -> [ClaimPrimaryKey] + sessionHeightStoreIndex := prefix.NewStore(parentStore, types.KeyPrefix(types.ClaimSessionEndHeightPrefix)) + heightKey := types.ClaimSupplierEndSessionHeightKey(claim.SessionEndBlockHeight, primaryKey) + sessionHeightStoreIndex.Set(heightKey, primaryKey) + + logger.Info("indexed claim for supplier %s at session ending height %d", claim.SupplierAddress, claim.SessionEndBlockHeight) } -// GetClaim returns a claim from its index -func (k Keeper) GetClaim( - ctx sdk.Context, - index string, +// RemoveClaim removes a claim from the store +func (k Keeper) RemoveClaim(ctx sdk.Context, sessionId, supplierAddr string) { + logger := k.Logger(ctx).With("method", "RemoveClaim") -) (val types.Claim, found bool) { - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimKeyPrefix)) + parentStore := ctx.KVStore(k.storeKey) + primaryStore := prefix.NewStore(parentStore, types.KeyPrefix(types.ClaimPrimaryKeyPrefix)) - b := store.Get(types.ClaimKey( - index, - )) - if b == nil { - return val, false + // Check if the claim exists + primaryKey := types.ClaimPrimaryKey(sessionId, supplierAddr) + claim, foundClaim := k.getClaimByPrimaryKey(ctx, primaryKey) + if !foundClaim { + logger.Error("trying to delete non-existent claim with primary key %s for supplier %s and session %s", primaryKey, supplierAddr, sessionId) + return } - k.cdc.MustUnmarshal(b, &val) - return val, true + // Prepare the indices for deletion + addressStoreIndex := prefix.NewStore(parentStore, types.KeyPrefix(types.ClaimSupplierAddressPrefix)) + sessionHeightStoreIndex := prefix.NewStore(parentStore, types.KeyPrefix(types.ClaimSessionEndHeightPrefix)) + + addressKey := types.ClaimSupplierAddressKey(claim.SupplierAddress, primaryKey) + heightKey := types.ClaimSupplierEndSessionHeightKey(claim.SessionEndBlockHeight, primaryKey) + + // Delete all the entries (primary store and secondary indices) + primaryStore.Delete(primaryKey) + addressStoreIndex.Delete(addressKey) + sessionHeightStoreIndex.Delete(heightKey) + + logger.Info("deleted claim with primary key %s for supplier %s and session %s", primaryKey, supplierAddr, sessionId) } -// RemoveClaim removes a claim from the store -func (k Keeper) RemoveClaim( - ctx sdk.Context, - index string, - -) { - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimKeyPrefix)) - store.Delete(types.ClaimKey( - index, - )) +// GetClaim returns a Claim given a SessionId & SupplierAddr +func (k Keeper) GetClaim(ctx sdk.Context, sessionId, supplierAddr string) (val types.Claim, found bool) { + primaryKey := types.ClaimPrimaryKey(sessionId, supplierAddr) + return k.getClaimByPrimaryKey(ctx, primaryKey) } // GetAllClaims returns all claim -func (k Keeper) GetAllClaims(ctx sdk.Context) (list []types.Claim) { - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimKeyPrefix)) - iterator := sdk.KVStorePrefixIterator(store, []byte{}) - +func (k Keeper) GetAllClaims(ctx sdk.Context) (claims []types.Claim) { + primaryStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimPrimaryKeyPrefix)) + iterator := sdk.KVStorePrefixIterator(primaryStore, []byte{}) defer iterator.Close() for ; iterator.Valid(); iterator.Next() { var val types.Claim k.cdc.MustUnmarshal(iterator.Value(), &val) - list = append(list, val) + claims = append(claims, val) } return } + +// GetClaimsByAddress returns all claims for a given supplier address +func (k Keeper) GetClaimsByAddress(ctx sdk.Context, address string) (claims []types.Claim) { + addressStoreIndex := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimSupplierAddressPrefix)) + + iterator := sdk.KVStorePrefixIterator(addressStoreIndex, []byte(address)) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + primaryKey := iterator.Value() + claim, claimFound := k.getClaimByPrimaryKey(ctx, primaryKey) + if claimFound { + claims = append(claims, claim) + } + } + + return claims +} + +// GetClaimsByAddress returns all claims whose session ended at the given block height +func (k Keeper) GetClaimsByHeight(ctx sdk.Context, height uint64) (claims []types.Claim) { + sessionHeightStoreIndex := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimSessionEndHeightPrefix)) + + heightBz := make([]byte, 8) + binary.BigEndian.PutUint64(heightBz, height) + + iterator := sdk.KVStorePrefixIterator(sessionHeightStoreIndex, heightBz) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + primaryKey := iterator.Value() + claim, claimFound := k.getClaimByPrimaryKey(ctx, primaryKey) + if claimFound { + claims = append(claims, claim) + } + } + + return claims +} + +// GetClaimsByAddress returns all claims matching the given session id +func (k Keeper) GetClaimsBySession(ctx sdk.Context, sessionId string) (claims []types.Claim) { + sessionIdStoreIndex := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimPrimaryKeyPrefix)) + + iterator := sdk.KVStorePrefixIterator(sessionIdStoreIndex, []byte(sessionId)) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var val types.Claim + k.cdc.MustUnmarshal(iterator.Value(), &val) + claims = append(claims, val) + } + + return claims +} + +// getClaimByPrimaryKey is a helper that retrieves, if exists, the Claim associated with the key provided +func (k Keeper) getClaimByPrimaryKey(ctx sdk.Context, primaryKey []byte) (val types.Claim, found bool) { + primaryStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimPrimaryKeyPrefix)) + b := primaryStore.Get(primaryKey) + if b == nil { + return val, false + } + k.cdc.MustUnmarshal(b, &val) + return val, true +} diff --git a/x/supplier/keeper/claim_test.go b/x/supplier/keeper/claim_test.go index dc80a9938..c60403a45 100644 --- a/x/supplier/keeper/claim_test.go +++ b/x/supplier/keeper/claim_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "fmt" "strconv" "testing" @@ -9,6 +10,7 @@ import ( keepertest "github.com/pokt-network/poktroll/testutil/keeper" "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" "github.com/pokt-network/poktroll/x/supplier/keeper" "github.com/pokt-network/poktroll/x/supplier/types" ) @@ -17,48 +19,92 @@ import ( var _ = strconv.IntSize func createNClaims(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.Claim { - items := make([]types.Claim, n) - for i := range items { - items[i].Index = strconv.Itoa(i) - - keeper.SetClaim(ctx, items[i]) + claims := make([]types.Claim, n) + for i := range claims { + claims[i].SupplierAddress = sample.AccAddress() + claims[i].SessionId = fmt.Sprintf("session-%d", i) + claims[i].SessionEndBlockHeight = uint64(i) + claims[i].RootHash = []byte(fmt.Sprintf("rootHash-%d", i)) + keeper.InsertClaim(ctx, claims[i]) } - return items + return claims } -func TestClaimGet(t *testing.T) { +func TestClaim_Get(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) - items := createNClaims(keeper, ctx, 10) - for _, item := range items { - rst, found := keeper.GetClaim(ctx, - item.Index, + claims := createNClaims(keeper, ctx, 10) + for _, claim := range claims { + foundClaim, isClaimFound := keeper.GetClaim(ctx, + claim.SessionId, + claim.SupplierAddress, ) - require.True(t, found) + require.True(t, isClaimFound) require.Equal(t, - nullify.Fill(&item), - nullify.Fill(&rst), + nullify.Fill(&claim), + nullify.Fill(&foundClaim), ) } } -func TestClaimRemove(t *testing.T) { +func TestClaim_Remove(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) - items := createNClaims(keeper, ctx, 10) - for _, item := range items { + claims := createNClaims(keeper, ctx, 10) + for _, claim := range claims { keeper.RemoveClaim(ctx, - item.Index, + claim.SessionId, + claim.SupplierAddress, ) - _, found := keeper.GetClaim(ctx, - item.Index, + _, isClaimFound := keeper.GetClaim(ctx, + claim.SessionId, + claim.SupplierAddress, ) - require.False(t, found) + require.False(t, isClaimFound) } } -func TestGetAllClaims(t *testing.T) { +func TestClaim_GetAll(t *testing.T) { + keeper, ctx := keepertest.SupplierKeeper(t) + claims := createNClaims(keeper, ctx, 10) + + // Get all the claims and check if they match + allFoundClaims := keeper.GetAllClaims(ctx) + require.ElementsMatch(t, + nullify.Fill(claims), + nullify.Fill(allFoundClaims), + ) +} + +func TestClaim_GetAll_ByAddress(t *testing.T) { + keeper, ctx := keepertest.SupplierKeeper(t) + claims := createNClaims(keeper, ctx, 10) + + // Get all claims for a given address + allFoundClaimsByAddress := keeper.GetClaimsByAddress(ctx, claims[3].SupplierAddress) + require.ElementsMatch(t, + nullify.Fill([]types.Claim{claims[3]}), + nullify.Fill(allFoundClaimsByAddress), + ) +} + +func TestClaim_GetAll_ByHeight(t *testing.T) { + keeper, ctx := keepertest.SupplierKeeper(t) + claims := createNClaims(keeper, ctx, 10) + + // Get all claims for a given ending session block height + allFoundClaimsEndingAtHeight := keeper.GetClaimsByHeight(ctx, claims[6].SessionEndBlockHeight) + require.ElementsMatch(t, + nullify.Fill([]types.Claim{claims[6]}), + nullify.Fill(allFoundClaimsEndingAtHeight), + ) +} + +func TestClaim_GetAll_BySession(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) - items := createNClaims(keeper, ctx, 10) + claims := createNClaims(keeper, ctx, 10) + + // Get all claims for a given ending session block height + allFoundClaimsForSession := keeper.GetClaimsBySession(ctx, claims[8].SessionId) require.ElementsMatch(t, - nullify.Fill(items), - nullify.Fill(keeper.GetAllClaims(ctx)), + nullify.Fill([]types.Claim{claims[8]}), + nullify.Fill(allFoundClaimsForSession), ) } diff --git a/x/supplier/keeper/msg_server_create_claim.go b/x/supplier/keeper/msg_server_create_claim.go index 752da7343..7a7722294 100644 --- a/x/supplier/keeper/msg_server_create_claim.go +++ b/x/supplier/keeper/msg_server_create_claim.go @@ -10,13 +10,25 @@ import ( func (k msgServer) CreateClaim(goCtx context.Context, msg *types.MsgCreateClaim) (*types.MsgCreateClaimResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + logger := k.Logger(ctx).With("method", "CreateClaim") if err := msg.ValidateBasic(); err != nil { return nil, err } + claim := types.Claim{ + SupplierAddress: msg.SupplierAddress, + SessionId: msg.SessionHeader.SessionId, + SessionEndBlockHeight: uint64(msg.SessionHeader.SessionEndBlockHeight), + RootHash: msg.RootHash, + } + k.Keeper.InsertClaim(ctx, claim) + + logger.Info("created claim for supplier %s at session ending height %d", claim.SupplierAddress, claim.SessionEndBlockHeight) + logger.Info("TODO_INCOMPLETE: Handling actual claim business logic %s", claim.SessionId) + /* - INCOMPLETE: Handling the message + TODO_INCOMPLETE: Handling the message ## Validation diff --git a/x/supplier/keeper/query_claim.go b/x/supplier/keeper/query_claim.go index 6f2021f14..7009d3bab 100644 --- a/x/supplier/keeper/query_claim.go +++ b/x/supplier/keeper/query_claim.go @@ -2,6 +2,8 @@ package keeper import ( "context" + "encoding/binary" + "fmt" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" @@ -17,19 +19,55 @@ func (k Keeper) AllClaims(goCtx context.Context, req *types.QueryAllClaimsReques return nil, status.Error(codes.InvalidArgument, "invalid request") } - var claims []types.Claim ctx := sdk.UnwrapSDKContext(goCtx) - store := ctx.KVStore(k.storeKey) - claimStore := prefix.NewStore(store, types.KeyPrefix(types.ClaimKeyPrefix)) + // isCustomIndex is used to determined if we'll be using the store that points + // to the actual Claim values, or a secondary index that points to the primary keys. + var isCustomIndex bool + var keyPrefix []byte + switch filter := req.Filter.(type) { + case *types.QueryAllClaimsRequest_SupplierAddress: + isCustomIndex = true + keyPrefix = types.KeyPrefix(types.ClaimSupplierAddressPrefix) + keyPrefix = append(keyPrefix, []byte(filter.SupplierAddress)...) + + case *types.QueryAllClaimsRequest_SessionEndHeight: + isCustomIndex = true + heightBz := make([]byte, 8) + binary.BigEndian.PutUint64(heightBz, filter.SessionEndHeight) + + keyPrefix = types.KeyPrefix(types.ClaimSessionEndHeightPrefix) + keyPrefix = append(keyPrefix, heightBz...) + + case *types.QueryAllClaimsRequest_SessionId: + isCustomIndex = false + keyPrefix = types.KeyPrefix(types.ClaimPrimaryKeyPrefix) + keyPrefix = append(keyPrefix, []byte(filter.SessionId)...) + + default: + isCustomIndex = false + keyPrefix = types.KeyPrefix(types.ClaimPrimaryKeyPrefix) + } + claimStore := prefix.NewStore(store, keyPrefix) + + var claims []types.Claim pageRes, err := query.Paginate(claimStore, req.Pagination, func(key []byte, value []byte) error { - var claim types.Claim - if err := k.cdc.Unmarshal(value, &claim); err != nil { - return err + if isCustomIndex { + // We retrieve the primaryKey, and need to query the actual Claim before decoding it. + claim, claimFound := k.getClaimByPrimaryKey(ctx, value) + if claimFound { + claims = append(claims, claim) + } + } else { + // The value is an encoded Claim. + var claim types.Claim + if err := k.cdc.Unmarshal(value, &claim); err != nil { + return err + } + claims = append(claims, claim) } - claims = append(claims, claim) return nil }) @@ -44,14 +82,20 @@ func (k Keeper) Claim(goCtx context.Context, req *types.QueryGetClaimRequest) (* if req == nil { return nil, status.Error(codes.InvalidArgument, "invalid request") } + + if err := req.ValidateBasic(); err != nil { + return nil, err + } + ctx := sdk.UnwrapSDKContext(goCtx) val, found := k.GetClaim( ctx, - req.Index, + req.SessionId, + req.SupplierAddress, ) if !found { - return nil, status.Error(codes.NotFound, "not found") + return nil, status.Error(codes.NotFound, fmt.Sprintf("claim not found for session %s and supplier %s", req.SessionId, req.SupplierAddress)) } return &types.QueryGetClaimResponse{Claim: val}, nil diff --git a/x/supplier/keeper/query_claim_test.go b/x/supplier/keeper/query_claim_test.go index 0d403a104..dcae244e1 100644 --- a/x/supplier/keeper/query_claim_test.go +++ b/x/supplier/keeper/query_claim_test.go @@ -1,7 +1,6 @@ package keeper_test import ( - "strconv" "testing" sdk "github.com/cosmos/cosmos-sdk/types" @@ -12,50 +11,88 @@ import ( keepertest "github.com/pokt-network/poktroll/testutil/keeper" "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" "github.com/pokt-network/poktroll/x/supplier/types" ) -func TestClaimQuerySingle(t *testing.T) { +func TestClaim_QuerySingle(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) wctx := sdk.WrapSDKContext(ctx) - msgs := createNClaims(keeper, ctx, 2) + claims := createNClaims(keeper, ctx, 2) tests := []struct { - desc string - request *types.QueryGetClaimRequest + desc string + + request *types.QueryGetClaimRequest + response *types.QueryGetClaimResponse err error }{ { - desc: "First", + desc: "First Claim", + request: &types.QueryGetClaimRequest{ - Index: msgs[0].Index, + SessionId: claims[0].SessionId, + SupplierAddress: claims[0].SupplierAddress, }, - response: &types.QueryGetClaimResponse{Claim: msgs[0]}, + + response: &types.QueryGetClaimResponse{Claim: claims[0]}, + err: nil, }, { - desc: "Second", + desc: "Second Claim", + + request: &types.QueryGetClaimRequest{ + SessionId: claims[1].SessionId, + SupplierAddress: claims[1].SupplierAddress, + }, + + response: &types.QueryGetClaimResponse{Claim: claims[1]}, + err: nil, + }, + { + desc: "Claim Not Found - Random SessionId", + + request: &types.QueryGetClaimRequest{ + SessionId: "not a real session id", + SupplierAddress: claims[0].SupplierAddress, + }, + + err: status.Error(codes.NotFound, "claim not found"), + }, + { + desc: "Claim Not Found - Random Supplier Address", + request: &types.QueryGetClaimRequest{ - Index: msgs[1].Index, + SessionId: claims[0].SessionId, + SupplierAddress: sample.AccAddress(), }, - response: &types.QueryGetClaimResponse{Claim: msgs[1]}, + + err: status.Error(codes.NotFound, "claim not found"), }, { - desc: "KeyNotFound", + desc: "InvalidRequest - Missing SessionId", request: &types.QueryGetClaimRequest{ - Index: strconv.Itoa(100000), + // SessionId: Intentionally Omitted + SupplierAddress: claims[0].SupplierAddress, }, - err: status.Error(codes.NotFound, "not found"), + + err: types.ErrSupplierInvalidSessionId, }, { - desc: "InvalidRequest", - err: status.Error(codes.InvalidArgument, "invalid request"), + desc: "InvalidRequest - Missing SupplierAddress", + request: &types.QueryGetClaimRequest{ + SessionId: claims[0].SessionId, + // SupplierAddress: Intentionally Omitted, + }, + + err: types.ErrSupplierInvalidAddress, }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { response, err := keeper.Claim(wctx, tc.request) if tc.err != nil { - require.ErrorIs(t, err, tc.err) + require.ErrorContains(t, err, tc.err.Error()) } else { require.NoError(t, err) require.Equal(t, @@ -67,10 +104,10 @@ func TestClaimQuerySingle(t *testing.T) { } } -func TestClaimQueryPaginated(t *testing.T) { +func TestClaim_QueryPaginated(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) wctx := sdk.WrapSDKContext(ctx) - msgs := createNClaims(keeper, ctx, 5) + claims := createNClaims(keeper, ctx, 10) request := func(next []byte, offset, limit uint64, total bool) *types.QueryAllClaimsRequest { return &types.QueryAllClaimsRequest{ @@ -84,12 +121,12 @@ func TestClaimQueryPaginated(t *testing.T) { } t.Run("ByOffset", func(t *testing.T) { step := 2 - for i := 0; i < len(msgs); i += step { + for i := 0; i < len(claims); i += step { resp, err := keeper.AllClaims(wctx, request(nil, uint64(i), uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.Claim), step) require.Subset(t, - nullify.Fill(msgs), + nullify.Fill(claims), nullify.Fill(resp.Claim), ) } @@ -97,12 +134,12 @@ func TestClaimQueryPaginated(t *testing.T) { t.Run("ByKey", func(t *testing.T) { step := 2 var next []byte - for i := 0; i < len(msgs); i += step { + for i := 0; i < len(claims); i += step { resp, err := keeper.AllClaims(wctx, request(next, 0, uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.Claim), step) require.Subset(t, - nullify.Fill(msgs), + nullify.Fill(claims), nullify.Fill(resp.Claim), ) next = resp.Pagination.NextKey @@ -111,9 +148,9 @@ func TestClaimQueryPaginated(t *testing.T) { t.Run("Total", func(t *testing.T) { resp, err := keeper.AllClaims(wctx, request(nil, 0, 0, true)) require.NoError(t, err) - require.Equal(t, len(msgs), int(resp.Pagination.Total)) + require.Equal(t, len(claims), int(resp.Pagination.Total)) require.ElementsMatch(t, - nullify.Fill(msgs), + nullify.Fill(claims), nullify.Fill(resp.Claim), ) }) @@ -121,4 +158,34 @@ func TestClaimQueryPaginated(t *testing.T) { _, err := keeper.AllClaims(wctx, nil) require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "invalid request")) }) + + t.Run("BySupplierAddress", func(t *testing.T) { + req := request(nil, 0, 0, true) + req.Filter = &types.QueryAllClaimsRequest_SupplierAddress{ + SupplierAddress: claims[0].SupplierAddress, + } + resp, err := keeper.AllClaims(wctx, req) + require.NoError(t, err) + require.Equal(t, 1, int(resp.Pagination.Total)) + }) + + t.Run("BySessionId", func(t *testing.T) { + req := request(nil, 0, 0, true) + req.Filter = &types.QueryAllClaimsRequest_SessionId{ + SessionId: claims[0].SessionId, + } + resp, err := keeper.AllClaims(wctx, req) + require.NoError(t, err) + require.Equal(t, 1, int(resp.Pagination.Total)) + }) + + t.Run("BySessionEndHeight", func(t *testing.T) { + req := request(nil, 0, 0, true) + req.Filter = &types.QueryAllClaimsRequest_SessionEndHeight{ + SessionEndHeight: claims[0].SessionEndBlockHeight, + } + resp, err := keeper.AllClaims(wctx, req) + require.NoError(t, err) + require.Equal(t, 1, int(resp.Pagination.Total)) + }) } diff --git a/x/supplier/types/errors.go b/x/supplier/types/errors.go index bc5a32be9..c175461ed 100644 --- a/x/supplier/types/errors.go +++ b/x/supplier/types/errors.go @@ -17,4 +17,5 @@ var ( ErrSupplierInvalidSessionId = sdkerrors.Register(ModuleName, 7, "invalid session ID") ErrSupplierInvalidService = sdkerrors.Register(ModuleName, 8, "invalid service in supplier") ErrSupplierInvalidClaimRootHash = sdkerrors.Register(ModuleName, 9, "invalid root hash") + ErrSupplierInvalidSessionEndHeight = sdkerrors.Register(ModuleName, 10, "invalid session ending height") ) diff --git a/x/supplier/types/genesis.go b/x/supplier/types/genesis.go index 521bfdc0d..b68751dcd 100644 --- a/x/supplier/types/genesis.go +++ b/x/supplier/types/genesis.go @@ -17,7 +17,6 @@ const DefaultIndex uint64 = 1 func DefaultGenesis() *GenesisState { return &GenesisState{ SupplierList: []sharedtypes.Supplier{}, - ClaimList: []Claim{}, // this line is used by starport scaffolding # genesis/types/default Params: DefaultParams(), } @@ -63,16 +62,6 @@ func (gs GenesisState) Validate() error { } } - // Check for duplicated index in claim - claimIndexMap := make(map[string]struct{}) - - for _, elem := range gs.ClaimList { - index := string(ClaimKey(elem.Index)) - if _, ok := claimIndexMap[index]; ok { - return fmt.Errorf("duplicated index for claim") - } - claimIndexMap[index] = struct{}{} - } // this line is used by starport scaffolding # genesis/types/validate return gs.Params.Validate() diff --git a/x/supplier/types/genesis_test.go b/x/supplier/types/genesis_test.go index 6a0c11190..5625fa435 100644 --- a/x/supplier/types/genesis_test.go +++ b/x/supplier/types/genesis_test.go @@ -70,14 +70,6 @@ func TestGenesisState_Validate(t *testing.T) { Services: serviceList2, }, }, - ClaimList: []types.Claim{ - { - Index: "0", - }, - { - Index: "1", - }, - }, // this line is used by starport scaffolding # types/genesis/validField }, valid: true, @@ -306,20 +298,6 @@ func TestGenesisState_Validate(t *testing.T) { }, valid: false, }, - { - desc: "duplicated claim", - genState: &types.GenesisState{ - ClaimList: []types.Claim{ - { - Index: "0", - }, - { - Index: "0", - }, - }, - }, - valid: false, - }, // this line is used by starport scaffolding # types/genesis/testcase } for _, tc := range tests { diff --git a/x/supplier/types/key_claim.go b/x/supplier/types/key_claim.go index e57164b57..03186bf15 100644 --- a/x/supplier/types/key_claim.go +++ b/x/supplier/types/key_claim.go @@ -1,22 +1,59 @@ package types -import "encoding/binary" +import ( + "encoding/binary" +) var _ binary.ByteOrder const ( - // ClaimKeyPrefix is the prefix to retrieve all Claim - ClaimKeyPrefix = "Claim/value/" + + // ClaimPrimaryKeyPrefix is the prefix to retrieve the entire Claim object (the primary store) + ClaimPrimaryKeyPrefix = "Claim/value/" + + // ClaimSupplierAddressPrefix is the key to retrieve a Claim's Primary Key from the Address index + ClaimSupplierAddressPrefix = "Claim/address/" + + // ClaimSessionEndHeightPrefix is the key to retrieve a Claim's Primary Key from the Height index + ClaimSessionEndHeightPrefix = "Claim/height/" ) -// ClaimKey returns the store key to retrieve a Claim from the index fields -func ClaimKey( - index string, -) []byte { +// ClaimPrimaryKey returns the primary store key to retrieve a Claim by creating a composite key of the sessionId and supplierAddr +func ClaimPrimaryKey(sessionId, supplierAddr string) []byte { + var key []byte + + // We are guaranteed uniqueness of the primary key if it's a composite of the (sessionId, supplierAddr) + // because every supplier can only have one claim per session. + key = append(key, []byte(sessionId)...) + key = append(key, []byte("/")...) + key = append(key, []byte(supplierAddr)...) + key = append(key, []byte("/")...) + + return key +} + +// ClaimSupplierAddressKey returns the address key to iterate through claims given a supplier Address +func ClaimSupplierAddressKey(supplierAddr string, primaryKey []byte) []byte { var key []byte - indexBytes := []byte(index) - key = append(key, indexBytes...) + key = append(key, []byte(supplierAddr)...) + key = append(key, []byte("/")...) + key = append(key, primaryKey...) + key = append(key, []byte("/")...) + + return key +} + +// ClaimSupplierAddressKey returns the address key to iterate through claims given a supplier Address +func ClaimSupplierEndSessionHeightKey(sessionEndHeight uint64, primaryKey []byte) []byte { + var key []byte + + heightBz := make([]byte, 8) + binary.BigEndian.PutUint64(heightBz, sessionEndHeight) + + key = append(key, []byte(heightBz)...) + key = append(key, []byte("/")...) + key = append(key, primaryKey...) key = append(key, []byte("/")...) return key diff --git a/x/supplier/types/message_create_claim.go b/x/supplier/types/message_create_claim.go index 0e702c956..0b20c27ec 100644 --- a/x/supplier/types/message_create_claim.go +++ b/x/supplier/types/message_create_claim.go @@ -12,7 +12,11 @@ const TypeMsgCreateClaim = "create_claim" var _ sdk.Msg = (*MsgCreateClaim)(nil) -func NewMsgCreateClaim(supplierAddress string, sessionHeader *sessiontypes.SessionHeader, rootHash []byte) *MsgCreateClaim { +func NewMsgCreateClaim( + supplierAddress string, + sessionHeader *sessiontypes.SessionHeader, + rootHash []byte, +) *MsgCreateClaim { return &MsgCreateClaim{ SupplierAddress: supplierAddress, SessionHeader: sessionHeader, diff --git a/x/supplier/types/query_get_claim.go b/x/supplier/types/query_get_claim.go new file mode 100644 index 000000000..0b7e1623f --- /dev/null +++ b/x/supplier/types/query_get_claim.go @@ -0,0 +1,50 @@ +package types + +import ( + fmt "fmt" + + sdkerrors "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// NOTE: Please note that these messages are not of type `sdk.Msg`, and are therefore not a message/request +// that will be signable or invoke a state transition. However, following a similar `ValidateBasic` pattern +// allows us to localize & reuse validation logic. + +// ValidateBasic performs basic (non-state-dependant) validation on a QueryGetClaimRequest. +func (query *QueryGetClaimRequest) ValidateBasic() error { + // Validate the supplier address + if _, err := sdk.AccAddressFromBech32(query.SupplierAddress); err != nil { + return sdkerrors.Wrapf(ErrSupplierInvalidAddress, "invalid supplier address for claim being retrieved %s; (%v)", query.SupplierAddress, err) + } + + // TODO_TECHDEBT: Validate the session ID once we have a deterministic way to generate it + if query.SessionId == "" { + return sdkerrors.Wrapf(ErrSupplierInvalidSessionId, "invalid session ID for claim being retrieved %s", query.SessionId) + } + return nil +} + +// ValidateBasic performs basic (non-state-dependant) validation on a QueryAllClaimsRequest. +func (query *QueryAllClaimsRequest) ValidateBasic() error { + switch filter := query.Filter.(type) { + case *QueryAllClaimsRequest_SupplierAddress: + if _, err := sdk.AccAddressFromBech32(filter.SupplierAddress); err != nil { + return sdkerrors.Wrapf(ErrSupplierInvalidAddress, "invalid supplier address for claims being retrieved %s; (%v)", filter.SupplierAddress, err) + } + + case *QueryAllClaimsRequest_SessionId: + // TODO_TECHDEBT: Validate the session ID once we have a deterministic way to generate it + fmt.Println("TODO: SessionID check is currently a noop: ", filter.SessionId) + + case *QueryAllClaimsRequest_SessionEndHeight: + if filter.SessionEndHeight < 0 { + return sdkerrors.Wrapf(ErrSupplierInvalidSessionEndHeight, "invalid session end height for claims being retrieved %d", filter.SessionEndHeight) + } + + default: + // No filter is set + fmt.Println("No specific filter set") + } + return nil +}