diff --git a/Makefile b/Makefile index be2e63282..3b68fff2a 100644 --- a/Makefile +++ b/Makefile @@ -298,6 +298,22 @@ app2_delegate_gateway2: ## Delegate trust to gateway2 app3_delegate_gateway3: ## Delegate trust to gateway3 APP=app3 GATEWAY_ADDR=pokt1zhmkkd0rh788mc9prfq0m2h88t9ge0j83gnxya make app_delegate +.PHONY: app_undelegate +app_undelegate: ## Undelegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked + pocketd --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + +.PHONY: app1_undelegate_gateway1 +app1_undelegate_gateway1: ## Undelegate trust to gateway1 + APP=app1 GATEWAY_ADDR=pokt15vzxjqklzjtlz7lahe8z2dfe9nm5vxwwmscne4 make app_undelegate + +.PHONY: app2_undelegate_gateway2 +app2_undelegate_gateway2: ## Undelegate trust to gateway2 + APP=app2 GATEWAY_ADDR=pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz make app_undelegate + +.PHONY: app3_undelegate_gateway3 +app3_undelegate_gateway3: ## Undelegate trust to gateway3 + APP=app3 GATEWAY_ADDR=pokt1zhmkkd0rh788mc9prfq0m2h88t9ge0j83gnxya make app_undelegate + ################# ### Suppliers ### ################# diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index d6f879b53..0e4c63907 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46737,7 +46737,7 @@ paths: type: object properties: max_delegated_gateways: - type: integer + type: string format: int64 title: >- The maximum number of gateways an application can delegate @@ -47973,174 +47973,7 @@ paths: properties: '@type': type: string - description: >- - A URL/resource name that uniquely identifies the type of - the serialized - - protocol buffer message. This string must contain at - least - - one "/" character. The last segment of the URL's path - must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in - a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary - all types that they - - expect it to use in the context of Any. However, for - URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally - set up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based - on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in - the official - - protobuf release, and it is not used for type URLs - beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer - message along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values - in the form - - of utility functions or additional generated methods of the - Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by - default use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the - last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield - type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with - an - - additional field `@type` which contains the type URL. - Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom - JSON - - representation, that representation will be embedded adding - a field - - `value` which holds the custom JSON in addition to the - `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } tags: - Query /pocket/supplier/supplier: @@ -48316,174 +48149,7 @@ paths: properties: '@type': type: string - description: >- - A URL/resource name that uniquely identifies the type of - the serialized - - protocol buffer message. This string must contain at - least - - one "/" character. The last segment of the URL's path - must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in - a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary - all types that they - - expect it to use in the context of Any. However, for - URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally - set up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based - on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in - the official - - protobuf release, and it is not used for type URLs - beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer - message along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values - in the form - - of utility functions or additional generated methods of the - Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by - default use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the - last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield - type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with - an - - additional field `@type` which contains the type URL. - Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom - JSON - - representation, that representation will be embedded adding - a field - - `value` which holds the custom JSON in addition to the - `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } parameters: - name: pagination.key description: |- @@ -48687,174 +48353,7 @@ paths: properties: '@type': type: string - description: >- - A URL/resource name that uniquely identifies the type of - the serialized - - protocol buffer message. This string must contain at - least - - one "/" character. The last segment of the URL's path - must represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in - a canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary - all types that they - - expect it to use in the context of Any. However, for - URLs which use the - - scheme `http`, `https`, or no scheme, one can optionally - set up a type - - server that maps type URLs to message definitions as - follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based - on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in - the official - - protobuf release, and it is not used for type URLs - beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) - might be - - used with implementation specific semantics. additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer - message along with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values - in the form - - of utility functions or additional generated methods of the - Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by - default use - - 'type.googleapis.com/full.type.name' as the type URL and the - unpack - - methods only use the fully qualified type name after the - last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield - type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with - an - - additional field `@type` which contains the type URL. - Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom - JSON - - representation, that representation will be embedded adding - a field - - `value` which holds the custom JSON in addition to the - `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } parameters: - name: address in: path @@ -77840,7 +77339,7 @@ definitions: type: object properties: max_delegated_gateways: - type: integer + type: string format: int64 title: The maximum number of gateways an application can delegate trust to description: Params defines the parameters for the module. @@ -78018,7 +77517,7 @@ definitions: type: object properties: max_delegated_gateways: - type: integer + type: string format: int64 title: >- The maximum number of gateways an application can delegate trust diff --git a/go.mod b/go.mod index 7e42ae4a4..12d4e67a9 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( cosmossdk.io/math v1.0.1 github.com/cometbft/cometbft v0.37.2 github.com/cometbft/cometbft-db v0.8.0 + github.com/cosmos/cosmos-proto v1.0.0-beta.2 github.com/cosmos/cosmos-sdk v0.47.3 github.com/cosmos/gogoproto v1.4.10 github.com/cosmos/ibc-go/v7 v7.1.0 @@ -26,6 +27,7 @@ require ( go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.12.0 golang.org/x/sync v0.3.0 + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 google.golang.org/grpc v1.56.1 gopkg.in/yaml.v2 v2.4.0 ) @@ -69,7 +71,6 @@ require ( github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect - github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.0 // indirect @@ -265,7 +266,6 @@ require ( gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/api v0.122.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/proto/pocket/application/tx.proto b/proto/pocket/application/tx.proto index 0b49ce706..e9f50a048 100644 --- a/proto/pocket/application/tx.proto +++ b/proto/pocket/application/tx.proto @@ -42,7 +42,9 @@ message MsgDelegateToGateway { message MsgDelegateToGatewayResponse {} message MsgUndelegateFromGateway { - string address = 1; + option (cosmos.msg.v1.signer) = "appAddress"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries + string appAddress = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding + string gatewayAddress = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the gateway the application wants to undelegate from using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding } message MsgUndelegateFromGatewayResponse {} diff --git a/x/application/client/cli/tx_undelegate_from_gateway.go b/x/application/client/cli/tx_undelegate_from_gateway.go index 6c6bdf2a1..0b31de3d2 100644 --- a/x/application/client/cli/tx_undelegate_from_gateway.go +++ b/x/application/client/cli/tx_undelegate_from_gateway.go @@ -3,22 +3,29 @@ package cli import ( "strconv" + "pocket/x/application/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/application/types" ) var _ = strconv.Itoa(0) func CmdUndelegateFromGateway() *cobra.Command { cmd := &cobra.Command{ - Use: "undelegate-from-gateway", - Short: "Broadcast message undelegate-from-gateway", - Args: cobra.ExactArgs(0), + Use: "undelegate-from-gateway [gateway address]", + Short: "Undelegate an application from a gateway", + Long: `Undelegate an application from the gateway with the provided address. This is a broadcast operation +that removes the authority from the gateway specified to sign relays requests for the application, disallowing the gateway +act on the behalf of the application during a session. + +Example: +$ pocketd --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { - + gatewayAddress := args[0] clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err @@ -26,10 +33,12 @@ func CmdUndelegateFromGateway() *cobra.Command { msg := types.NewMsgUndelegateFromGateway( clientCtx.GetFromAddress().String(), + gatewayAddress, ) if err := msg.ValidateBasic(); err != nil { return err } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } diff --git a/x/application/client/cli/tx_undelegate_from_gateway_test.go b/x/application/client/cli/tx_undelegate_from_gateway_test.go new file mode 100644 index 000000000..93e9382ff --- /dev/null +++ b/x/application/client/cli/tx_undelegate_from_gateway_test.go @@ -0,0 +1,117 @@ +package cli_test + +import ( + "fmt" + "testing" + + sdkerrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/testutil" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/status" + + "pocket/testutil/network" + "pocket/x/application/client/cli" + "pocket/x/application/types" +) + +func TestCLI_UndelegateFromGateway(t *testing.T) { + net, _ := networkWithApplicationObjects(t, 2) + val := net.Validators[0] + ctx := val.ClientCtx + + // Create a keyring and add an account for the application to be delegated + // and the gateway to be delegated to + kr := ctx.Keyring + accounts := testutil.CreateKeyringAccounts(t, kr, 2) + appAccount := accounts[0] + gatewayAccount := accounts[1] + + // Update the context with the new keyring + ctx = ctx.WithKeyring(kr) + + // Common args used for all requests + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(net.Config.BondDenom, sdkmath.NewInt(10))).String()), + } + + tests := []struct { + desc string + appAddress string + gatewayAddress string + err *sdkerrors.Error + }{ + { + desc: "undelegate from gateway: valid", + appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + }, + { + desc: "invalid - missing app address", + // appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - invalid app address", + appAddress: "invalid address", + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - missing gateway address", + appAddress: appAccount.Address.String(), + // gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidGatewayAddress, + }, + { + desc: "invalid - invalid gateway address", + appAddress: appAccount.Address.String(), + gatewayAddress: "invalid address", + err: types.ErrAppInvalidGatewayAddress, + }, + } + + // Initialize the App and Gateway Accounts by sending it some funds from the validator account that is part of genesis + network.InitAccount(t, net, appAccount.Address) + network.InitAccount(t, net, gatewayAccount.Address) + + // Run the tests + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + // Wait for a new block to be committed + require.NoError(t, net.WaitForNextBlock()) + + // Prepare the arguments for the CLI command + args := []string{ + tt.gatewayAddress, + fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.appAddress), + } + args = append(args, commonArgs...) + + // Execute the command + undelegateOutput, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdUndelegateFromGateway(), args) + + // Validate the error if one is expected + if tt.err != nil { + stat, ok := status.FromError(tt.err) + require.True(t, ok) + require.Contains(t, stat.Message(), tt.err.Error()) + return + } + require.NoError(t, err) + + // Check the response + var resp sdk.TxResponse + require.NoError(t, net.Config.Codec.UnmarshalJSON(undelegateOutput.Bytes(), &resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.TxHash) + require.Equal(t, uint32(0), resp.Code) + }) + } +} diff --git a/x/application/keeper/msg_server_undelegate_from_gateway.go b/x/application/keeper/msg_server_undelegate_from_gateway.go index f4b1d45d9..39889063a 100644 --- a/x/application/keeper/msg_server_undelegate_from_gateway.go +++ b/x/application/keeper/msg_server_undelegate_from_gateway.go @@ -3,6 +3,7 @@ package keeper import ( "context" + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "pocket/x/application/types" @@ -11,12 +12,40 @@ import ( func (k msgServer) UndelegateFromGateway(goCtx context.Context, msg *types.MsgUndelegateFromGateway) (*types.MsgUndelegateFromGatewayResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + logger := k.Logger(ctx).With("method", "UndelegateFromGateway") + logger.Info("About to undelegate application from gateway with msg: %v", msg) + if err := msg.ValidateBasic(); err != nil { + logger.Error("Undelegation Message failed basic validation: %v", err) return nil, err } - // TODO: Handling the message - _ = ctx + // Retrieve the application from the store + app, found := k.GetApplication(ctx, msg.AppAddress) + if !found { + logger.Info("Application not found with address [%s]", msg.AppAddress) + return nil, sdkerrors.Wrapf(types.ErrAppNotFound, "application not found with address: %s", msg.AppAddress) + } + logger.Info("Application found with address [%s]", msg.AppAddress) + + // Check if the application is already delegated to the gateway + foundIdx := -1 + for i, gatewayAddr := range app.DelegateeGatewayAddresses { + if gatewayAddr == msg.GatewayAddress { + foundIdx = i + } + } + if foundIdx == -1 { + logger.Info("Application not delegated to gateway with address [%s]", msg.GatewayAddress) + return nil, sdkerrors.Wrapf(types.ErrAppNotDelegated, "application not delegated to gateway with address: %s", msg.GatewayAddress) + } + + // Remove the gateway from the application's delegatee gateway public keys + app.DelegateeGatewayAddresses = append(app.DelegateeGatewayAddresses[:foundIdx], app.DelegateeGatewayAddresses[foundIdx+1:]...) + + // Update the application store with the new delegation + k.SetApplication(ctx, app) + logger.Info("Successfully undelegated application from gateway for app: %+v", app) return &types.MsgUndelegateFromGatewayResponse{}, nil } diff --git a/x/application/keeper/msg_server_undelegate_from_gateway_test.go b/x/application/keeper/msg_server_undelegate_from_gateway_test.go new file mode 100644 index 000000000..e9c9bf70c --- /dev/null +++ b/x/application/keeper/msg_server_undelegate_from_gateway_test.go @@ -0,0 +1,220 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + keepertest "pocket/testutil/keeper" + "pocket/testutil/sample" + "pocket/x/application/keeper" + "pocket/x/application/types" + sharedtypes "pocket/x/shared/types" +) + +func TestMsgServer_UndelegateFromGateway_SuccessfullyUndelegate(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateways + appAddr := sample.AccAddress() + gatewayAddresses := make([]string, int(k.GetParams(ctx).MaxDelegatedGateways)) + for i := 0; i < len(gatewayAddresses); i++ { + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + gatewayAddresses[i] = gatewayAddr + } + t.Cleanup(func() { + for _, gatewayAddr := range gatewayAddresses { + delete(keepertest.StakedGatewayMap, gatewayAddr) + } + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation messages and delegate the application to the gateways + for _, gatewayAddr := range gatewayAddresses { + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + } + + // Verify that the application exists + maxDelegatedGateways := k.GetParams(ctx).MaxDelegatedGateways + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, maxDelegatedGateways, int64(len(foundApp.DelegateeGatewayAddresses))) + for i, gatewayAddr := range gatewayAddresses { + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[i]) + } + + // Prepare an undelegation message + undelegateMsg := &types.MsgUndelegateFromGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddresses[3], + } + + // Undelegate the application from the gateway + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.NoError(t, err) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, maxDelegatedGateways-1, int64(len(foundApp.DelegateeGatewayAddresses))) + gatewayAddresses = append(gatewayAddresses[:3], gatewayAddresses[4:]...) + for i, gatewayAddr := range gatewayAddresses { + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[i]) + } +} + +func TestMsgServer_UndelegateFromGateway_FailNotDelegated(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateway + appAddr := sample.AccAddress() + gatewayAddr1 := sample.AccAddress() + gatewayAddr2 := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr1] = struct{}{} + keepertest.StakedGatewayMap[gatewayAddr2] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr1) + delete(keepertest.StakedGatewayMap, gatewayAddr2) + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the undelegation message + undelegateMsg := &types.MsgUndelegateFromGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr1, + } + + // Attempt to undelgate the application from the gateway + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.ErrorIs(t, err, types.ErrAppNotDelegated) + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses)) + + // Prepare a delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr2, + } + + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Ensure the failed undelegation did not affect the application + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.ErrorIs(t, err, types.ErrAppNotDelegated) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr2, foundApp.DelegateeGatewayAddresses[0]) +} + +func TestMsgServer_UndelegateFromGateway_SuccessfullyUndelegateFromUnstakedGateway(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateways + appAddr := sample.AccAddress() + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message and delegate the application to the gateway + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Verify that the application exists + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[0]) + + // Mock unstaking the gateway + delete(keepertest.StakedGatewayMap, gatewayAddr) + + // Prepare an undelegation message + undelegateMsg := &types.MsgUndelegateFromGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Undelegate the application from the gateway + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.NoError(t, err) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses)) +} diff --git a/x/application/simulation/undelegate_from_gateway.go b/x/application/simulation/undelegate_from_gateway.go index ae03b5927..c5702c0d5 100644 --- a/x/application/simulation/undelegate_from_gateway.go +++ b/x/application/simulation/undelegate_from_gateway.go @@ -3,11 +3,12 @@ package simulation import ( "math/rand" + "pocket/x/application/keeper" + "pocket/x/application/types" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/application/keeper" - "pocket/x/application/types" ) func SimulateMsgUndelegateFromGateway( @@ -17,9 +18,11 @@ func SimulateMsgUndelegateFromGateway( ) simtypes.Operation { return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - simAccount, _ := simtypes.RandomAcc(r, accs) + simAppAccount, _ := simtypes.RandomAcc(r, accs) + simGatewayAccount, _ := simtypes.RandomAcc(r, accs) msg := &types.MsgUndelegateFromGateway{ - Address: simAccount.Address.String(), + AppAddress: simAppAccount.Address.String(), + GatewayAddress: simGatewayAccount.Address.String(), } // TODO: Handling the UndelegateFromGateway simulation diff --git a/x/application/types/errors.go b/x/application/types/errors.go index 3445ec6f4..ea89e77e1 100644 --- a/x/application/types/errors.go +++ b/x/application/types/errors.go @@ -18,4 +18,5 @@ var ( ErrAppAlreadyDelegated = sdkerrors.Register(ModuleName, 9, "application already delegated to gateway") ErrAppMaxDelegatedGateways = sdkerrors.Register(ModuleName, 10, "maximum number of delegated gateways reached") ErrAppInvalidMaxDelegatedGateways = sdkerrors.Register(ModuleName, 11, "invalid MaxDelegatedGateways parameter") + ErrAppNotDelegated = sdkerrors.Register(ModuleName, 12, "application not delegated to gateway") ) diff --git a/x/application/types/message_undelegate_from_gateway.go b/x/application/types/message_undelegate_from_gateway.go index 240605383..4d74748c1 100644 --- a/x/application/types/message_undelegate_from_gateway.go +++ b/x/application/types/message_undelegate_from_gateway.go @@ -9,9 +9,10 @@ const TypeMsgUndelegateFromGateway = "undelegate_from_gateway" var _ sdk.Msg = (*MsgUndelegateFromGateway)(nil) -func NewMsgUndelegateFromGateway(address string) *MsgUndelegateFromGateway { +func NewMsgUndelegateFromGateway(appAddress, gatewayAddress string) *MsgUndelegateFromGateway { return &MsgUndelegateFromGateway{ - Address: address, + AppAddress: appAddress, + GatewayAddress: gatewayAddress, } } @@ -24,7 +25,7 @@ func (msg *MsgUndelegateFromGateway) Type() string { } func (msg *MsgUndelegateFromGateway) GetSigners() []sdk.AccAddress { - address, err := sdk.AccAddressFromBech32(msg.Address) + address, err := sdk.AccAddressFromBech32(msg.AppAddress) if err != nil { panic(err) } @@ -37,9 +38,13 @@ func (msg *MsgUndelegateFromGateway) GetSignBytes() []byte { } func (msg *MsgUndelegateFromGateway) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(msg.Address) - if err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid address address (%s)", err) + // Validate the application address + if _, err := sdk.AccAddressFromBech32(msg.AppAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidAddress, "invalid application address %s; (%v)", msg.AppAddress, err) + } + // Validate the gateway address + if _, err := sdk.AccAddressFromBech32(msg.GatewayAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidGatewayAddress, "invalid gateway address %s; (%v)", msg.GatewayAddress, err) } return nil } diff --git a/x/application/types/message_undelegate_from_gateway_test.go b/x/application/types/message_undelegate_from_gateway_test.go index 1781a887a..72919eefb 100644 --- a/x/application/types/message_undelegate_from_gateway_test.go +++ b/x/application/types/message_undelegate_from_gateway_test.go @@ -3,9 +3,9 @@ package types import ( "testing" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/stretchr/testify/require" "pocket/testutil/sample" + + "github.com/stretchr/testify/require" ) func TestMsgUndelegateFromGateway_ValidateBasic(t *testing.T) { @@ -15,15 +15,31 @@ func TestMsgUndelegateFromGateway_ValidateBasic(t *testing.T) { err error }{ { - name: "invalid address", + name: "invalid app address - no gateway address", + msg: MsgUndelegateFromGateway{ + AppAddress: "invalid_address", + // GatewayAddress: sample.AccAddress(), + }, + err: ErrAppInvalidAddress, + }, { + name: "valid app address - no gateway address", + msg: MsgUndelegateFromGateway{ + AppAddress: sample.AccAddress(), + // GatewayAddress: sample.AccAddress(), + }, + err: ErrAppInvalidGatewayAddress, + }, { + name: "valid app address - invalid gateway address", msg: MsgUndelegateFromGateway{ - Address: "invalid_address", + AppAddress: sample.AccAddress(), + GatewayAddress: "invalid_address", }, - err: sdkerrors.ErrInvalidAddress, + err: ErrAppInvalidGatewayAddress, }, { name: "valid address", msg: MsgUndelegateFromGateway{ - Address: sample.AccAddress(), + AppAddress: sample.AccAddress(), + GatewayAddress: sample.AccAddress(), }, }, }