From 762dd5a177bf8f10a60b7a8269232610ef544009 Mon Sep 17 00:00:00 2001 From: tokikuch Date: Tue, 19 Dec 2023 18:02:30 -0800 Subject: [PATCH] Allow an AppMsgStake tx to change the address of a staked app (#1585) The patch implements [PIP-35](https://forum.pokt.network/t/pip-35-introduce-a-secure-way-to-transfer-a-staked-app-to-a-new-account/4806), allowing the existing AppMsgStake transaction to change the address of a staked app. This enables us to *transfer* the existing app slot from one to a new account without unstaking. To make this operation easier, this patch also introduces a new command `app transfer`. ## Description ### Summary generated by Reviewpad on 15 Dec 23 11:53 UTC This pull request includes changes in multiple files. Here is a summary of the changes: 1. The file `appStateChanges_test.go` has changes related to application state changes and transfers. The changes include the addition of new import statements, test functions, and helper functions. 2. The file `types.go` has changes related to the `MsgStake` type. The changes involve adding a new method `IsValidTransfer` to check for a special case in the `MsgStake` type where ownership transfer is being done. 3. The file `codec.go` has changes related to the support of an "App Transfer" feature. The changes include adding a constant and a new function for checking if a certain upgrade has occurred. 4. The file `expectedKeepers.go` has changes related to the addition of a new interface `AppKeeper`. 5. The file `keeper.go` has changes that involve adding a new field to the `Keeper` struct. 6. The file `baseapp.go` has changes that include formatting changes in the package comment and modifications in the `DeliverTx` function. 7. The file `app.go` has changes related to the assignment of a field in the `app.accountKeeper` object. 8. The file `auth.go` has changes that include adding comments and conditions for non-custodial and output address editor upgrades. 9. The file `common_test.go` has changes that involve importing packages, renaming a package, and adding/modifying functions. 10. The file `appStateChanges.go` has changes related to the validation and transfer functionality of applications. 11. The file `txUtil.go` has changes that add a new function for transferring an application. 12. The file `app/cmd/cli/app.go` has changes related to the addition of a new command for transferring the ownership of a staked app. 13. The file `keeper.go` has changes that add a new method for checking if a message is for transferring ownership. 14. The file `handler.go` has changes that include import statements, function parameter modifications, and logic for transferring application ownership. These are the summaries of the changes in each file. Let me know if you have any specific questions or need further information regarding these changes. --- app/app.go | 1 + app/cmd/cli/app.go | 60 +++++++++++ app/cmd/cli/txUtil.go | 46 ++++++++ baseapp/baseapp.go | 18 ++-- codec/codec.go | 7 ++ x/apps/handler.go | 49 ++++++--- x/apps/keeper/appStateChanges.go | 103 +++++++++++++++--- x/apps/keeper/appStateChanges_test.go | 144 +++++++++++++++++++++++++- x/apps/keeper/appUtil.go | 31 ++++++ x/apps/keeper/common_test.go | 22 ++-- x/apps/types/msg.go | 28 ++++- x/auth/ante.go | 17 +++ x/auth/keeper/keeper.go | 1 + x/auth/types/expectedKeepers.go | 4 + 14 files changed, 479 insertions(+), 52 deletions(-) diff --git a/app/app.go b/app/app.go index a5d04cf98..21f4f7bbe 100644 --- a/app/app.go +++ b/app/app.go @@ -88,6 +88,7 @@ func NewPocketCoreApp(genState GenesisState, keybase keys.Keybase, tmClient clie app.nodesKeeper.PocketKeeper = app.pocketKeeper app.appsKeeper.PocketKeeper = app.pocketKeeper app.accountKeeper.POSKeeper = app.nodesKeeper + app.accountKeeper.AppKeeper = app.appsKeeper // setup module manager app.mm = module.NewManager( auth.NewAppModule(app.accountKeeper), diff --git a/app/cmd/cli/app.go b/app/cmd/cli/app.go index 355aebf3e..8c1d18ee8 100644 --- a/app/cmd/cli/app.go +++ b/app/cmd/cli/app.go @@ -19,6 +19,7 @@ func init() { rootCmd.AddCommand(appCmd) appCmd.AddCommand(appStakeCmd) appCmd.AddCommand(appUnstakeCmd) + appCmd.AddCommand(appTransferCmd) appCmd.AddCommand(createAATCmd) } @@ -116,6 +117,65 @@ Prompts the user for the account passphrase.`, }, } +var appTransferCmd = &cobra.Command{ + Use: "transfer [memo]", + Short: "Transfer the ownership of a staked app from one to another", + Long: `Submits a transaction to transfer the ownership of a staked app from + to a new account specified as without unstaking +any app. In other words, this edits the address of a staked app. To run this +command, you must have the private key of the current staked app +`, + Args: cobra.MinimumNArgs(4), + Run: func(cmd *cobra.Command, args []string) { + app.InitConfig(datadir, tmNode, persistentPeers, seeds, remoteCLIURL) + + currentAppAddr := args[0] + newAppPubKey := args[1] + networkId := args[2] + feeStr := args[3] + memo := "" + if len(args) >= 5 { + memo = args[4] + } + + fee, err := strconv.ParseInt(feeStr, 10, 64) + if err != nil { + fmt.Println("Invalid fee:", err) + return + } + + fmt.Printf("Enter passphrase to unlock %s: ", currentAppAddr) + passphrase := app.Credentials(pwd) + + rawTx, err := TransferApp( + currentAppAddr, + newAppPubKey, + passphrase, + networkId, + fee, + memo, + ) + if err != nil { + fmt.Println("Failed to build a transaction:", err) + return + } + + rawTxBytes, err := json.Marshal(rawTx) + if err != nil { + fmt.Println(err) + return + } + + resp, err := QueryRPC(SendRawTxPath, rawTxBytes) + if err != nil { + fmt.Println("Failed to submit a transaction:", err) + return + } + + fmt.Println(resp) + }, +} + var createAATCmd = &cobra.Command{ Use: "create-aat ", Short: "Creates an application authentication token", diff --git a/app/cmd/cli/txUtil.go b/app/cmd/cli/txUtil.go index c6a4eea95..1985ec309 100644 --- a/app/cmd/cli/txUtil.go +++ b/app/cmd/cli/txUtil.go @@ -397,6 +397,52 @@ func UnstakeApp(fromAddr, passphrase, chainID string, fees int64, legacyCodec bo }, nil } +func TransferApp( + currentAppAddrStr, newAppPubKeyStr, passphrase, networkId string, + fee int64, + memo string, +) (*rpc.SendRawTxParams, error) { + currentAppAddr, err := sdk.AddressFromHex(currentAppAddrStr) + if err != nil { + return nil, err + } + + newAppPubKey, err := crypto.NewPublicKey(newAppPubKeyStr) + if err != nil { + return nil, err + } + + keybase, err := app.GetKeybase() + if err != nil { + return nil, err + } + + msg := appsType.MsgStake{PubKey: newAppPubKey} + if err = msg.ValidateBasic(); err != nil { + return nil, err + } + + txBz, err := newTxBz( + app.Codec(), + &msg, + currentAppAddr, + networkId, + keybase, + passphrase, + fee, + memo, + false, + ) + if err != nil { + return nil, err + } + + return &rpc.SendRawTxParams{ + Addr: currentAppAddrStr, + RawHexBytes: hex.EncodeToString(txBz), + }, nil +} + func DAOTx(fromAddr, toAddr, passphrase string, amount sdk.BigInt, action, chainID string, fees int64, legacyCodec bool) (*rpc.SendRawTxParams, error) { fa, err := sdk.AddressFromHex(fromAddr) if err != nil { diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index e34960ae6..cab30500d 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -1,10 +1,10 @@ -///* -//Package baseapp contains data structures that provide basic data storage -//functionality and act as a bridge between the ABCI interface and the SDK -//abstractions. +// /* +// Package baseapp contains data structures that provide basic data storage +// functionality and act as a bridge between the ABCI interface and the SDK +// abstractions. // -//BaseApp has no state except the CommitMultiStore you provide upon init. -//*/ +// BaseApp has no state except the CommitMultiStore you provide upon init. +// */ package baseapp import ( @@ -12,7 +12,6 @@ import ( "fmt" "github.com/pokt-network/pocket-core/codec/types" "github.com/pokt-network/pocket-core/crypto" - types2 "github.com/pokt-network/pocket-core/x/apps/types" "github.com/pokt-network/pocket-core/x/auth" "github.com/tendermint/tendermint/evidence" "github.com/tendermint/tendermint/node" @@ -810,9 +809,8 @@ func (app *BaseApp) DeliverTx(req abci.RequestDeliverTx) (res abci.ResponseDeliv msg := tx.GetMsg() messageType = msg.Type() recipient = msg.GetRecipient() - if signerPK == nil || messageType == types2.MsgAppStakeName { - signers := msg.GetSigners() - if len(signers) >= 1 { + if signerPK == nil { + if signers := msg.GetSigners(); len(signers) >= 1 { signer = signers[0] } } else { diff --git a/codec/codec.go b/codec/codec.go index 84febd38a..79f24db70 100644 --- a/codec/codec.go +++ b/codec/codec.go @@ -59,6 +59,7 @@ const ( OutputAddressEditKey = "OEDIT" ClearUnjailedValSessionKey = "CRVAL" PerChainRTTM = "PerChainRTTM" + AppTransferKey = "AppTransfer" ) func GetCodecUpgradeHeight() int64 { @@ -287,6 +288,12 @@ func (cdc *Codec) IsAfterPerChainRTTMUpgrade(height int64) bool { TestMode <= -3 } +func (cdc *Codec) IsAfterAppTransferUpgrade(height int64) bool { + return (UpgradeFeatureMap[AppTransferKey] != 0 && + height >= UpgradeFeatureMap[AppTransferKey]) || + TestMode <= -3 +} + // IsOnNonCustodialUpgrade Note: includes the actual upgrade height func (cdc *Codec) IsOnNonCustodialUpgrade(height int64) bool { return (UpgradeFeatureMap[NonCustodialUpdateKey] != 0 && height == UpgradeFeatureMap[NonCustodialUpdateKey]) || TestMode <= -3 diff --git a/x/apps/handler.go b/x/apps/handler.go index a2925ac80..48236dd23 100644 --- a/x/apps/handler.go +++ b/x/apps/handler.go @@ -2,15 +2,16 @@ package pos import ( "fmt" + "reflect" + "github.com/pokt-network/pocket-core/crypto" sdk "github.com/pokt-network/pocket-core/types" "github.com/pokt-network/pocket-core/x/apps/keeper" "github.com/pokt-network/pocket-core/x/apps/types" - "reflect" ) func NewHandler(k keeper.Keeper) sdk.Handler { - return func(ctx sdk.Ctx, msg sdk.Msg, _ crypto.PublicKey) sdk.Result { + return func(ctx sdk.Ctx, msg sdk.Msg, signer crypto.PublicKey) sdk.Result { ctx = ctx.WithEventManager(sdk.NewEventManager()) // convert to value for switch consistency if reflect.ValueOf(msg).Kind() == reflect.Ptr { @@ -18,7 +19,7 @@ func NewHandler(k keeper.Keeper) sdk.Handler { } switch msg := msg.(type) { case types.MsgStake: - return handleStake(ctx, msg, k) + return handleStake(ctx, msg, signer, k) case types.MsgBeginUnstake: return handleMsgBeginUnstake(ctx, msg, k) case types.MsgUnjail: @@ -30,25 +31,41 @@ func NewHandler(k keeper.Keeper) sdk.Handler { } } -func handleStake(ctx sdk.Ctx, msg types.MsgStake, k keeper.Keeper) sdk.Result { +func handleStake( + ctx sdk.Ctx, + msg types.MsgStake, + signer crypto.PublicKey, + k keeper.Keeper, +) sdk.Result { pk := msg.PubKey addr := pk.Address() ctx.Logger().Info("Begin Staking App Message received from " + sdk.Address(pk.Address()).String()) // create application object using the message fields application := types.NewApplication(sdk.Address(addr), pk, msg.Chains, sdk.ZeroInt()) ctx.Logger().Info("Validate App Can Stake " + sdk.Address(addr).String()) - // check if they can stake - if err := k.ValidateApplicationStaking(ctx, application, msg.Value); err != nil { - ctx.Logger().Error(fmt.Sprintf("Validate App Can Stake Error, at height: %d with address: %s", ctx.BlockHeight(), sdk.Address(addr).String())) - return err.Result() - } - ctx.Logger().Info("Change App state to Staked " + sdk.Address(addr).String()) - // change the application state to staked - err := k.StakeApplication(ctx, application, msg.Value) - if err != nil { - return err.Result() + // check if the msg is to transfer an application first + if curApp, err := k.ValidateApplicationTransfer(ctx, signer, msg); err == nil { + ctx.Logger().Info( + "Transferring application", + "from", curApp.Address.String(), + "to", msg.PubKey.Address().String(), + ) + k.TransferApplication(ctx, curApp, msg.PubKey) + } else { + // otherwise check if the message is to stake an application + if err := k.ValidateApplicationStaking(ctx, application, msg.Value); err != nil { + ctx.Logger().Error(fmt.Sprintf("Validate App Can Stake Error, at height: %d with address: %s", ctx.BlockHeight(), sdk.Address(addr).String())) + return err.Result() + } + + ctx.Logger().Info("Change App state to Staked " + sdk.Address(addr).String()) + // change the application state to staked + if err := k.StakeApplication(ctx, application, msg.Value); err != nil { + return err.Result() + } } // create the event + signerAddrStr := sdk.Address(signer.Address()).String() ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeCreateApplication, @@ -57,13 +74,13 @@ func handleStake(ctx sdk.Ctx, msg types.MsgStake, k keeper.Keeper) sdk.Result { sdk.NewEvent( types.EventTypeStake, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), - sdk.NewAttribute(sdk.AttributeKeySender, sdk.Address(addr).String()), + sdk.NewAttribute(sdk.AttributeKeySender, signerAddrStr), sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Value.String()), ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), - sdk.NewAttribute(sdk.AttributeKeySender, sdk.Address(addr).String()), + sdk.NewAttribute(sdk.AttributeKeySender, signerAddrStr), ), }) return sdk.Result{Events: ctx.EventManager().Events()} diff --git a/x/apps/keeper/appStateChanges.go b/x/apps/keeper/appStateChanges.go index d2e57c87e..3c3985194 100644 --- a/x/apps/keeper/appStateChanges.go +++ b/x/apps/keeper/appStateChanges.go @@ -2,14 +2,45 @@ package keeper import ( "fmt" - "github.com/tendermint/tendermint/libs/strings" "time" "github.com/pokt-network/pocket-core/crypto" sdk "github.com/pokt-network/pocket-core/types" "github.com/pokt-network/pocket-core/x/apps/types" + "github.com/tendermint/tendermint/libs/strings" ) +func ensurePubKeyTypeSupported( + ctx sdk.Ctx, + pubKey crypto.PublicKey, + codespace sdk.CodespaceType, +) sdk.Error { + params := ctx.ConsensusParams() + if params == nil { + return nil + } + + // pubKey.PubKey() converts pocket's PublicKey to tendermint's PubKey + tmPubKey, err := crypto.CheckConsensusPubKey(pubKey.PubKey()) + if err != nil { + return types.ErrApplicationPubKeyTypeNotSupported( + codespace, + err.Error(), + params.Validator.PubKeyTypes, + ) + } + + if !strings.StringInSlice(tmPubKey.Type, params.Validator.PubKeyTypes) { + return types.ErrApplicationPubKeyTypeNotSupported( + codespace, + tmPubKey.Type, + params.Validator.PubKeyTypes, + ) + } + + return nil +} + // ValidateApplicationStaking - Check application before staking func (k Keeper) ValidateApplicationStaking(ctx sdk.Ctx, application types.Application, amount sdk.BigInt) sdk.Error { // convert the amount to sdk.Coin @@ -29,19 +60,12 @@ func (k Keeper) ValidateApplicationStaking(ctx sdk.Ctx, application types.Applic return types.ErrApplicationStatus(k.codespace) } } else { - // ensure public key type is supported - if ctx.ConsensusParams() != nil { - tmPubKey, err := crypto.CheckConsensusPubKey(application.PublicKey.PubKey()) - if err != nil { - return types.ErrApplicationPubKeyTypeNotSupported(k.Codespace(), - err.Error(), - ctx.ConsensusParams().Validator.PubKeyTypes) - } - if !strings.StringInSlice(tmPubKey.Type, ctx.ConsensusParams().Validator.PubKeyTypes) { - return types.ErrApplicationPubKeyTypeNotSupported(k.Codespace(), - tmPubKey.Type, - ctx.ConsensusParams().Validator.PubKeyTypes) - } + if err := ensurePubKeyTypeSupported( + ctx, + application.PublicKey, + k.Codespace(), + ); err != nil { + return err } } // ensure the amount they are staking is < the minimum stake amount @@ -77,6 +101,35 @@ func (k Keeper) ValidateEditStake(ctx sdk.Ctx, currentApp types.Application, amo return nil } +func (k Keeper) ValidateApplicationTransfer( + ctx sdk.Ctx, + signer crypto.PublicKey, + msg types.MsgStake, +) (types.Application, sdk.Error) { + if !ctx.IsAfterUpgradeHeight() || + !k.Cdc.IsAfterAppTransferUpgrade(ctx.BlockHeight()) { + return types.Application{}, types.ErrApplicationStatus(k.codespace) + } + + // The signer must be a staked app + curApp, found := k.GetApplication(ctx, sdk.Address(signer.Address())) + if !found || !curApp.IsStaked() { + return types.Application{}, types.ErrApplicationStatus(k.codespace) + } + + if err := ensurePubKeyTypeSupported(ctx, msg.PubKey, k.Codespace()); err != nil { + return types.Application{}, err + } + + // The pubKey of msgStake must not be a pre-existing application + newAppAddr := sdk.Address(msg.PubKey.Address()) + if _, found = k.GetApplication(ctx, newAppAddr); found { + return types.Application{}, types.ErrApplicationStatus(k.codespace) + } + + return curApp, nil +} + // StakeApplication - Store ops when a application stakes func (k Keeper) StakeApplication(ctx sdk.Ctx, application types.Application, amount sdk.BigInt) sdk.Error { // edit stake @@ -297,6 +350,28 @@ func (k Keeper) ForceApplicationUnstake(ctx sdk.Ctx, application types.Applicati return nil } +// Transfer the ownership by adding a new app and delete the current app +func (k Keeper) TransferApplication( + ctx sdk.Ctx, + curApp types.Application, + newAppPubKey crypto.PublicKey, +) { + // Add a new staked application + // Since we don't change the staked amount, we can inherit all fields + // and don't need to move tokens. + newApp := curApp.UpdateStatus(sdk.Staked) + newApp.Address = sdk.Address(newAppPubKey.Address()) + newApp.PublicKey = newAppPubKey + k.SetApplication(ctx, newApp) + + // Delete the current application + // Before UpgradeCodecHeight, we used to keep the app with Unstaked status, + // but after UpgradeCodecHeight, we just delete the app. + // (See unstakeAllMatureApplications calling DeleteApplication) + k.deleteApplicationFromStakingSet(ctx, curApp) + k.DeleteApplication(ctx, curApp.Address) +} + // JailApplication - Send a application to jail func (k Keeper) JailApplication(ctx sdk.Ctx, addr sdk.Address) { application, found := k.GetApplication(ctx, addr) diff --git a/x/apps/keeper/appStateChanges_test.go b/x/apps/keeper/appStateChanges_test.go index 67b11823b..1b650be7e 100644 --- a/x/apps/keeper/appStateChanges_test.go +++ b/x/apps/keeper/appStateChanges_test.go @@ -1,11 +1,14 @@ package keeper import ( + "reflect" + "testing" + "github.com/pokt-network/pocket-core/codec" + "github.com/pokt-network/pocket-core/crypto" sdk "github.com/pokt-network/pocket-core/types" "github.com/pokt-network/pocket-core/x/apps/types" - "reflect" - "testing" + "github.com/stretchr/testify/assert" ) func TestAppStateChange_ValidateApplicaitonBeginUnstaking(t *testing.T) { @@ -371,3 +374,140 @@ func TestAppStateChange_BeginUnstakingApplication(t *testing.T) { }) } } + +// Fund an account by minting tokens in a module account and sending it +// to the given account. +func fundAccount( + t *testing.T, + ctx *sdk.Context, + k *Keeper, + address sdk.Address, + amount sdk.BigInt, +) { + minter := types.StakedPoolName + addMintedCoinsToModule(t, ctx, k, minter) + sendFromModuleToAccount(t, ctx, k, minter, address, amount) +} + +func transferApp( + t *testing.T, + ctx *sdk.Context, + k *Keeper, + transferFrom, transferTo crypto.PublicKey, +) sdk.Error { + curApp, err := k.ValidateApplicationTransfer( + ctx, + transferFrom, + types.MsgStake{ + PubKey: transferTo, + Chains: nil, + Value: sdk.ZeroInt(), + }, + ) + if err != nil { + return err + } + k.TransferApplication(ctx, curApp, transferTo) + return nil +} + +func TestAppStateChange_Transfer(t *testing.T) { + originalUpgradeHeight := codec.UpgradeHeight + originalTestMode := codec.TestMode + originalFeatKey := codec.UpgradeFeatureMap[codec.AppTransferKey] + t.Cleanup(func() { + codec.UpgradeHeight = originalUpgradeHeight + codec.TestMode = originalTestMode + codec.UpgradeFeatureMap[codec.AppTransferKey] = originalFeatKey + }) + codec.UpgradeHeight = -1 + + ctx, _, keeper := createTestInput(t, true) + + // Create four wallets and app-stake three of them + apps := make([]types.Application, 3) + pubKeys := make([]crypto.PublicKey, 4) + addrs := make([]sdk.Address, 4) + for i := range apps { + apps[i] = createNewApplication() + pubKeys[i] = apps[i].PublicKey + addrs[i] = sdk.Address(pubKeys[i].Address()) + amount := sdk.NewInt(int64(i) * 10000000) + fundAccount(t, &ctx, &keeper, apps[i].Address, amount) + assert.Nil(t, keeper.StakeApplication(ctx, apps[i], amount)) + } + pubKeys[3] = getRandomPubKey() + addrs[3] = sdk.Address(pubKeys[3].Address()) + + // apps[0]: staked + // apps[1]: unstaking + // apps[2]: staked and jailed + // apps[3]: not an application (see pubKeys[3]) and will be used for the transfer + keeper.BeginUnstakingApplication(ctx, apps[1]) + keeper.JailApplication(ctx, apps[2].Address) + + stakedApps := keeper.GetApplications(ctx, uint16(len(apps)*2)) + assert.Equal(t, len(apps), len(stakedApps)) + + // transfer: apps[0]-->apps[3](new) fails before ugprade + err := transferApp(t, &ctx, &keeper, pubKeys[0], pubKeys[3]) + assert.NotNil(t, err) + assert.Equal(t, keeper.Codespace(), err.Codespace()) + assert.Equal(t, types.CodeInvalidStatus, err.Code()) + + // upgrade! + codec.UpgradeFeatureMap[codec.AppTransferKey] = -1 + + // transfer: apps[0]-->apps[3](new) - success + err = transferApp(t, &ctx, &keeper, pubKeys[0], pubKeys[3]) + assert.Nil(t, err) + + // transfer: apps[3]-->apps[2](unstaking) - fail + err = transferApp(t, &ctx, &keeper, pubKeys[3], pubKeys[2]) + assert.NotNil(t, err) + assert.Equal(t, keeper.Codespace(), err.Codespace()) + assert.Equal(t, types.CodeInvalidStatus, err.Code()) + + // transfer: apps[3]-->apps[0](previous owner) success + err = transferApp(t, &ctx, &keeper, pubKeys[3], pubKeys[0]) + assert.Nil(t, err) + + // transfer an unstaking app - fail + err = transferApp(t, &ctx, &keeper, pubKeys[1], getRandomPubKey()) + assert.NotNil(t, err) + assert.Equal(t, keeper.Codespace(), err.Codespace()) + assert.Equal(t, types.CodeInvalidStatus, err.Code()) + + // transfer a new app - fail + err = transferApp(t, &ctx, &keeper, getRandomPubKey(), getRandomPubKey()) + assert.NotNil(t, err) + assert.Equal(t, keeper.Codespace(), err.Codespace()) + assert.Equal(t, types.CodeInvalidStatus, err.Code()) + + // verify the state + // app[0]: staked + // app[1]: unstaking + // app[2]: staked and jailed + stakedApps = keeper.GetApplications(ctx, uint16(len(apps)*2)) + assert.Equal(t, 3, len(stakedApps)) + stakedApp, found := keeper.GetApplication(ctx, addrs[0]) + assert.True(t, found) + assert.True(t, stakedApp.IsStaked()) + stakedApp, found = keeper.GetApplication(ctx, addrs[1]) + assert.True(t, found) + assert.True(t, stakedApp.IsUnstaking()) + stakedApp, found = keeper.GetApplication(ctx, addrs[2]) + assert.True(t, found) + assert.True(t, stakedApp.IsStaked()) + assert.True(t, stakedApp.IsJailed()) + + // transfer a jailed app - success + err = transferApp(t, &ctx, &keeper, pubKeys[2], pubKeys[3]) + assert.Nil(t, err) + stakedApp, found = keeper.GetApplication(ctx, addrs[2]) + assert.False(t, found) + stakedApp, found = keeper.GetApplication(ctx, addrs[3]) + assert.True(t, found) + assert.True(t, stakedApp.IsStaked()) + assert.True(t, stakedApp.IsJailed()) +} diff --git a/x/apps/keeper/appUtil.go b/x/apps/keeper/appUtil.go index 3e0777dcc..6d1f2ec66 100644 --- a/x/apps/keeper/appUtil.go +++ b/x/apps/keeper/appUtil.go @@ -30,3 +30,34 @@ func (k Keeper) AllApplications(ctx sdk.Ctx) (apps []exported.ApplicationI) { } return apps } + +// IsMsgAppTransfer - Returns if the given message is to transfer the ownership +func (k Keeper) IsMsgAppTransfer( + ctx sdk.Ctx, + msgSigner sdk.Address, + msg sdk.Msg, +) bool { + if !ctx.IsAfterUpgradeHeight() || + !k.Cdc.IsAfterAppTransferUpgrade(ctx.BlockHeight()) { + return false + } + + msgStake, ok := msg.(*types.MsgStake) + if !ok { + return false + } + + if msgStake.IsValidTransfer() != nil { + return false + } + + if msgSigner.Equals(sdk.Address(msgStake.PubKey.Address())) { + return false + } + + if _, found := k.GetApplication(ctx, msgSigner); !found { + return false + } + + return true +} diff --git a/x/apps/keeper/common_test.go b/x/apps/keeper/common_test.go index eed1b7462..5d72bb10d 100644 --- a/x/apps/keeper/common_test.go +++ b/x/apps/keeper/common_test.go @@ -1,13 +1,19 @@ package keeper import ( - types2 "github.com/pokt-network/pocket-core/codec/types" "math/rand" "testing" + "github.com/pokt-network/pocket-core/codec" + types2 "github.com/pokt-network/pocket-core/codec/types" "github.com/pokt-network/pocket-core/crypto" + "github.com/pokt-network/pocket-core/store" + sdk "github.com/pokt-network/pocket-core/types" "github.com/pokt-network/pocket-core/types/module" "github.com/pokt-network/pocket-core/x/apps/exported" + "github.com/pokt-network/pocket-core/x/apps/types" + "github.com/pokt-network/pocket-core/x/auth" + "github.com/pokt-network/pocket-core/x/gov" govTypes "github.com/pokt-network/pocket-core/x/gov/types" "github.com/pokt-network/pocket-core/x/nodes" nodeskeeper "github.com/pokt-network/pocket-core/x/nodes/keeper" @@ -17,13 +23,6 @@ import ( "github.com/tendermint/tendermint/libs/log" tmtypes "github.com/tendermint/tendermint/types" dbm "github.com/tendermint/tm-db" - - "github.com/pokt-network/pocket-core/codec" - "github.com/pokt-network/pocket-core/store" - sdk "github.com/pokt-network/pocket-core/types" - "github.com/pokt-network/pocket-core/x/apps/types" - "github.com/pokt-network/pocket-core/x/auth" - "github.com/pokt-network/pocket-core/x/gov" ) // : deadcode unused @@ -185,6 +184,13 @@ func getUnstakedApplication() types.Application { return v.UpdateStatus(sdk.Unstaked) } +func createNewApplication() types.Application { + v := getApplication() + v.StakedTokens = sdk.ZeroInt() + v.Status = sdk.Unstaked + return v +} + func getUnstakingApplication() types.Application { v := getApplication() return v.UpdateStatus(sdk.Unstaking) diff --git a/x/apps/types/msg.go b/x/apps/types/msg.go index 3615b7d09..5a5667662 100644 --- a/x/apps/types/msg.go +++ b/x/apps/types/msg.go @@ -2,8 +2,8 @@ package types import ( "fmt" - "github.com/pokt-network/pocket-core/codec" + "github.com/pokt-network/pocket-core/codec" "github.com/pokt-network/pocket-core/crypto" sdk "github.com/pokt-network/pocket-core/types" ) @@ -45,6 +45,12 @@ func (msg MsgStake) GetSignBytes() []byte { // ValidateBasic quick validity check for staking an application func (msg MsgStake) ValidateBasic() sdk.Error { + // App's MsgStake has a special case for transferring the ownership. + // We first check if the given message is that special case or not. + if err := msg.IsValidTransfer(); err == nil { + return nil + } + if msg.PubKey == nil || msg.PubKey.RawString() == "" { return ErrNilApplicationAddr(DefaultCodespace) } @@ -62,6 +68,24 @@ func (msg MsgStake) ValidateBasic() sdk.Error { return nil } +func (msg MsgStake) IsValidTransfer() sdk.Error { + if msg.PubKey == nil { + return ErrNilApplicationAddr(DefaultCodespace) + } + + // The stake amount must be zero for transfer + if !msg.Value.IsZero() { + return ErrBadStakeAmount(DefaultCodespace) + } + + // The chains must be empty for transfer + if len(msg.Chains) > 0 { + return ErrTooManyChains(DefaultCodespace) + } + + return nil +} + // Route provides router key for msg func (msg MsgStake) Route() string { return RouterKey } @@ -177,7 +201,7 @@ func (msg MsgBeginUnstake) GetFee() sdk.BigInt { return sdk.NewInt(AppFeeMap[msg.Type()]) } -//---------------------------------------------------------------------------------------------------------------------- +// ---------------------------------------------------------------------------------------------------------------------- // Route provides router key for msg func (msg MsgUnjail) Route() string { return RouterKey } diff --git a/x/auth/ante.go b/x/auth/ante.go index 9b2b36077..66824a84d 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -63,7 +63,13 @@ func ValidateTransaction(ctx sdk.Ctx, k Keeper, stdTx types.StdTx, params Params return nil, types.ErrDuplicateTx(ModuleName, hex.EncodeToString(txHash)) } + // Please note that GetSigners() is simply redirected to Msg.GetSigners() + // and does not return the actual signer of this transaction in order to + // prevent transactions from being accepted unconditionally. + // If you want to allow a transaction signed by an address that is not + // included in this return value, add a specific condition case by case. validSigners := stdTx.GetSigners() + if k.Cdc.IsAfterNonCustodialUpgrade(ctx.BlockHeight()) && k.Cdc.IsAfterOutputAddressEditorUpgrade(ctx.BlockHeight()) { // MsgStake may be signed by the current output address. We need to ask @@ -72,6 +78,17 @@ func ValidateTransaction(ctx sdk.Ctx, k Keeper, stdTx types.StdTx, params Params k.POSKeeper.GetMsgStakeOutputSigner(ctx, stdTx.Msg)) } + if ctx.IsAfterUpgradeHeight() && + k.Cdc.IsAfterAppTransferUpgrade(ctx.BlockHeight()) { + msgSigner := sdk.Address(stdTx.Signature.Address()) + // AppMsgStake has a special case of AppTransfer. In that case, we accept + // a message where the pubkey is different from the signer, delegating + // most of validation work to the app's message handler after fee deduction. + if k.AppKeeper.IsMsgAppTransfer(ctx, msgSigner, stdTx.Msg) { + validSigners = append(validSigners, msgSigner) + } + } + var pk posCrypto.PublicKey for _, signer := range validSigners { // attempt to get the public key from the signature diff --git a/x/auth/keeper/keeper.go b/x/auth/keeper/keeper.go index 17091130a..45861514f 100644 --- a/x/auth/keeper/keeper.go +++ b/x/auth/keeper/keeper.go @@ -13,6 +13,7 @@ import ( type Keeper struct { Cdc *codec.Codec POSKeeper types.PosKeeper + AppKeeper types.AppKeeper storeKey sdk.StoreKey subspace sdk.Subspace permAddrs map[string]types.PermissionsForAddress diff --git a/x/auth/types/expectedKeepers.go b/x/auth/types/expectedKeepers.go index 9e8b09c45..6712dd459 100644 --- a/x/auth/types/expectedKeepers.go +++ b/x/auth/types/expectedKeepers.go @@ -7,3 +7,7 @@ import ( type PosKeeper interface { GetMsgStakeOutputSigner(sdk.Ctx, sdk.Msg) sdk.Address } + +type AppKeeper interface { + IsMsgAppTransfer(sdk.Ctx, sdk.Address, sdk.Msg) bool +}