diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 72fd27476..f85653c21 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -804,7 +804,11 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, txHash string, mode // Construct usable logs in multi-message transactions. logs = append(logs, fmt.Sprintf("Msg %d: %s", msgIdx, msgResult.Log)) } - + // A tx must only contain one msg. If the msg execution is success, record it + if code == sdk.ABCICodeOK { + routerName := msgs[0].Route() + ctx.RouterCallRecord()[routerName] = true + } result = sdk.Result{ Code: code, Data: data, diff --git a/server/tm_cmds.go b/server/tm_cmds.go index eadb07b20..73c28117f 100644 --- a/server/tm_cmds.go +++ b/server/tm_cmds.go @@ -67,7 +67,7 @@ func ShowAddressCmd(ctx *Context) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cfg := ctx.Config privValidator := pvm.LoadOrGenFilePV(cfg.PrivValidatorKeyFile(), cfg.PrivValidatorStateFile()) - valAddr := (sdk.ValAddress)(privValidator.GetAddress()) + valAddr := (sdk.ConsAddress)(privValidator.GetAddress()) if viper.GetBool(client.FlagJson) { return printlnJSON(valAddr) diff --git a/types/context.go b/types/context.go index 07ca5e381..367021726 100644 --- a/types/context.go +++ b/types/context.go @@ -45,6 +45,7 @@ func NewContext(ms MultiStore, header abci.Header, runTxMode RunTxMode, logger l c = c.WithTxBytes(nil) c = c.WithLogger(logger) c = c.WithVoteInfos(nil) + c = c.WithRouterCallRecord(make(map[string]bool)) return c } @@ -137,6 +138,7 @@ const ( contextKeyLogger contextKeyVoteInfos contextKeyAccountCache + contextKeyRouterCallRecord ) // NOTE: Do not expose MultiStore. @@ -183,6 +185,10 @@ func (c Context) AccountCache() AccountCache { return c.Value(contextKeyAccountCache).(AccountCache) } +func (c Context) RouterCallRecord() map[string]bool { + return c.Value(contextKeyRouterCallRecord).(map[string]bool) +} + func (c Context) WithMultiStore(ms MultiStore) Context { return c.withValue(contextKeyMultiStore, ms) } func (c Context) WithBlockHeader(header abci.Header) Context { @@ -233,6 +239,10 @@ func (c Context) WithAccountCache(cache AccountCache) Context { return c.withValue(contextKeyAccountCache, cache) } +func (c Context) WithRouterCallRecord(record map[string]bool) Context { + return c.withValue(contextKeyRouterCallRecord, record) +} + // Cache the multistore and return a new cached context. The cached context is // written to the context when writeCache is called. func (c Context) CacheContext() (cc Context, writeCache func()) { diff --git a/x/gov/client/utils.go b/x/gov/client/utils.go index 013f3944a..9bed23a3d 100644 --- a/x/gov/client/utils.go +++ b/x/gov/client/utils.go @@ -24,6 +24,14 @@ func NormalizeProposalType(proposalType string) string { return "ParameterChange" case "SoftwareUpgrade", "software_upgrade": return "SoftwareUpgrade" + case "ListTradingPair", "list_trading_pair": + return "ListTradingPair" + case "FeeChange", "fee_change": + return "FeeChange" + case "CreateValidator", "create_validator": + return "CreateValidator" + case "RemoveValidator", "remove_validator": + return "RemoveValidator" } return "" } diff --git a/x/gov/msgs_test.go b/x/gov/msgs_test.go index 1e25b7334..8bb3784df 100644 --- a/x/gov/msgs_test.go +++ b/x/gov/msgs_test.go @@ -34,7 +34,7 @@ func TestMsgSubmitProposal(t *testing.T) { {"Test Proposal", "", gov.ProposalTypeText, addrs[0], coinsPos, false}, {"Test Proposal", "the purpose of this proposal is to test", gov.ProposalTypeParameterChange, addrs[0], coinsPos, true}, {"Test Proposal", "the purpose of this proposal is to test", gov.ProposalTypeSoftwareUpgrade, addrs[0], coinsPos, true}, - {"Test Proposal", "the purpose of this proposal is to test", 0x06, addrs[0], coinsPos, false}, + {"Test Proposal", "the purpose of this proposal is to test", 0x08, addrs[0], coinsPos, false}, {"Test Proposal", "the purpose of this proposal is to test", gov.ProposalTypeText, sdk.AccAddress{}, coinsPos, false}, {"Test Proposal", "the purpose of this proposal is to test", gov.ProposalTypeText, addrs[0], coinsZero, true}, {"Test Proposal", "the purpose of this proposal is to test", gov.ProposalTypeText, addrs[0], coinsNeg, false}, diff --git a/x/gov/proposals.go b/x/gov/proposals.go index 43427c03f..cfe836d5e 100644 --- a/x/gov/proposals.go +++ b/x/gov/proposals.go @@ -119,6 +119,7 @@ const ( // ProposalTypeFeeChange belongs to ProposalTypeParameterChange. We use this to make it easily to distinguish。 ProposalTypeFeeChange ProposalKind = 0x05 ProposalTypeCreateValidator ProposalKind = 0x06 + ProposalTypeRemoveValidator ProposalKind = 0x07 ) // String to proposalType byte. Returns ff if invalid. @@ -136,6 +137,8 @@ func ProposalTypeFromString(str string) (ProposalKind, error) { return ProposalTypeFeeChange, nil case "CreateValidator": return ProposalTypeCreateValidator, nil + case "RemoveValidator": + return ProposalTypeRemoveValidator, nil default: return ProposalKind(0xff), errors.Errorf("'%s' is not a valid proposal type", str) } @@ -147,7 +150,9 @@ func validProposalType(pt ProposalKind) bool { pt == ProposalTypeParameterChange || pt == ProposalTypeSoftwareUpgrade || pt == ProposalTypeListTradingPair || - pt == ProposalTypeFeeChange { + pt == ProposalTypeFeeChange || + pt == ProposalTypeCreateValidator || + pt == ProposalTypeRemoveValidator { return true } return false @@ -200,6 +205,8 @@ func (pt ProposalKind) String() string { return "FeeChange" case ProposalTypeCreateValidator: return "CreateValidator" + case ProposalTypeRemoveValidator: + return "RemoveValidator" default: return "" } diff --git a/x/stake/client/cli/flags.go b/x/stake/client/cli/flags.go index 66378ec7a..1f3a4d116 100644 --- a/x/stake/client/cli/flags.go +++ b/x/stake/client/cli/flags.go @@ -30,7 +30,9 @@ const ( FlagNodeID = "node-id" FlagIP = "ip" - FlagProposalID = "proposal-id" + FlagProposalID = "proposal-id" + FlagConsAddrValidator = "cons-addr-validator" + FlagDeposit = "deposit" FlagOutputDocument = "output-document" // inspired by wget -O ) diff --git a/x/stake/client/cli/tx.go b/x/stake/client/cli/tx.go index 6843c81c6..cd06d6e02 100644 --- a/x/stake/client/cli/tx.go +++ b/x/stake/client/cli/tx.go @@ -28,11 +28,11 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command { WithCodec(cdc). WithAccountDecoder(authcmd.GetAccountDecoder(cdc)) - amounstStr := viper.GetString(FlagAmount) - if amounstStr == "" { + amountStr := viper.GetString(FlagAmount) + if amountStr == "" { return fmt.Errorf("Must specify amount to stake using --amount") } - amount, err := sdk.ParseCoin(amounstStr) + amount, err := sdk.ParseCoin(amountStr) if err != nil { return err } @@ -102,13 +102,21 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command { proposalId := viper.GetInt64(FlagProposalID) if proposalId == 0 { - title := "" + depositStr := viper.GetString(FlagDeposit) + if depositStr == "" { + return fmt.Errorf("must specify deposit amount when proposalId is zero using --deposit") + } + deposit, err := sdk.ParseCoin(depositStr) + if err != nil { + return err + } + title := "create validator" description, err := json.Marshal(msg) if err != nil { return err } msg = gov.NewMsgSubmitProposal(title, string(description), - gov.ProposalTypeCreateValidator, valAddr, sdk.Coins{amount}) + gov.ProposalTypeCreateValidator, valAddr, sdk.Coins{deposit}) } else { msg = stake.MsgCreateValidatorProposal{ MsgCreateValidator: msg.(stake.MsgCreateValidator), @@ -123,6 +131,7 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command { cmd.Flags().Int64(FlagProposalID, 0, "id of the CreateValidator proposal") cmd.Flags().AddFlagSet(fsPk) cmd.Flags().AddFlagSet(fsAmount) + cmd.Flags().String(FlagDeposit, "", "deposit token amount") cmd.Flags().AddFlagSet(fsDescriptionCreate) cmd.Flags().AddFlagSet(fsCommissionCreate) cmd.Flags().AddFlagSet(fsDelegator) @@ -134,6 +143,68 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command { return cmd } +// GetCmdEditValidator implements the create edit validator command. +func GetCmdRemoveValidator(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove-validator", + Short: "remove validator", + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := authtxb.NewTxBuilderFromCLI().WithCodec(cdc) + cliCtx := context.NewCLIContext(). + WithCodec(cdc). + WithAccountDecoder(authcmd.GetAccountDecoder(cdc)) + + launcher, err := cliCtx.GetFromAddress() + if err != nil { + return err + } + + validatorAddr, err := sdk.ValAddressFromBech32(viper.GetString(FlagAddressValidator)) + if err != nil { + return err + } + validatorConsAddr, err := sdk.ConsAddressFromBech32(viper.GetString(FlagConsAddrValidator)) + if err != nil { + return err + } + proposalId := viper.GetInt64(FlagProposalID) + + var msg sdk.Msg + msg = stake.NewMsgRemoveValidator(launcher, validatorAddr, validatorConsAddr, proposalId) + if proposalId == 0 { + depositStr := viper.GetString(FlagDeposit) + if depositStr == "" { + return fmt.Errorf("must specify deposit amount when proposalId is zero using --deposit") + } + deposit, err := sdk.ParseCoin(depositStr) + if err != nil { + return err + } + title := "remove validator" + description, err := json.Marshal(msg) + if err != nil { + return err + } + msg = gov.NewMsgSubmitProposal(title, string(description), + gov.ProposalTypeRemoveValidator, launcher, sdk.Coins{deposit}) + } + + if cliCtx.GenerateOnly { + return utils.PrintUnsignedStdTx(txBldr, cliCtx, []sdk.Msg{msg}, false) + } + // build and sign the transaction, then broadcast to Tendermint + return utils.CompleteAndBroadcastTxCli(txBldr, cliCtx, []sdk.Msg{msg}) + }, + } + + cmd.Flags().Int64(FlagProposalID, 0, "id of the remove validator proposal") + cmd.Flags().String(FlagAddressValidator, "", "validator address") + cmd.Flags().String(FlagConsAddrValidator, "", "validator consensus address") + cmd.Flags().String(FlagDeposit, "", "deposit token amount") + + return cmd +} + // GetCmdEditValidator implements the create edit validator command. func GetCmdEditValidator(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ diff --git a/x/stake/handler.go b/x/stake/handler.go index f0d8b3437..b2f018ca9 100644 --- a/x/stake/handler.go +++ b/x/stake/handler.go @@ -2,10 +2,9 @@ package stake import ( "bytes" - "errors" + "encoding/json" "fmt" - "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/stake/keeper" @@ -20,6 +19,8 @@ func NewHandler(k keeper.Keeper, govKeeper gov.Keeper) sdk.Handler { switch msg := msg.(type) { case types.MsgCreateValidatorProposal: return handleMsgCreateValidatorAfterProposal(ctx, msg, k, govKeeper) + case types.MsgRemoveValidator: + return handleMsgRemoveValidatorAfterProposal(ctx, msg, k, govKeeper) // disabled other msg handling //case types.MsgEditValidator: // return handleMsgEditValidator(ctx, msg, k) @@ -105,7 +106,7 @@ func handleMsgCreateValidatorAfterProposal(ctx sdk.Context, msg MsgCreateValidat height := ctx.BlockHeader().Height // do not checkProposal for the genesis txs if height != 0 { - if err := checkProposal(ctx, k.Codec(), govKeeper, msg); err != nil { + if err := checkCreateProposal(ctx, k, govKeeper, msg); err != nil { return ErrInvalidProposal(k.Codespace(), err.Error()).Result() } } @@ -113,6 +114,36 @@ func handleMsgCreateValidatorAfterProposal(ctx sdk.Context, msg MsgCreateValidat return handleMsgCreateValidator(ctx, msg.MsgCreateValidator, k) } +func handleMsgRemoveValidatorAfterProposal(ctx sdk.Context, msg MsgRemoveValidator, k keeper.Keeper, govKeeper gov.Keeper) sdk.Result { + if err := checkRemoveProposal(ctx, k, govKeeper, msg); err != nil { + return ErrInvalidProposal(k.Codespace(), err.Error()).Result() + } + + var tags sdk.Tags + var result sdk.Result + k.IterateDelegationsToValidator(ctx, msg.ValAddr, func(del sdk.Delegation) (stop bool) { + msgBeginUnbonding := MsgBeginUnbonding{ + ValidatorAddr: del.GetValidatorAddr(), + DelegatorAddr: del.GetDelegatorAddr(), + SharesAmount: del.GetShares(), + } + result = handleMsgBeginUnbonding(ctx, msgBeginUnbonding, k) + // handleMsgBeginUnbonding return error, abort execution + if !result.IsOK() { + return true + } + tags = tags.AppendTags(result.Tags) + return false + }) + + // If there is a failure in handling MsgBeginUnbonding, return an error + if !result.IsOK() { + return result + } + + return sdk.Result{Tags: tags} +} + func handleMsgCreateValidator(ctx sdk.Context, msg MsgCreateValidator, k keeper.Keeper) sdk.Result { // check to see if the pubkey or sender has been registered before _, found := k.GetValidator(ctx, msg.ValidatorAddr) @@ -164,33 +195,85 @@ func handleMsgCreateValidator(ctx sdk.Context, msg MsgCreateValidator, k keeper. } } -func checkProposal(ctx sdk.Context, cdc *codec.Codec, govKeeper gov.Keeper, msg MsgCreateValidatorProposal) error { +func checkCreateProposal(ctx sdk.Context, keeper keeper.Keeper, govKeeper gov.Keeper, msg MsgCreateValidatorProposal) error { proposal := govKeeper.GetProposal(ctx, msg.ProposalId) if proposal == nil { - return errors.New(fmt.Sprintf("proposal %d does not exist", msg.ProposalId)) + return fmt.Errorf("proposal %d does not exist", msg.ProposalId) } if proposal.GetProposalType() != gov.ProposalTypeCreateValidator { - return errors.New(fmt.Sprintf("proposal type %s is not equal to %s", - proposal.GetProposalType(), gov.ProposalTypeCreateValidator)) + return fmt.Errorf("proposal type %s is not equal to %s", + proposal.GetProposalType().String(), gov.ProposalTypeCreateValidator.String()) } if proposal.GetStatus() != gov.StatusPassed { - return errors.New(fmt.Sprintf("proposal status %d is not not passed", - proposal.GetStatus())) + return fmt.Errorf("proposal status %s is not not passed", + proposal.GetStatus().String()) } - var createValidatorParams MsgCreateValidator - err := cdc.UnmarshalJSON([]byte(proposal.GetDescription()), &createValidatorParams) + var createValidatorJson CreateValidatorJsonMsg + err := json.Unmarshal([]byte(proposal.GetDescription()), &createValidatorJson) + if err != nil { + return fmt.Errorf("unmarshal createValidator params failed, err=%s", err.Error()) + } + createValidatorMsgProposal, err := createValidatorJson.ToMsgCreateValidator() if err != nil { - return errors.New(fmt.Sprintf("unmarshal createValidator params failed, err=%s", err.Error())) + return fmt.Errorf("invalid pubkey, err=%s", err.Error()) } - if !msg.MsgCreateValidator.Equals(createValidatorParams) { - return errors.New("createValidator msg is not identical to the proposal one") + if !msg.MsgCreateValidator.Equals(createValidatorMsgProposal) { + return fmt.Errorf("createValidator msg is not identical to the proposal one") } return nil } +func checkRemoveProposal(ctx sdk.Context, keeper keeper.Keeper, govKeeper gov.Keeper, msg MsgRemoveValidator) error { + proposal := govKeeper.GetProposal(ctx, msg.ProposalId) + if proposal == nil { + return fmt.Errorf("proposal %d does not exist", msg.ProposalId) + } + if proposal.GetProposalType() != gov.ProposalTypeRemoveValidator { + return fmt.Errorf("proposal type %s is not equal to %s", + proposal.GetProposalType().String(), gov.ProposalTypeRemoveValidator.String()) + } + if proposal.GetStatus() != gov.StatusPassed { + return fmt.Errorf("proposal status %s is not not passed", + proposal.GetStatus().String()) + } + + // Check proposal description + var proposalRemoveValidator MsgRemoveValidator + err := json.Unmarshal([]byte(proposal.GetDescription()), &proposalRemoveValidator) + if err != nil { + return fmt.Errorf("unmarshal removeValidator params failed, err=%s", err.Error()) + } + if !msg.ValAddr.Equals(proposalRemoveValidator.ValAddr) || !msg.ValConsAddr.Equals(proposalRemoveValidator.ValConsAddr) { + return fmt.Errorf("removeValidator msg is not identical to the proposal one") + } + + // Check validator information + validatorToRemove, ok := keeper.GetValidator(ctx, msg.ValAddr) + if !ok { + return fmt.Errorf("trying to remove a non-existing validator") + } + if !validatorToRemove.ConsAddress().Equals(msg.ValConsAddr) { + return fmt.Errorf("consensus address can't match actual validator consensus address") + } + + // Check launcher authority + if sdk.ValAddress(msg.LauncherAddr).Equals(msg.ValAddr) { + return nil + } + // If the launcher isn't the target validator operator, then the launcher must be the operator of other active validator + launcherValidator, ok := keeper.GetValidator(ctx, sdk.ValAddress(msg.LauncherAddr)) + if !ok { + return fmt.Errorf("the launcher is not a validator operator") + } + if launcherValidator.Status != sdk.Bonded { + return fmt.Errorf("the status of launcher validator is not bonded") + } + return nil +} + func handleMsgEditValidator(ctx sdk.Context, msg types.MsgEditValidator, k keeper.Keeper) sdk.Result { // validator must already be registered validator, found := k.GetValidator(ctx, msg.ValidatorAddr) diff --git a/x/stake/handler_test.go b/x/stake/handler_test.go index c2c9ebd9a..55b6a8039 100644 --- a/x/stake/handler_test.go +++ b/x/stake/handler_test.go @@ -1,7 +1,7 @@ package stake import ( - "fmt" + "encoding/json" "testing" "time" @@ -1023,35 +1023,120 @@ func TestBondUnbondRedelegateSlashTwice(t *testing.T) { func TestCreateValidatorAfterProposal(t *testing.T) { ctx, _, keeper, govKeeper, _ := keep.CreateTestInputWithGov(t, false, 1000) - err := govKeeper.SetInitialProposalID(ctx, 0) + err := govKeeper.SetInitialProposalID(ctx, 1) require.Nil(t, err) valA := sdk.ValAddress(keep.Addrs[0]) valB := sdk.ValAddress(keep.Addrs[1]) ctx = ctx.WithBlockHeight(1) - proposalDescA := fmt.Sprintf("{\"type\": \"test/stake/CreateValidator\",\"value\": {\"Description\": {\"moniker\": \"\",\"identity\": \"\",\"website\": \"\",\"details\": \"\"},\"Commission\": {\"rate\": \"0\",\"max_rate\": \"0\",\"max_change_rate\": \"0\"},\"delegator_address\": \"%s\",\"validator_address\": \"%s\",\"pubkey\": {\"type\": \"tendermint/PubKeyEd25519\",\"value\": \"C0hc/A7sxhlEBEhDb4/J30BWbyNp5yQAKBRUy1Uq8QA=\"},\"delegation\": {\"denom\": \"steak\",\"amount\": \"10000000000\"}}}", keep.Addrs[0].String(), valA.String()) - proposalA := govKeeper.NewTextProposal(ctx, "CreateValidatorProposal", proposalDescA, gov.ProposalTypeCreateValidator) + msgCreateValidator := NewTestMsgCreateValidator(valA, keep.PKs[0], 10) + proposalDesc, _ := json.Marshal(msgCreateValidator) + proposalA := govKeeper.NewTextProposal(ctx, "CreateValidatorProposal", string(proposalDesc), gov.ProposalTypeCreateValidator) proposalA.SetStatus(gov.StatusPassed) govKeeper.SetProposal(ctx, proposalA) msgCreateValidatorA := MsgCreateValidatorProposal{ - MsgCreateValidator: NewTestMsgCreateValidator(valA, keep.PKs[0], 100), - ProposalId: 0, + MsgCreateValidator: NewTestMsgCreateValidator(valA, keep.PKs[0], 10), + ProposalId: 1, } result := handleMsgCreateValidatorAfterProposal(ctx, msgCreateValidatorA, keeper, govKeeper) require.True(t, result.IsOK()) ctx = ctx.WithBlockHeight(2) - proposalDescB := fmt.Sprintf("{\"type\": \"test/stake/CreateValidator\",\"value\": {\"Description\": {\"moniker\": \"\",\"identity\": \"\",\"website\": \"\",\"details\": \"\"},\"Commission\": {\"rate\": \"0\",\"max_rate\": \"0\",\"max_change_rate\": \"0\"},\"delegator_address\": \"%s\",\"validator_address\": \"%s\",\"pubkey\": {\"type\": \"tendermint/PubKeyEd25519\",\"value\": \"C0hc/A7sxhlEBEhDb4/J30BWbyNp5yQAKBRUy1Uq8QE=\"},\"delegation\": {\"denom\": \"steak\",\"amount\": \"10000000000\"}}}", keep.Addrs[1].String(), valB.String()) - proposalB := govKeeper.NewTextProposal(ctx, "CreateValidatorProposal", proposalDescB, gov.ProposalTypeCreateValidator) + msgCreateValidator = NewTestMsgCreateValidator(valB, keep.PKs[1], 10) + proposalDesc, _ = json.Marshal(msgCreateValidator) + proposalB := govKeeper.NewTextProposal(ctx, "CreateValidatorProposal", string(proposalDesc), gov.ProposalTypeCreateValidator) proposalB.SetStatus(gov.StatusPassed) govKeeper.SetProposal(ctx, proposalB) msgCreateValidatorB := MsgCreateValidatorProposal{ - MsgCreateValidator: NewTestMsgCreateValidator(valB, keep.PKs[1], 1000), // I deliberately changed amount value to 1000, amount should be 100 - ProposalId: 1, + MsgCreateValidator: NewTestMsgCreateValidator(valB, keep.PKs[1], 100), // I deliberately changed amount value to 100, amount should be 10 + ProposalId: 2, } result = handleMsgCreateValidatorAfterProposal(ctx, msgCreateValidatorB, keeper, govKeeper) require.False(t, result.IsOK()) } + +func TestRemoveValidatorAfterProposal(t *testing.T) { + ctx, _, keeper, govKeeper, _ := keep.CreateTestInputWithGov(t, false, 1000) + + err := govKeeper.SetInitialProposalID(ctx, 0) + require.Nil(t, err) + valA := sdk.ValAddress(keep.Addrs[0]) + valB := sdk.ValAddress(keep.Addrs[1]) + valC := sdk.ValAddress(keep.Addrs[2]) + valD := sdk.ValAddress(keep.Addrs[3]) + valE := sdk.ValAddress(keep.Addrs[4]) + valF := sdk.ValAddress(keep.Addrs[5]) + + msgCreateValidator := NewTestMsgCreateValidator(valA, keep.PKs[0], 10) + got := handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + msgCreateValidator = NewTestMsgCreateValidator(valB, keep.PKs[1], 10) + got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + msgCreateValidator = NewTestMsgCreateValidator(valC, keep.PKs[2], 10) + got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + updates := keeper.ApplyAndReturnValidatorSetUpdates(ctx) + require.Equal(t, 3, len(updates)) + + msgCreateValidator = NewTestMsgCreateValidator(valD, keep.PKs[3], 10) + got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + msgCreateValidator = NewTestMsgCreateValidator(valF, keep.PKs[5], 10) + got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + ctx = ctx.WithBlockHeight(1) + removeValidatorMsg := NewMsgRemoveValidator(nil, valA, sdk.ConsAddress(keep.PKs[0].Address()), 0) + proposalDesc, _ := json.Marshal(removeValidatorMsg) + + proposal := govKeeper.NewTextProposal(ctx, "RemoveValidatorProposal", string(proposalDesc), gov.ProposalTypeRemoveValidator) + proposal.SetStatus(gov.StatusPassed) + govKeeper.SetProposal(ctx, proposal) + + // Launcher isn't a bonded validator + msgRemoveValidator := NewMsgRemoveValidator(sdk.AccAddress(valD), valA, sdk.ConsAddress(keep.PKs[0].Address()), 0) + result := handleMsgRemoveValidatorAfterProposal(ctx, msgRemoveValidator, keeper, govKeeper) + require.False(t, result.IsOK()) + + // Launcher isn't a validator + msgRemoveValidator = NewMsgRemoveValidator(sdk.AccAddress(valE), valA, sdk.ConsAddress(keep.PKs[0].Address()), 0) + result = handleMsgRemoveValidatorAfterProposal(ctx, msgRemoveValidator, keeper, govKeeper) + require.False(t, result.IsOK()) + + // Launcher is a bonded validator + msgRemoveValidator = NewMsgRemoveValidator(sdk.AccAddress(valC), valA, sdk.ConsAddress(keep.PKs[0].Address()), 0) + result = handleMsgRemoveValidatorAfterProposal(ctx, msgRemoveValidator, keeper, govKeeper) + require.True(t, result.IsOK()) + + ctx = ctx.WithBlockHeight(2) + removeValidatorMsg = NewMsgRemoveValidator(nil, valD, sdk.ConsAddress(keep.PKs[3].Address()), 0) + proposalDesc, _ = json.Marshal(removeValidatorMsg) + proposal = govKeeper.NewTextProposal(ctx, "RemoveValidatorProposal", string(proposalDesc), gov.ProposalTypeRemoveValidator) + proposal.SetStatus(gov.StatusPassed) + govKeeper.SetProposal(ctx, proposal) + + // Launcher is the operator of target validator + msgRemoveValidator = NewMsgRemoveValidator(sdk.AccAddress(valD), valD, sdk.ConsAddress(keep.PKs[3].Address()), 1) + result = handleMsgRemoveValidatorAfterProposal(ctx, msgRemoveValidator, keeper, govKeeper) + require.True(t, result.IsOK()) + + ctx = ctx.WithBlockHeight(2) + removeValidatorMsg = NewMsgRemoveValidator(nil, valF, sdk.ConsAddress(keep.PKs[5].Address()), 0) + proposalDesc, _ = json.Marshal(removeValidatorMsg) + proposal = govKeeper.NewTextProposal(ctx, "RemoveValidatorProposal", string(proposalDesc), gov.ProposalTypeRemoveValidator) + proposal.SetStatus(gov.StatusPassed) + govKeeper.SetProposal(ctx, proposal) + + // Try to remove a different validator + msgRemoveValidator = NewMsgRemoveValidator(sdk.AccAddress(valF), valB, sdk.ConsAddress(keep.PKs[1].Address()), 2) + result = handleMsgRemoveValidatorAfterProposal(ctx, msgRemoveValidator, keeper, govKeeper) + require.False(t, result.IsOK()) +} diff --git a/x/stake/keeper/keeper.go b/x/stake/keeper/keeper.go index 09bde360d..3431eaed8 100644 --- a/x/stake/keeper/keeper.go +++ b/x/stake/keeper/keeper.go @@ -51,11 +51,6 @@ func (k Keeper) Codespace() sdk.CodespaceType { return k.codespace } -// return the amino codec -func (k Keeper) Codec() *codec.Codec { - return k.cdc -} - //_______________________________________________________________________ // load the pool diff --git a/x/stake/keeper/sdk_types.go b/x/stake/keeper/sdk_types.go index 5569c979b..2b9042b4b 100644 --- a/x/stake/keeper/sdk_types.go +++ b/x/stake/keeper/sdk_types.go @@ -123,3 +123,22 @@ func (k Keeper) IterateDelegations(ctx sdk.Context, delAddr sdk.AccAddress, i++ } } + +// iterate through all of the delegations to a validator +func (k Keeper) IterateDelegationsToValidator(ctx sdk.Context, valAddr sdk.ValAddress, + fn func(del sdk.Delegation) (stop bool)) { + + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, DelegationKey) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + del := types.MustUnmarshalDelegation(k.cdc, iterator.Key(), iterator.Value()) + if !del.ValidatorAddr.Equals(valAddr) { + continue + } + stop := fn(del) + if stop { + break + } + } +} diff --git a/x/stake/keeper/test_common.go b/x/stake/keeper/test_common.go index b0758c272..5c67f5428 100644 --- a/x/stake/keeper/test_common.go +++ b/x/stake/keeper/test_common.go @@ -60,6 +60,7 @@ func MakeTestCodec() *codec.Codec { cdc.RegisterInterface((*sdk.Msg)(nil), nil) cdc.RegisterConcrete(bank.MsgSend{}, "test/stake/Send", nil) cdc.RegisterConcrete(types.MsgCreateValidator{}, "test/stake/CreateValidator", nil) + cdc.RegisterConcrete(types.MsgRemoveValidator{}, "test/stake/RemoveValidator", nil) cdc.RegisterConcrete(types.MsgCreateValidatorProposal{}, "test/stake/CreateValidatorProposal", nil) cdc.RegisterConcrete(types.MsgEditValidator{}, "test/stake/EditValidator", nil) cdc.RegisterConcrete(types.MsgBeginUnbonding{}, "test/stake/BeginUnbonding", nil) diff --git a/x/stake/stake.go b/x/stake/stake.go index b9a9cfae9..eb8e777c4 100644 --- a/x/stake/stake.go +++ b/x/stake/stake.go @@ -19,6 +19,7 @@ type ( Params = types.Params Pool = types.Pool MsgCreateValidator = types.MsgCreateValidator + MsgRemoveValidator = types.MsgRemoveValidator MsgCreateValidatorProposal = types.MsgCreateValidatorProposal MsgEditValidator = types.MsgEditValidator MsgDelegate = types.MsgDelegate @@ -28,6 +29,7 @@ type ( QueryDelegatorParams = querier.QueryDelegatorParams QueryValidatorParams = querier.QueryValidatorParams QueryBondsParams = querier.QueryBondsParams + CreateValidatorJsonMsg = types.CreateValidatorJsonMsg ) var ( @@ -76,6 +78,7 @@ var ( RegisterCodec = types.RegisterCodec NewMsgCreateValidator = types.NewMsgCreateValidator + NewMsgRemoveValidator = types.NewMsgRemoveValidator NewMsgCreateValidatorOnBehalfOf = types.NewMsgCreateValidatorOnBehalfOf NewMsgEditValidator = types.NewMsgEditValidator NewMsgDelegate = types.NewMsgDelegate diff --git a/x/stake/types/codec.go b/x/stake/types/codec.go index 175b92ae1..95df5e9bb 100644 --- a/x/stake/types/codec.go +++ b/x/stake/types/codec.go @@ -7,6 +7,7 @@ import ( // Register concrete types on codec codec func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgCreateValidator{}, "cosmos-sdk/MsgCreateValidator", nil) + cdc.RegisterConcrete(MsgRemoveValidator{}, "cosmos-sdk/MsgRemoveValidator", nil) cdc.RegisterConcrete(MsgCreateValidatorProposal{}, "cosmos-sdk/MsgCreateValidatorProposal", nil) cdc.RegisterConcrete(MsgEditValidator{}, "cosmos-sdk/MsgEditValidator", nil) cdc.RegisterConcrete(MsgDelegate{}, "cosmos-sdk/MsgDelegate", nil) diff --git a/x/stake/types/errors.go b/x/stake/types/errors.go index 475c26f0e..c744a9748 100644 --- a/x/stake/types/errors.go +++ b/x/stake/types/errors.go @@ -29,6 +29,10 @@ func ErrNilValidatorAddr(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidInput, "validator address is nil") } +func ErrNilValidatorConsAddr(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidInput, "validator consensus address is nil") +} + func ErrBadValidatorAddr(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidAddress, "validator address is invalid") } @@ -90,6 +94,10 @@ func ErrNilDelegatorAddr(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidInput, "delegator address is nil") } +func ErrNilLauncherAddr(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidInput, "launcher address of remove validator is nil") +} + func ErrBadDenom(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidDelegation, "invalid coin denomination") } diff --git a/x/stake/types/msg.go b/x/stake/types/msg.go index 0cfd42530..10cbfe67c 100644 --- a/x/stake/types/msg.go +++ b/x/stake/types/msg.go @@ -2,9 +2,11 @@ package types import ( "bytes" + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" ) // name to identify transaction routes @@ -17,7 +19,7 @@ var _, _, _ sdk.Msg = &MsgCreateValidator{}, &MsgEditValidator{}, &MsgDelegate{} // MsgCreateValidator - struct for bonding transactions type MsgCreateValidator struct { - Description + Description Description Commission CommissionMsg DelegatorAddr sdk.AccAddress `json:"delegator_address"` ValidatorAddr sdk.ValAddress `json:"validator_address"` @@ -25,6 +27,33 @@ type MsgCreateValidator struct { Delegation sdk.Coin `json:"delegation"` } +type CreateValidatorJsonMsg struct { + Description Description + Commission CommissionMsg + DelegatorAddr sdk.AccAddress `json:"delegator_address"` + ValidatorAddr sdk.ValAddress `json:"validator_address"` + PubKey []byte `json:"pubkey"` + Delegation sdk.Coin `json:"delegation"` +} + +func (jsonMsg CreateValidatorJsonMsg) ToMsgCreateValidator() (MsgCreateValidator, error) { + if len(jsonMsg.PubKey) != ed25519.PubKeyEd25519Size { + return MsgCreateValidator{}, fmt.Errorf("pubkey size should be %d", ed25519.PubKeyEd25519Size) + } + + var pubkey ed25519.PubKeyEd25519 + copy(pubkey[:], jsonMsg.PubKey) + + return MsgCreateValidator{ + Description: jsonMsg.Description, + Commission: jsonMsg.Commission, + DelegatorAddr: jsonMsg.DelegatorAddr, + ValidatorAddr: jsonMsg.ValidatorAddr, + PubKey: pubkey, + Delegation: jsonMsg.Delegation, + }, nil +} + type MsgCreateValidatorProposal struct { MsgCreateValidator ProposalId int64 `json:"proposal_id"` @@ -116,6 +145,10 @@ func (msg MsgCreateValidator) Equals(other MsgCreateValidator) bool { return false } + if !msg.PubKey.Equals(other.PubKey) { + return false + } + return msg.Delegation.IsEqual(other.Delegation) && msg.DelegatorAddr.Equals(other.DelegatorAddr) && msg.ValidatorAddr.Equals(other.ValidatorAddr) && @@ -360,3 +393,65 @@ func (msg MsgBeginUnbonding) ValidateBasic() sdk.Error { func (msg MsgBeginUnbonding) GetInvolvedAddresses() []sdk.AccAddress { return []sdk.AccAddress{msg.DelegatorAddr, sdk.AccAddress(msg.ValidatorAddr)} } + +type MsgRemoveValidator struct { + LauncherAddr sdk.AccAddress `json:"launcher_addr"` + ValAddr sdk.ValAddress `json:"val_addr"` + ValConsAddr sdk.ConsAddress `json:"val_cons_addr"` + ProposalId int64 `json:"proposal_id"` +} + +func NewMsgRemoveValidator(launcherAddr sdk.AccAddress, valAddr sdk.ValAddress, + valConsAddr sdk.ConsAddress, proposalId int64) MsgRemoveValidator { + return MsgRemoveValidator{ + LauncherAddr: launcherAddr, + ValAddr: valAddr, + ValConsAddr: valConsAddr, + ProposalId: proposalId, + } +} + +//nolint +func (msg MsgRemoveValidator) Route() string { return MsgRoute } +func (msg MsgRemoveValidator) Type() string { return "remove_validator" } +func (msg MsgRemoveValidator) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.LauncherAddr} } + +// get the bytes for the message signer to sign on +func (msg MsgRemoveValidator) GetSignBytes() []byte { + b, err := MsgCdc.MarshalJSON(struct { + LauncherAddr sdk.AccAddress `json:"launcher_addr"` + ValAddr sdk.ValAddress `json:"val_addr"` + ValConsAddr sdk.ConsAddress `json:"val_cons_addr"` + ProposalId int64 `json:"proposal_id"` + }{ + LauncherAddr: msg.LauncherAddr, + ValAddr: msg.ValAddr, + ValConsAddr: msg.ValConsAddr, + ProposalId: msg.ProposalId, + }) + if err != nil { + panic(err) + } + return sdk.MustSortJSON(b) +} + +// quick validity check +func (msg MsgRemoveValidator) ValidateBasic() sdk.Error { + if msg.LauncherAddr.Empty() { + return ErrNilLauncherAddr(DefaultCodespace) + } + if msg.ValAddr.Empty() { + return ErrNilValidatorAddr(DefaultCodespace) + } + if msg.ValConsAddr.Empty() { + return ErrNilValidatorConsAddr(DefaultCodespace) + } + if msg.ProposalId <= 0 { + return ErrInvalidProposal(DefaultCodespace, fmt.Sprintf("Proposal id is expected to be positive, actual value is %d", msg.ProposalId)) + } + return nil +} + +func (msg MsgRemoveValidator) GetInvolvedAddresses() []sdk.AccAddress { + return []sdk.AccAddress{msg.LauncherAddr} +}