From cff985ffc5e771d5a7b4cce51b153c78d783e2ad Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Fri, 8 Feb 2019 12:44:19 -0800 Subject: [PATCH] Merge PR #3546: Min Self Delegation --- PENDING.md | 1 + client/lcd/test_helpers.go | 1 + cmd/gaia/app/genesis_test.go | 2 +- cmd/gaia/cli_test/test_helpers.go | 1 + cmd/gaia/init/gentx.go | 8 ++- cmd/gaia/init/testnet.go | 1 + types/stake.go | 1 + x/distribution/keeper/allocation_test.go | 6 +- x/distribution/keeper/delegation_test.go | 16 ++--- x/distribution/keeper/querier_test.go | 2 +- x/gov/tally_test.go | 8 +-- x/slashing/app_test.go | 2 +- x/slashing/errors.go | 5 ++ x/slashing/handler.go | 4 ++ x/slashing/handler_test.go | 29 +++++++++ x/slashing/params.go | 8 +-- x/slashing/test_common.go | 2 +- x/staking/app_test.go | 6 +- x/staking/client/cli/flags.go | 9 ++- x/staking/client/cli/tx.go | 27 +++++++- x/staking/handler.go | 12 ++++ x/staking/handler_test.go | 64 ++++++++++++++++++ x/staking/keeper/delegation.go | 16 +++-- x/staking/keeper/delegation_test.go | 15 +++-- x/staking/simulation/msgs.go | 9 +-- x/staking/staking.go | 4 ++ x/staking/test_common.go | 14 +++- x/staking/types/errors.go | 12 ++++ x/staking/types/msg.go | 83 ++++++++++++++---------- x/staking/types/msg_test.go | 45 ++++++++----- x/staking/types/validator.go | 49 ++++++++------ 31 files changed, 337 insertions(+), 125 deletions(-) diff --git a/PENDING.md b/PENDING.md index fb41e14383..6b7d20360e 100644 --- a/PENDING.md +++ b/PENDING.md @@ -21,6 +21,7 @@ BREAKING CHANGES * Gaia * [\#3457](https://github.com/cosmos/cosmos-sdk/issues/3457) Changed governance tally validatorGovInfo to use sdk.Int power instead of sdk.Dec + * [\#3495](https://github.com/cosmos/cosmos-sdk/issues/3495) Added Validator Minimum Self Delegation * Reintroduce OR semantics for tx fees * SDK diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index fc47c534c6..fe921de49d 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -238,6 +238,7 @@ func InitializeTestLCD(t *testing.T, nValidators int, initAddrs []sdk.AccAddress sdk.NewCoin(staking.DefaultBondDenom, startTokens), staking.NewDescription(fmt.Sprintf("validator-%d", i+1), "", "", ""), staking.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.OneInt(), ) stdSignMsg := txbuilder.StdSignMsg{ ChainID: genDoc.ChainID, diff --git a/cmd/gaia/app/genesis_test.go b/cmd/gaia/app/genesis_test.go index eb45112177..30765c6ae6 100644 --- a/cmd/gaia/app/genesis_test.go +++ b/cmd/gaia/app/genesis_test.go @@ -105,7 +105,7 @@ func makeMsg(name string, pk crypto.PubKey) auth.StdTx { desc := staking.NewDescription(name, "", "", "") comm := staking.CommissionMsg{} msg := staking.NewMsgCreateValidator(sdk.ValAddress(pk.Address()), pk, sdk.NewInt64Coin(bondDenom, - 50), desc, comm) + 50), desc, comm, sdk.OneInt()) return auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, nil, "") } diff --git a/cmd/gaia/cli_test/test_helpers.go b/cmd/gaia/cli_test/test_helpers.go index f5a71b4afb..aba604ae08 100644 --- a/cmd/gaia/cli_test/test_helpers.go +++ b/cmd/gaia/cli_test/test_helpers.go @@ -303,6 +303,7 @@ func (f *Fixtures) TxStakingCreateValidator(from, consPubKey string, amount sdk. cmd := fmt.Sprintf("gaiacli tx staking create-validator %v --from=%s --pubkey=%s", f.Flags(), from, consPubKey) cmd += fmt.Sprintf(" --amount=%v --moniker=%v --commission-rate=%v", amount, from, "0.05") cmd += fmt.Sprintf(" --commission-max-rate=%v --commission-max-change-rate=%v", "0.20", "0.10") + cmd += fmt.Sprintf(" --min-self-delegation=%v", "1") return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } diff --git a/cmd/gaia/init/gentx.go b/cmd/gaia/init/gentx.go index ecdbdeec43..5c35818cd9 100644 --- a/cmd/gaia/init/gentx.go +++ b/cmd/gaia/init/gentx.go @@ -36,6 +36,7 @@ var ( defaultCommissionRate = "0.1" defaultCommissionMaxRate = "0.2" defaultCommissionMaxChangeRate = "0.01" + defaultMinSelfDelegation = "1" ) // GenTxCmd builds the gaiad gentx command. @@ -53,7 +54,8 @@ following delegation and commission default parameters: commission rate: %s commission max rate: %s commission max change rate: %s -`, defaultAmount, defaultCommissionRate, defaultCommissionMaxRate, defaultCommissionMaxChangeRate), + minimum self delegation: %s +`, defaultAmount, defaultCommissionRate, defaultCommissionMaxRate, defaultCommissionMaxChangeRate, defaultMinSelfDelegation), RunE: func(cmd *cobra.Command, args []string) error { config := ctx.Config @@ -173,6 +175,7 @@ following delegation and commission default parameters: cmd.Flags().String(cli.FlagIP, ip, "The node's public IP") cmd.Flags().String(cli.FlagNodeID, "", "The node's NodeID") cmd.Flags().AddFlagSet(cli.FsCommissionCreate) + cmd.Flags().AddFlagSet(cli.FsMinSelfDelegation) cmd.Flags().AddFlagSet(cli.FsAmount) cmd.Flags().AddFlagSet(cli.FsPk) cmd.MarkFlagRequired(client.FlagName) @@ -232,6 +235,9 @@ func prepareFlagsForTxCreateValidator(config *cfg.Config, nodeID, ip, chainID st if viper.GetString(cli.FlagCommissionMaxChangeRate) == "" { viper.Set(cli.FlagCommissionMaxChangeRate, defaultCommissionMaxChangeRate) } + if viper.GetString(cli.FlagMinSelfDelegation) == "" { + viper.Set(cli.FlagMinSelfDelegation, defaultMinSelfDelegation) + } } func makeOutputFilepath(rootDir, nodeID string) (string, error) { diff --git a/cmd/gaia/init/testnet.go b/cmd/gaia/init/testnet.go index 921f24f422..24cfd2b1db 100644 --- a/cmd/gaia/init/testnet.go +++ b/cmd/gaia/init/testnet.go @@ -207,6 +207,7 @@ func initTestnet(config *tmconfig.Config, cdc *codec.Codec) error { sdk.NewCoin(stakingtypes.DefaultBondDenom, valTokens), staking.NewDescription(nodeDirName, "", "", ""), staking.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.OneInt(), ) kb, err := keys.NewKeyBaseFromDir(clientDir) if err != nil { diff --git a/types/stake.go b/types/stake.go index 05b3c4c199..6cf13385b4 100644 --- a/types/stake.go +++ b/types/stake.go @@ -46,6 +46,7 @@ type Validator interface { GetBondedTokens() Int // validator bonded tokens GetTendermintPower() int64 // validation power in tendermint GetCommission() Dec // validator commission rate + GetMinSelfDelegation() Int // validator minimum self delegation GetDelegatorShares() Dec // total outstanding delegator shares GetDelegatorShareExRate() Dec // tokens per delegator share exchange rate } diff --git a/x/distribution/keeper/allocation_test.go b/x/distribution/keeper/allocation_test.go index 704e37de54..84736f8b2b 100644 --- a/x/distribution/keeper/allocation_test.go +++ b/x/distribution/keeper/allocation_test.go @@ -20,7 +20,7 @@ func TestAllocateTokensToValidatorWithCommission(t *testing.T) { // create validator with 50% commission commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) val := sk.Validator(ctx, valOpAddr1) @@ -50,13 +50,13 @@ func TestAllocateTokensToManyValidators(t *testing.T) { // create validator with 50% commission commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) // create second validator with 0% commission commission = staking.NewCommissionMsg(sdk.NewDec(0), sdk.NewDec(0), sdk.NewDec(0)) msg = staking.NewMsgCreateValidator(valOpAddr2, valConsPk2, - sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) abciValA := abci.Validator{ diff --git a/x/distribution/keeper/delegation_test.go b/x/distribution/keeper/delegation_test.go index fb0cda012c..b6082b34ce 100644 --- a/x/distribution/keeper/delegation_test.go +++ b/x/distribution/keeper/delegation_test.go @@ -19,7 +19,7 @@ func TestCalculateRewardsBasic(t *testing.T) { // create validator with 50% commission commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) // end block to bond validator @@ -74,7 +74,7 @@ func TestCalculateRewardsAfterSlash(t *testing.T) { valPower := int64(100) valTokens := staking.TokensFromTendermintPower(valPower) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission, sdk.OneInt()) got := sh(ctx, msg) require.True(t, got.IsOK(), "%v", got) @@ -137,7 +137,7 @@ func TestCalculateRewardsAfterManySlashes(t *testing.T) { valTokens := staking.TokensFromTendermintPower(power) commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) // end block to bond validator @@ -209,7 +209,7 @@ func TestCalculateRewardsMultiDelegator(t *testing.T) { // create validator with 50% commission commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) // end block to bond validator @@ -271,7 +271,7 @@ func TestWithdrawDelegationRewardsBasic(t *testing.T) { valTokens := staking.TokensFromTendermintPower(power) commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) // assert correct initial balance @@ -327,7 +327,7 @@ func TestCalculateRewardsAfterManySlashesInSameBlock(t *testing.T) { valTokens := staking.TokensFromTendermintPower(power) commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) // end block to bond validator @@ -394,7 +394,7 @@ func TestCalculateRewardsMultiDelegatorMultiSlash(t *testing.T) { power := int64(100) valTokens := staking.TokensFromTendermintPower(power) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, valTokens), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) // end block to bond validator @@ -464,7 +464,7 @@ func TestCalculateRewardsMultiDelegatorMultWithdraw(t *testing.T) { // create validator with 50% commission commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) // end block to bond validator diff --git a/x/distribution/keeper/querier_test.go b/x/distribution/keeper/querier_test.go index eae5324edc..61c3af57ba 100644 --- a/x/distribution/keeper/querier_test.go +++ b/x/distribution/keeper/querier_test.go @@ -158,7 +158,7 @@ func TestQueries(t *testing.T) { keeper.SetOutstandingRewards(ctx, sdk.DecCoins{}) comm := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, - sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, comm) + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, comm, sdk.OneInt()) require.True(t, sh(ctx, msg).IsOK()) staking.EndBlocker(ctx, sk) val := sk.Validator(ctx, valOpAddr1) diff --git a/x/gov/tally_test.go b/x/gov/tally_test.go index f2dbe79608..966635fc76 100644 --- a/x/gov/tally_test.go +++ b/x/gov/tally_test.go @@ -28,7 +28,7 @@ func createValidators(t *testing.T, stakingHandler sdk.Handler, ctx sdk.Context, valTokens := staking.TokensFromTendermintPower(powerAmt[i]) valCreateMsg := staking.NewMsgCreateValidator( addrs[i], pubkeys[i], sdk.NewCoin(staking.DefaultBondDenom, valTokens), - testDescription, testCommissionMsg, + testDescription, testCommissionMsg, sdk.OneInt(), ) res := stakingHandler(ctx, valCreateMsg) @@ -426,19 +426,19 @@ func TestTallyDelgatorMultipleInherit(t *testing.T) { valTokens1 := staking.TokensFromTendermintPower(25) val1CreateMsg := staking.NewMsgCreateValidator( - sdk.ValAddress(addrs[0]), ed25519.GenPrivKey().PubKey(), sdk.NewCoin(staking.DefaultBondDenom, valTokens1), testDescription, testCommissionMsg, + sdk.ValAddress(addrs[0]), ed25519.GenPrivKey().PubKey(), sdk.NewCoin(staking.DefaultBondDenom, valTokens1), testDescription, testCommissionMsg, sdk.OneInt(), ) stakingHandler(ctx, val1CreateMsg) valTokens2 := staking.TokensFromTendermintPower(6) val2CreateMsg := staking.NewMsgCreateValidator( - sdk.ValAddress(addrs[1]), ed25519.GenPrivKey().PubKey(), sdk.NewCoin(staking.DefaultBondDenom, valTokens2), testDescription, testCommissionMsg, + sdk.ValAddress(addrs[1]), ed25519.GenPrivKey().PubKey(), sdk.NewCoin(staking.DefaultBondDenom, valTokens2), testDescription, testCommissionMsg, sdk.OneInt(), ) stakingHandler(ctx, val2CreateMsg) valTokens3 := staking.TokensFromTendermintPower(7) val3CreateMsg := staking.NewMsgCreateValidator( - sdk.ValAddress(addrs[2]), ed25519.GenPrivKey().PubKey(), sdk.NewCoin(staking.DefaultBondDenom, valTokens3), testDescription, testCommissionMsg, + sdk.ValAddress(addrs[2]), ed25519.GenPrivKey().PubKey(), sdk.NewCoin(staking.DefaultBondDenom, valTokens3), testDescription, testCommissionMsg, sdk.OneInt(), ) stakingHandler(ctx, val3CreateMsg) diff --git a/x/slashing/app_test.go b/x/slashing/app_test.go index 6a77f44649..56f5a0e127 100644 --- a/x/slashing/app_test.go +++ b/x/slashing/app_test.go @@ -109,7 +109,7 @@ func TestSlashingMsgs(t *testing.T) { commission := staking.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) createValidatorMsg := staking.NewMsgCreateValidator( - sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, commission, + sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, commission, sdk.OneInt(), ) mock.SignCheckDeliver(t, mapp.Cdc, mapp.BaseApp, []sdk.Msg{createValidatorMsg}, []uint64{0}, []uint64{0}, true, true, priv1) mock.CheckBalance(t, mapp, addr1, sdk.Coins{genCoin.Minus(bondCoin)}) diff --git a/x/slashing/errors.go b/x/slashing/errors.go index 81c8e18047..94997026df 100644 --- a/x/slashing/errors.go +++ b/x/slashing/errors.go @@ -16,6 +16,7 @@ const ( CodeValidatorJailed CodeType = 102 CodeValidatorNotJailed CodeType = 103 CodeMissingSelfDelegation CodeType = 104 + CodeSelfDelegationTooLow CodeType = 105 ) func ErrNoValidatorForAddress(codespace sdk.CodespaceType) sdk.Error { @@ -37,3 +38,7 @@ func ErrValidatorNotJailed(codespace sdk.CodespaceType) sdk.Error { func ErrMissingSelfDelegation(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeMissingSelfDelegation, "validator has no self-delegation; cannot be unjailed") } + +func ErrSelfDelegationTooLowToUnjail(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeValidatorNotJailed, "validator's self delegation less than MinSelfDelegation, cannot be unjailed") +} diff --git a/x/slashing/handler.go b/x/slashing/handler.go index 201cc68ee7..d9ae9f2d81 100644 --- a/x/slashing/handler.go +++ b/x/slashing/handler.go @@ -31,6 +31,10 @@ func handleMsgUnjail(ctx sdk.Context, msg MsgUnjail, k Keeper) sdk.Result { return ErrMissingSelfDelegation(k.codespace).Result() } + if validator.GetDelegatorShareExRate().Mul(selfDel.GetShares()).TruncateInt().LT(validator.GetMinSelfDelegation()) { + return ErrSelfDelegationTooLowToUnjail(k.codespace).Result() + } + // cannot be unjailed if not jailed if !validator.GetJailed() { return ErrValidatorNotJailed(k.codespace).Result() diff --git a/x/slashing/handler_test.go b/x/slashing/handler_test.go index f57dead00c..92311bd0f3 100644 --- a/x/slashing/handler_test.go +++ b/x/slashing/handler_test.go @@ -35,6 +35,35 @@ func TestCannotUnjailUnlessJailed(t *testing.T) { require.EqualValues(t, DefaultCodespace, got.Codespace) } +func TestCannotUnjailUnlessMeetMinSelfDelegation(t *testing.T) { + // initial setup + ctx, ck, sk, _, keeper := createTestInput(t, DefaultParams()) + slh := NewHandler(keeper) + amtInt := int64(100) + addr, val, amt := addrs[0], pks[0], types.TokensFromTendermintPower(amtInt) + msg := NewTestMsgCreateValidator(addr, val, amt) + msg.MinSelfDelegation = amt + got := staking.NewHandler(sk)(ctx, msg) + require.True(t, got.IsOK()) + staking.EndBlocker(ctx, sk) + + require.Equal( + t, ck.GetCoins(ctx, sdk.AccAddress(addr)), + sdk.Coins{sdk.NewCoin(sk.GetParams(ctx).BondDenom, initCoins.Sub(amt))}, + ) + + undelegateMsg := staking.NewMsgUndelegate(sdk.AccAddress(addr), addr, sdk.OneDec()) + got = staking.NewHandler(sk)(ctx, undelegateMsg) + + require.True(t, sk.Validator(ctx, addr).GetJailed()) + + // assert non-jailed validator can't be unjailed + got = slh(ctx, NewMsgUnjail(addr)) + require.False(t, got.IsOK(), "allowed unjail of validator with less than MinSelfDelegation") + require.EqualValues(t, CodeValidatorNotJailed, got.Code) + require.EqualValues(t, DefaultCodespace, got.Codespace) +} + func TestJailedValidatorDelegations(t *testing.T) { ctx, _, stakingKeeper, _, slashingKeeper := createTestInput(t, DefaultParams()) diff --git a/x/slashing/params.go b/x/slashing/params.go index 439de35ea2..5a4f18f167 100644 --- a/x/slashing/params.go +++ b/x/slashing/params.go @@ -18,10 +18,10 @@ const ( // The Double Sign Jail period ends at Max Time supported by Amino (Dec 31, 9999 - 23:59:59 GMT) var ( - DoubleSignJailEndTime = time.Unix(253402300799, 0) - DefaultMinSignedPerWindow sdk.Dec = sdk.NewDecWithPrec(5, 1) - DefaultSlashFractionDoubleSign sdk.Dec = sdk.NewDec(1).Quo(sdk.NewDec(20)) - DefaultSlashFractionDowntime sdk.Dec = sdk.NewDec(1).Quo(sdk.NewDec(100)) + DoubleSignJailEndTime = time.Unix(253402300799, 0) + DefaultMinSignedPerWindow = sdk.NewDecWithPrec(5, 1) + DefaultSlashFractionDoubleSign = sdk.NewDec(1).Quo(sdk.NewDec(20)) + DefaultSlashFractionDowntime = sdk.NewDec(1).Quo(sdk.NewDec(100)) ) // Parameter store keys diff --git a/x/slashing/test_common.go b/x/slashing/test_common.go index 93d12d7716..9b8736fc83 100644 --- a/x/slashing/test_common.go +++ b/x/slashing/test_common.go @@ -116,7 +116,7 @@ func NewTestMsgCreateValidator(address sdk.ValAddress, pubKey crypto.PubKey, amt commission := staking.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) return staking.NewMsgCreateValidator( address, pubKey, sdk.NewCoin(staking.DefaultBondDenom, amt), - staking.Description{}, commission, + staking.Description{}, commission, sdk.OneInt(), ) } diff --git a/x/staking/app_test.go b/x/staking/app_test.go index a160595c70..53216a47d8 100644 --- a/x/staking/app_test.go +++ b/x/staking/app_test.go @@ -119,7 +119,7 @@ func TestStakingMsgs(t *testing.T) { // create validator description := NewDescription("foo_moniker", "", "", "") createValidatorMsg := NewMsgCreateValidator( - sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, commissionMsg, + sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, commissionMsg, sdk.OneInt(), ) mock.SignCheckDeliver(t, mApp.Cdc, mApp.BaseApp, []sdk.Msg{createValidatorMsg}, []uint64{0}, []uint64{0}, true, true, priv1) @@ -133,7 +133,7 @@ func TestStakingMsgs(t *testing.T) { // addr1 create validator on behalf of addr2 createValidatorMsgOnBehalfOf := NewMsgCreateValidatorOnBehalfOf( - addr1, sdk.ValAddress(addr2), priv2.PubKey(), bondCoin, description, commissionMsg, + addr1, sdk.ValAddress(addr2), priv2.PubKey(), bondCoin, description, commissionMsg, sdk.OneInt(), ) mock.SignCheckDeliver(t, mApp.Cdc, mApp.BaseApp, []sdk.Msg{createValidatorMsgOnBehalfOf}, []uint64{0, 0}, []uint64{1, 0}, true, true, priv1, priv2) @@ -150,7 +150,7 @@ func TestStakingMsgs(t *testing.T) { // edit the validator description = NewDescription("bar_moniker", "", "", "") - editValidatorMsg := NewMsgEditValidator(sdk.ValAddress(addr1), description, nil) + editValidatorMsg := NewMsgEditValidator(sdk.ValAddress(addr1), description, nil, nil) mock.SignCheckDeliver(t, mApp.Cdc, mApp.BaseApp, []sdk.Msg{editValidatorMsg}, []uint64{0}, []uint64{2}, true, true, priv1) validator = checkValidator(t, mApp, keeper, sdk.ValAddress(addr1), true) diff --git a/x/staking/client/cli/flags.go b/x/staking/client/cli/flags.go index e704e44e3d..14cac4c073 100644 --- a/x/staking/client/cli/flags.go +++ b/x/staking/client/cli/flags.go @@ -26,8 +26,11 @@ const ( FlagCommissionMaxRate = "commission-max-rate" FlagCommissionMaxChangeRate = "commission-max-change-rate" - FlagNodeID = "node-id" - FlagIP = "ip" + FlagMinSelfDelegation = "min-self-delegation" + + FlagGenesisFormat = "genesis-format" + FlagNodeID = "node-id" + FlagIP = "ip" ) // common flagsets to add to various functions @@ -38,6 +41,7 @@ var ( fsDescriptionCreate = flag.NewFlagSet("", flag.ContinueOnError) FsCommissionCreate = flag.NewFlagSet("", flag.ContinueOnError) fsCommissionUpdate = flag.NewFlagSet("", flag.ContinueOnError) + FsMinSelfDelegation = flag.NewFlagSet("", flag.ContinueOnError) fsDescriptionEdit = flag.NewFlagSet("", flag.ContinueOnError) fsValidator = flag.NewFlagSet("", flag.ContinueOnError) fsDelegator = flag.NewFlagSet("", flag.ContinueOnError) @@ -57,6 +61,7 @@ func init() { FsCommissionCreate.String(FlagCommissionRate, "", "The initial commission rate percentage") FsCommissionCreate.String(FlagCommissionMaxRate, "", "The maximum commission rate percentage") FsCommissionCreate.String(FlagCommissionMaxChangeRate, "", "The maximum commission change rate percentage (per day)") + FsMinSelfDelegation.String(FlagMinSelfDelegation, "", "The minimum self delegation required on the validator") fsDescriptionEdit.String(FlagMoniker, types.DoNotModifyDesc, "The validator's name") fsDescriptionEdit.String(FlagIdentity, types.DoNotModifyDesc, "The (optional) identity signature (ex. UPort or Keybase)") fsDescriptionEdit.String(FlagWebsite, types.DoNotModifyDesc, "The validator's (optional) website") diff --git a/x/staking/client/cli/tx.go b/x/staking/client/cli/tx.go index f8ddcfd06d..74f4a76f48 100644 --- a/x/staking/client/cli/tx.go +++ b/x/staking/client/cli/tx.go @@ -43,6 +43,7 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command { cmd.Flags().AddFlagSet(FsAmount) cmd.Flags().AddFlagSet(fsDescriptionCreate) cmd.Flags().AddFlagSet(FsCommissionCreate) + cmd.Flags().AddFlagSet(FsMinSelfDelegation) cmd.Flags().AddFlagSet(fsDelegator) cmd.Flags().String(FlagIP, "", fmt.Sprintf("The node's public IP. It takes effect only when used in combination with --%s", client.FlagGenerateOnly)) cmd.Flags().String(FlagNodeID, "", "The node's ID") @@ -87,7 +88,20 @@ func GetCmdEditValidator(cdc *codec.Codec) *cobra.Command { newRate = &rate } - msg := staking.NewMsgEditValidator(sdk.ValAddress(valAddr), description, newRate) + var newMinSelfDelegation *sdk.Int + + minSelfDelegationString := viper.GetString(FlagMinSelfDelegation) + if minSelfDelegationString != "" { + msb, ok := sdk.NewIntFromString(minSelfDelegationString) + if !ok { + return fmt.Errorf(staking.ErrMinSelfDelegationInvalid(staking.DefaultCodespace).Error()) + } + newMinSelfDelegation = &msb + } + + msg := staking.NewMsgEditValidator(sdk.ValAddress(valAddr), description, newRate, newMinSelfDelegation) + + // build and sign the transaction, then broadcast to Tendermint return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}, false) }, } @@ -238,6 +252,13 @@ func BuildCreateValidatorMsg(cliCtx context.CLIContext, txBldr authtxb.TxBuilder return txBldr, nil, err } + // get the initial validator min self delegation + msbStr := viper.GetString(FlagMinSelfDelegation) + minSelfDelegation, ok := sdk.NewIntFromString(msbStr) + if !ok { + return txBldr, nil, fmt.Errorf(staking.ErrMinSelfDelegationInvalid(staking.DefaultCodespace).Error()) + } + delAddr := viper.GetString(FlagAddressDelegator) var msg sdk.Msg @@ -248,11 +269,11 @@ func BuildCreateValidatorMsg(cliCtx context.CLIContext, txBldr authtxb.TxBuilder } msg = staking.NewMsgCreateValidatorOnBehalfOf( - delAddr, sdk.ValAddress(valAddr), pk, amount, description, commissionMsg, + delAddr, sdk.ValAddress(valAddr), pk, amount, description, commissionMsg, minSelfDelegation, ) } else { msg = staking.NewMsgCreateValidator( - sdk.ValAddress(valAddr), pk, amount, description, commissionMsg, + sdk.ValAddress(valAddr), pk, amount, description, commissionMsg, minSelfDelegation, ) } diff --git a/x/staking/handler.go b/x/staking/handler.go index 76dee0eacc..81f24b6e27 100644 --- a/x/staking/handler.go +++ b/x/staking/handler.go @@ -126,6 +126,8 @@ func handleMsgCreateValidator(ctx sdk.Context, msg types.MsgCreateValidator, k k return err.Result() } + validator.MinSelfDelegation = msg.MinSelfDelegation + k.SetValidator(ctx, validator) k.SetValidatorByConsAddr(ctx, validator) k.SetNewValidatorByPowerIndex(ctx, validator) @@ -178,6 +180,16 @@ func handleMsgEditValidator(ctx sdk.Context, msg types.MsgEditValidator, k keepe validator.Commission = commission } + if msg.MinSelfDelegation != nil { + if !(*msg.MinSelfDelegation).GT(validator.MinSelfDelegation) { + return ErrMinSelfDelegationDecreased(k.Codespace()).Result() + } + if (*msg.MinSelfDelegation).GT(validator.Tokens) { + return ErrSelfDelegationBelowMinimum(k.Codespace()).Result() + } + validator.MinSelfDelegation = (*msg.MinSelfDelegation) + } + k.SetValidator(ctx, validator) tags := sdk.NewTags( diff --git a/x/staking/handler_test.go b/x/staking/handler_test.go index 7b8778a87e..cb7084356a 100644 --- a/x/staking/handler_test.go +++ b/x/staking/handler_test.go @@ -375,6 +375,70 @@ func TestIncrementsMsgDelegate(t *testing.T) { } } +func TestEditValidatorDecreaseMinSelfDelegation(t *testing.T) { + validatorAddr := sdk.ValAddress(keep.Addrs[0]) + + initPower := int64(100) + initBond := types.TokensFromTendermintPower(100) + ctx, _, keeper := keep.CreateTestInput(t, false, initPower) + _ = setInstantUnbondPeriod(keeper, ctx) + + // create validator + msgCreateValidator := NewTestMsgCreateValidator(validatorAddr, keep.PKs[0], initBond) + msgCreateValidator.MinSelfDelegation = sdk.NewInt(2) + got := handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected create-validator to be ok, got %v", got) + + // must end-block + updates := keeper.ApplyAndReturnValidatorSetUpdates(ctx) + require.Equal(t, 1, len(updates)) + + // verify the self-delegation exists + bond, found := keeper.GetDelegation(ctx, sdk.AccAddress(validatorAddr), validatorAddr) + require.True(t, found) + gotBond := bond.Shares.RoundInt() + require.Equal(t, initBond, gotBond, + "initBond: %v\ngotBond: %v\nbond: %v\n", + initBond, gotBond, bond) + + newMinSelfDelegation := sdk.OneInt() + msgEditValidator := NewMsgEditValidator(validatorAddr, Description{}, nil, &newMinSelfDelegation) + got = handleMsgEditValidator(ctx, msgEditValidator, keeper) + require.False(t, got.IsOK(), "should not be able to decrease minSelfDelegation") +} + +func TestEditValidatorIncreaseMinSelfDelegationBeyondCurrentBond(t *testing.T) { + validatorAddr := sdk.ValAddress(keep.Addrs[0]) + + initPower := int64(100) + initBond := types.TokensFromTendermintPower(100) + ctx, _, keeper := keep.CreateTestInput(t, false, initPower) + _ = setInstantUnbondPeriod(keeper, ctx) + + // create validator + msgCreateValidator := NewTestMsgCreateValidator(validatorAddr, keep.PKs[0], initBond) + msgCreateValidator.MinSelfDelegation = sdk.NewInt(2) + got := handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected create-validator to be ok, got %v", got) + + // must end-block + updates := keeper.ApplyAndReturnValidatorSetUpdates(ctx) + require.Equal(t, 1, len(updates)) + + // verify the self-delegation exists + bond, found := keeper.GetDelegation(ctx, sdk.AccAddress(validatorAddr), validatorAddr) + require.True(t, found) + gotBond := bond.Shares.RoundInt() + require.Equal(t, initBond, gotBond, + "initBond: %v\ngotBond: %v\nbond: %v\n", + initBond, gotBond, bond) + + newMinSelfDelegation := initBond.Add(sdk.OneInt()) + msgEditValidator := NewMsgEditValidator(validatorAddr, Description{}, nil, &newMinSelfDelegation) + got = handleMsgEditValidator(ctx, msgEditValidator, keeper) + require.False(t, got.IsOK(), "should not be able to increase minSelfDelegation above current self delegation") +} + func TestIncrementsMsgUnbond(t *testing.T) { initPower := int64(1000) initBond := TokensFromTendermintPower(initPower) diff --git a/x/staking/keeper/delegation.go b/x/staking/keeper/delegation.go index e6874d7c27..f1588477e8 100644 --- a/x/staking/keeper/delegation.go +++ b/x/staking/keeper/delegation.go @@ -516,13 +516,17 @@ func (k Keeper) unbond(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValA // subtract shares from delegation delegation.Shares = delegation.Shares.Sub(shares) - // update or remove the delegation + isValidatorOperator := bytes.Equal(delegation.DelegatorAddr, validator.OperatorAddr) + + // if the delegation is the operator of the validator and undelegating will decrease the validator's self delegation below their minimum + // trigger a jail validator + if isValidatorOperator && !validator.Jailed && validator.DelegatorShareExRate().Mul(delegation.Shares).TruncateInt().LT(validator.MinSelfDelegation) { + k.jailValidator(ctx, validator) + validator = k.mustGetValidator(ctx, validator.OperatorAddr) + } + + // remove the delegation if delegation.Shares.IsZero() { - // if the delegator is the operator of the validator then jail the validator - if bytes.Equal(delegation.DelegatorAddr, validator.OperatorAddr) && !validator.Jailed { - k.jailValidator(ctx, validator) - validator = k.mustGetValidator(ctx, validator.OperatorAddr) - } k.RemoveDelegation(ctx, delegation) } else { k.SetDelegation(ctx, delegation) diff --git a/x/staking/keeper/delegation_test.go b/x/staking/keeper/delegation_test.go index 49e8ccc722..e011765db0 100644 --- a/x/staking/keeper/delegation_test.go +++ b/x/staking/keeper/delegation_test.go @@ -250,9 +250,10 @@ func TestUnbondingDelegationsMaxEntries(t *testing.T) { require.NoError(t, err) } -// test removing all self delegation from a validator which should -// shift it from the bonded to unbonded state -func TestUndelegateSelfDelegation(t *testing.T) { +// test undelegating self delegation from a validator pushing it below MinSelfDelegation +// shift it from the bonded to unbonding state and jailed +func TestUndelegateSelfDelegationBelowMinSelfDelegation(t *testing.T) { + ctx, _, keeper := CreateTestInput(t, false, 0) pool := keeper.GetPool(ctx) startTokens := types.TokensFromTendermintPower(20) @@ -260,9 +261,12 @@ func TestUndelegateSelfDelegation(t *testing.T) { //create a validator with a self-delegation validator := types.NewValidator(addrVals[0], PKs[0], types.Description{}) + valTokens := types.TokensFromTendermintPower(10) + validator.MinSelfDelegation = valTokens validator, pool, issuedShares := validator.AddTokensFromDel(pool, valTokens) require.Equal(t, valTokens, issuedShares.RoundInt()) + keeper.SetPool(ctx, pool) validator = TestingUpdateValidator(keeper, ctx, validator, true) pool = keeper.GetPool(ctx) @@ -281,7 +285,7 @@ func TestUndelegateSelfDelegation(t *testing.T) { keeper.SetDelegation(ctx, delegation) val0AccAddr := sdk.AccAddress(addrVals[0].Bytes()) - _, err := keeper.Undelegate(ctx, val0AccAddr, addrVals[0], sdk.NewDecFromInt(valTokens)) + _, err := keeper.Undelegate(ctx, val0AccAddr, addrVals[0], sdk.NewDecFromInt(types.TokensFromTendermintPower(6))) require.NoError(t, err) // end block @@ -290,8 +294,9 @@ func TestUndelegateSelfDelegation(t *testing.T) { validator, found := keeper.GetValidator(ctx, addrVals[0]) require.True(t, found) - require.Equal(t, delTokens, validator.Tokens) + require.Equal(t, types.TokensFromTendermintPower(14), validator.Tokens) require.Equal(t, sdk.Unbonding, validator.Status) + require.True(t, validator.Jailed) } func TestUndelegateFromUnbondingValidator(t *testing.T) { diff --git a/x/staking/simulation/msgs.go b/x/staking/simulation/msgs.go index 7487671eed..8ae5c4baa2 100644 --- a/x/staking/simulation/msgs.go +++ b/x/staking/simulation/msgs.go @@ -44,7 +44,7 @@ func SimulateMsgCreateValidator(m auth.AccountKeeper, k staking.Keeper) simulati selfDelegation := sdk.NewCoin(denom, amount) msg := staking.NewMsgCreateValidator(address, acc.PubKey, - selfDelegation, description, commission) + selfDelegation, description, commission, sdk.OneInt()) if msg.ValidateBasic() != nil { return "", nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) @@ -82,16 +82,11 @@ func SimulateMsgEditValidator(k staking.Keeper) simulation.Operation { address := val.GetOperator() newCommissionRate := simulation.RandomDecAmount(r, val.Commission.MaxRate) - msg := staking.MsgEditValidator{ - Description: description, - ValidatorAddr: address, - CommissionRate: &newCommissionRate, - } + msg := staking.NewMsgEditValidator(address, description, &newCommissionRate, nil) if msg.ValidateBasic() != nil { return "", nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) } - ctx, write := ctx.CacheContext() result := handler(ctx, msg) if result.IsOK() { diff --git a/x/staking/staking.go b/x/staking/staking.go index 3544c2d2c1..dd41da0c07 100644 --- a/x/staking/staking.go +++ b/x/staking/staking.go @@ -164,6 +164,10 @@ var ( ErrBothShareMsgsGiven = types.ErrBothShareMsgsGiven ErrNeitherShareMsgsGiven = types.ErrNeitherShareMsgsGiven ErrMissingSignature = types.ErrMissingSignature + + ErrMinSelfDelegationInvalid = types.ErrMinSelfDelegationInvalid + ErrMinSelfDelegationDecreased = types.ErrMinSelfDelegationDecreased + ErrSelfDelegationBelowMinimum = types.ErrSelfDelegationBelowMinimum ) var ( diff --git a/x/staking/test_common.go b/x/staking/test_common.go index ad9d1ef795..aeb9ce626f 100644 --- a/x/staking/test_common.go +++ b/x/staking/test_common.go @@ -28,7 +28,7 @@ var ( func NewTestMsgCreateValidator(address sdk.ValAddress, pubKey crypto.PubKey, amt sdk.Int) MsgCreateValidator { return types.NewMsgCreateValidator( - address, pubKey, sdk.NewCoin(types.DefaultBondDenom, amt), Description{}, commissionMsg, + address, pubKey, sdk.NewCoin(types.DefaultBondDenom, amt), Description{}, commissionMsg, sdk.OneInt(), ) } @@ -38,7 +38,15 @@ func NewTestMsgCreateValidatorWithCommission(address sdk.ValAddress, pubKey cryp commission := NewCommissionMsg(commissionRate, sdk.OneDec(), sdk.ZeroDec()) return types.NewMsgCreateValidator( - address, pubKey, sdk.NewCoin(types.DefaultBondDenom, amt), Description{}, commission, + address, pubKey, sdk.NewCoin(types.DefaultBondDenom, amt), Description{}, commission, sdk.OneInt(), + ) +} + +func NewTestMsgCreateValidatorWithMinSelfDelegation(address sdk.ValAddress, pubKey crypto.PubKey, + amt sdk.Int, minSelfDelegation sdk.Int) MsgCreateValidator { + + return types.NewMsgCreateValidator( + address, pubKey, sdk.NewCoin(types.DefaultBondDenom, amt), Description{}, commissionMsg, minSelfDelegation, ) } @@ -51,5 +59,5 @@ func NewTestMsgCreateValidatorOnBehalfOf(delAddr sdk.AccAddress, valAddr sdk.Val valPubKey crypto.PubKey, amt sdk.Int) MsgCreateValidator { amount := sdk.NewCoin(types.DefaultBondDenom, amt) - return NewMsgCreateValidatorOnBehalfOf(delAddr, valAddr, valPubKey, amount, Description{}, commissionMsg) + return NewMsgCreateValidatorOnBehalfOf(delAddr, valAddr, valPubKey, amount, Description{}, commissionMsg, sdk.OneInt()) } diff --git a/x/staking/types/errors.go b/x/staking/types/errors.go index 37a80f24c9..012f9421f8 100644 --- a/x/staking/types/errors.go +++ b/x/staking/types/errors.go @@ -91,6 +91,18 @@ func ErrCommissionGTMaxChangeRate(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidValidator, "commission cannot be changed more than max change rate") } +func ErrSelfDelegationBelowMinimum(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidValidator, "validator's self delegation must be greater than their minimum self delegation") +} + +func ErrMinSelfDelegationInvalid(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidValidator, "minimum self delegation must be a positive integer") +} + +func ErrMinSelfDelegationDecreased(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidValidator, "minimum self delegation cannot be decrease") +} + func ErrNilDelegatorAddr(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidInput, "delegator address is nil") } diff --git a/x/staking/types/msg.go b/x/staking/types/msg.go index 2437d3ea38..14b315d949 100644 --- a/x/staking/types/msg.go +++ b/x/staking/types/msg.go @@ -23,42 +23,45 @@ var ( // MsgCreateValidator - struct for bonding transactions // TODO: Why does this need to contain a denomination in `Value` type MsgCreateValidator struct { - Description Description `json:"description"` - Commission CommissionMsg `json:"commission"` - DelegatorAddr sdk.AccAddress `json:"delegator_address"` - ValidatorAddr sdk.ValAddress `json:"validator_address"` - PubKey crypto.PubKey `json:"pubkey"` - Value sdk.Coin `json:"value"` + Description Description `json:"description"` + Commission CommissionMsg `json:"commission"` + MinSelfDelegation sdk.Int `json:"min_self_delegation"` + DelegatorAddr sdk.AccAddress `json:"delegator_address"` + ValidatorAddr sdk.ValAddress `json:"validator_address"` + PubKey crypto.PubKey `json:"pubkey"` + Value sdk.Coin `json:"value"` } type msgCreateValidatorJSON struct { - Description Description `json:"description"` - Commission CommissionMsg `json:"commission"` - DelegatorAddr sdk.AccAddress `json:"delegator_address"` - ValidatorAddr sdk.ValAddress `json:"validator_address"` - PubKey string `json:"pubkey"` - Value sdk.Coin `json:"value"` + Description Description `json:"description"` + Commission CommissionMsg `json:"commission"` + MinSelfDelegation sdk.Int `json:"min_self_delegation"` + DelegatorAddr sdk.AccAddress `json:"delegator_address"` + ValidatorAddr sdk.ValAddress `json:"validator_address"` + PubKey string `json:"pubkey"` + Value sdk.Coin `json:"value"` } // Default way to create validator. Delegator address and validator address are the same func NewMsgCreateValidator(valAddr sdk.ValAddress, pubkey crypto.PubKey, - selfDelegation sdk.Coin, description Description, commission CommissionMsg) MsgCreateValidator { + selfDelegation sdk.Coin, description Description, commission CommissionMsg, minSelfDelegation sdk.Int) MsgCreateValidator { return NewMsgCreateValidatorOnBehalfOf( - sdk.AccAddress(valAddr), valAddr, pubkey, selfDelegation, description, commission, + sdk.AccAddress(valAddr), valAddr, pubkey, selfDelegation, description, commission, minSelfDelegation, ) } // Creates validator msg by delegator address on behalf of validator address func NewMsgCreateValidatorOnBehalfOf(delAddr sdk.AccAddress, valAddr sdk.ValAddress, - pubkey crypto.PubKey, value sdk.Coin, description Description, commission CommissionMsg) MsgCreateValidator { + pubkey crypto.PubKey, value sdk.Coin, description Description, commission CommissionMsg, minSelfDelegation sdk.Int) MsgCreateValidator { return MsgCreateValidator{ - Description: description, - DelegatorAddr: delAddr, - ValidatorAddr: valAddr, - PubKey: pubkey, - Value: value, - Commission: commission, + Description: description, + DelegatorAddr: delAddr, + ValidatorAddr: valAddr, + PubKey: pubkey, + Value: value, + Commission: commission, + MinSelfDelegation: minSelfDelegation, } } @@ -83,12 +86,13 @@ func (msg MsgCreateValidator) GetSigners() []sdk.AccAddress { // serialization of the MsgCreateValidator type. func (msg MsgCreateValidator) MarshalJSON() ([]byte, error) { return json.Marshal(msgCreateValidatorJSON{ - Description: msg.Description, - Commission: msg.Commission, - DelegatorAddr: msg.DelegatorAddr, - ValidatorAddr: msg.ValidatorAddr, - PubKey: sdk.MustBech32ifyConsPub(msg.PubKey), - Value: msg.Value, + Description: msg.Description, + Commission: msg.Commission, + DelegatorAddr: msg.DelegatorAddr, + ValidatorAddr: msg.ValidatorAddr, + PubKey: sdk.MustBech32ifyConsPub(msg.PubKey), + Value: msg.Value, + MinSelfDelegation: msg.MinSelfDelegation, }) } @@ -106,6 +110,7 @@ func (msg *MsgCreateValidator) UnmarshalJSON(bz []byte) error { msg.ValidatorAddr = msgCreateValJSON.ValidatorAddr msg.PubKey = sdk.MustGetConsPubKeyBech32(msgCreateValJSON.PubKey) msg.Value = msgCreateValJSON.Value + msg.MinSelfDelegation = msgCreateValJSON.MinSelfDelegation return nil } @@ -134,6 +139,12 @@ func (msg MsgCreateValidator) ValidateBasic() sdk.Error { if msg.Commission == (CommissionMsg{}) { return sdk.NewError(DefaultCodespace, CodeInvalidInput, "commission must be included") } + if !msg.MinSelfDelegation.GT(sdk.ZeroInt()) { + return ErrMinSelfDelegationInvalid(DefaultCodespace) + } + if msg.Value.Amount.LT(msg.MinSelfDelegation) { + return ErrSelfDelegationBelowMinimum(DefaultCodespace) + } return nil } @@ -143,19 +154,21 @@ type MsgEditValidator struct { Description ValidatorAddr sdk.ValAddress `json:"address"` - // We pass a reference to the new commission rate as it's not mandatory to + // We pass a reference to the new commission rate and min self delegation as it's not mandatory to // update. If not updated, the deserialized rate will be zero with no way to // distinguish if an update was intended. // // REF: #2373 - CommissionRate *sdk.Dec `json:"commission_rate"` + CommissionRate *sdk.Dec `json:"commission_rate"` + MinSelfDelegation *sdk.Int `json:"min_self_delegation"` } -func NewMsgEditValidator(valAddr sdk.ValAddress, description Description, newRate *sdk.Dec) MsgEditValidator { +func NewMsgEditValidator(valAddr sdk.ValAddress, description Description, newRate *sdk.Dec, newMinSelfDelegation *sdk.Int) MsgEditValidator { return MsgEditValidator{ - Description: description, - CommissionRate: newRate, - ValidatorAddr: valAddr, + Description: description, + CommissionRate: newRate, + ValidatorAddr: valAddr, + MinSelfDelegation: newMinSelfDelegation, } } @@ -182,6 +195,10 @@ func (msg MsgEditValidator) ValidateBasic() sdk.Error { return sdk.NewError(DefaultCodespace, CodeInvalidInput, "transaction must include some information to modify") } + if msg.MinSelfDelegation != nil && !(*msg.MinSelfDelegation).GT(sdk.ZeroInt()) { + return ErrMinSelfDelegationInvalid(DefaultCodespace) + } + if msg.CommissionRate != nil { if msg.CommissionRate.GT(sdk.OneDec()) || msg.CommissionRate.LT(sdk.ZeroDec()) { return sdk.NewError(DefaultCodespace, CodeInvalidInput, "commission rate must be between 0 and 1, inclusive") diff --git a/x/staking/types/msg_test.go b/x/staking/types/msg_test.go index 6eba604377..ce3e6383bd 100644 --- a/x/staking/types/msg_test.go +++ b/x/staking/types/msg_test.go @@ -23,22 +23,26 @@ func TestMsgCreateValidator(t *testing.T) { tests := []struct { name, moniker, identity, website, details string commissionMsg CommissionMsg + minSelfDelegation sdk.Int validatorAddr sdk.ValAddress pubkey crypto.PubKey bond sdk.Coin expectPass bool }{ - {"basic good", "a", "b", "c", "d", commission1, addr1, pk1, coinPos, true}, - {"partial description", "", "", "c", "", commission1, addr1, pk1, coinPos, true}, - {"empty description", "", "", "", "", commission2, addr1, pk1, coinPos, false}, - {"empty address", "a", "b", "c", "d", commission2, emptyAddr, pk1, coinPos, false}, - {"empty pubkey", "a", "b", "c", "d", commission1, addr1, emptyPubkey, coinPos, true}, - {"empty bond", "a", "b", "c", "d", commission2, addr1, pk1, coinZero, false}, + {"basic good", "a", "b", "c", "d", commission1, sdk.OneInt(), addr1, pk1, coinPos, true}, + {"partial description", "", "", "c", "", commission1, sdk.OneInt(), addr1, pk1, coinPos, true}, + {"empty description", "", "", "", "", commission2, sdk.OneInt(), addr1, pk1, coinPos, false}, + {"empty address", "a", "b", "c", "d", commission2, sdk.OneInt(), emptyAddr, pk1, coinPos, false}, + {"empty pubkey", "a", "b", "c", "d", commission1, sdk.OneInt(), addr1, emptyPubkey, coinPos, true}, + {"empty bond", "a", "b", "c", "d", commission2, sdk.OneInt(), addr1, pk1, coinZero, false}, + {"zero min self delegation", "a", "b", "c", "d", commission1, sdk.ZeroInt(), addr1, pk1, coinPos, false}, + {"negative min self delegation", "a", "b", "c", "d", commission1, sdk.NewInt(-1), addr1, pk1, coinPos, false}, + {"delegation less than min self delegation", "a", "b", "c", "d", commission1, coinPos.Amount.Add(sdk.OneInt()), addr1, pk1, coinPos, false}, } for _, tc := range tests { description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details) - msg := NewMsgCreateValidator(tc.validatorAddr, tc.pubkey, tc.bond, description, tc.commissionMsg) + msg := NewMsgCreateValidator(tc.validatorAddr, tc.pubkey, tc.bond, description, tc.commissionMsg, tc.minSelfDelegation) if tc.expectPass { require.Nil(t, msg.ValidateBasic(), "test: %v", tc.name) } else { @@ -63,8 +67,9 @@ func TestMsgEditValidator(t *testing.T) { for _, tc := range tests { description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details) newRate := sdk.ZeroDec() + newMinSelfDelegation := sdk.OneInt() - msg := NewMsgEditValidator(tc.validatorAddr, description, &newRate) + msg := NewMsgEditValidator(tc.validatorAddr, description, &newRate, &newMinSelfDelegation) if tc.expectPass { require.Nil(t, msg.ValidateBasic(), "test: %v", tc.name) } else { @@ -81,25 +86,29 @@ func TestMsgCreateValidatorOnBehalfOf(t *testing.T) { tests := []struct { name, moniker, identity, website, details string commissionMsg CommissionMsg + minSelfDelegation sdk.Int delegatorAddr sdk.AccAddress validatorAddr sdk.ValAddress validatorPubKey crypto.PubKey bond sdk.Coin expectPass bool }{ - {"basic good", "a", "b", "c", "d", commission2, sdk.AccAddress(addr1), addr2, pk2, coinPos, true}, - {"partial description", "", "", "c", "", commission2, sdk.AccAddress(addr1), addr2, pk2, coinPos, true}, - {"empty description", "", "", "", "", commission1, sdk.AccAddress(addr1), addr2, pk2, coinPos, false}, - {"empty delegator address", "a", "b", "c", "d", commission1, sdk.AccAddress(emptyAddr), addr2, pk2, coinPos, false}, - {"empty validator address", "a", "b", "c", "d", commission2, sdk.AccAddress(addr1), emptyAddr, pk2, coinPos, false}, - {"empty pubkey", "a", "b", "c", "d", commission1, sdk.AccAddress(addr1), addr2, emptyPubkey, coinPos, true}, - {"empty bond", "a", "b", "c", "d", commission2, sdk.AccAddress(addr1), addr2, pk2, coinZero, false}, + {"basic good", "a", "b", "c", "d", commission2, sdk.OneInt(), sdk.AccAddress(addr1), addr2, pk2, coinPos, true}, + {"partial description", "", "", "c", "", commission2, sdk.OneInt(), sdk.AccAddress(addr1), addr2, pk2, coinPos, true}, + {"empty description", "", "", "", "", commission1, sdk.OneInt(), sdk.AccAddress(addr1), addr2, pk2, coinPos, false}, + {"empty delegator address", "a", "b", "c", "d", commission1, sdk.OneInt(), sdk.AccAddress(emptyAddr), addr2, pk2, coinPos, false}, + {"empty validator address", "a", "b", "c", "d", commission2, sdk.OneInt(), sdk.AccAddress(addr1), emptyAddr, pk2, coinPos, false}, + {"empty pubkey", "a", "b", "c", "d", commission1, sdk.OneInt(), sdk.AccAddress(addr1), addr2, emptyPubkey, coinPos, true}, + {"empty bond", "a", "b", "c", "d", commission2, sdk.OneInt(), sdk.AccAddress(addr1), addr2, pk2, coinZero, false}, + {"zero min self delegation", "a", "b", "c", "d", commission2, sdk.ZeroInt(), sdk.AccAddress(addr1), addr2, pk2, coinPos, false}, + {"negative min self delegation", "", "", "c", "", commission2, sdk.NewInt(-1), sdk.AccAddress(addr1), addr2, pk2, coinPos, false}, + {"delegation less than min self delegation", "a", "b", "c", "d", commission2, coinPos.Amount.Add(sdk.OneInt()), sdk.AccAddress(addr1), addr2, pk2, coinPos, false}, } for _, tc := range tests { description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details) msg := NewMsgCreateValidatorOnBehalfOf( - tc.delegatorAddr, tc.validatorAddr, tc.validatorPubKey, tc.bond, description, tc.commissionMsg, + tc.delegatorAddr, tc.validatorAddr, tc.validatorPubKey, tc.bond, description, tc.commissionMsg, tc.minSelfDelegation, ) if tc.expectPass { @@ -109,11 +118,11 @@ func TestMsgCreateValidatorOnBehalfOf(t *testing.T) { } } - msg := NewMsgCreateValidator(addr1, pk1, coinPos, Description{}, CommissionMsg{}) + msg := NewMsgCreateValidator(addr1, pk1, coinPos, Description{}, CommissionMsg{}, sdk.OneInt()) addrs := msg.GetSigners() require.Equal(t, []sdk.AccAddress{sdk.AccAddress(addr1)}, addrs, "Signers on default msg is wrong") - msg = NewMsgCreateValidatorOnBehalfOf(sdk.AccAddress(addr2), addr1, pk1, coinPos, Description{}, CommissionMsg{}) + msg = NewMsgCreateValidatorOnBehalfOf(sdk.AccAddress(addr2), addr1, pk1, coinPos, Description{}, CommissionMsg{}, sdk.OneInt()) addrs = msg.GetSigners() require.Equal(t, []sdk.AccAddress{sdk.AccAddress(addr2), sdk.AccAddress(addr1)}, addrs, "Signers for onbehalfof msg is wrong") } diff --git a/x/staking/types/validator.go b/x/staking/types/validator.go index 21145081f2..37e32b5d9b 100644 --- a/x/staking/types/validator.go +++ b/x/staking/types/validator.go @@ -32,16 +32,17 @@ const ( // divided by the current exchange rate. Voting power can be calculated as total // bonded shares multiplied by exchange rate. type Validator struct { - OperatorAddr sdk.ValAddress `json:"operator_address"` // address of the validator's operator; bech encoded in JSON - ConsPubKey crypto.PubKey `json:"consensus_pubkey"` // the consensus public key of the validator; bech encoded in JSON - Jailed bool `json:"jailed"` // has the validator been jailed from bonded status? - Status sdk.BondStatus `json:"status"` // validator status (bonded/unbonding/unbonded) - Tokens sdk.Int `json:"tokens"` // delegated tokens (incl. self-delegation) - DelegatorShares sdk.Dec `json:"delegator_shares"` // total shares issued to a validator's delegators - Description Description `json:"description"` // description terms for the validator - UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding - UnbondingCompletionTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding - Commission Commission `json:"commission"` // commission parameters + OperatorAddr sdk.ValAddress `json:"operator_address"` // address of the validator's operator; bech encoded in JSON + ConsPubKey crypto.PubKey `json:"consensus_pubkey"` // the consensus public key of the validator; bech encoded in JSON + Jailed bool `json:"jailed"` // has the validator been jailed from bonded status? + Status sdk.BondStatus `json:"status"` // validator status (bonded/unbonding/unbonded) + Tokens sdk.Int `json:"tokens"` // delegated tokens (incl. self-delegation) + DelegatorShares sdk.Dec `json:"delegator_shares"` // total shares issued to a validator's delegators + Description Description `json:"description"` // description terms for the validator + UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding + UnbondingCompletionTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding + Commission Commission `json:"commission"` // commission parameters + MinSelfDelegation sdk.Int `json:"min_self_delegation"` // validator's self declared minimum self delegation } // Validators is a collection of Validator @@ -67,6 +68,7 @@ func NewValidator(operator sdk.ValAddress, pubKey crypto.PubKey, description Des UnbondingHeight: int64(0), UnbondingCompletionTime: time.Unix(0, 0).UTC(), Commission: NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + MinSelfDelegation: sdk.OneInt(), } } @@ -106,24 +108,26 @@ func (v Validator) String() string { Description: %s Unbonding Height: %d Unbonding Completion Time: %v + Minimum Self Delegation: %v Commission: %s`, v.OperatorAddr, bechConsPubKey, v.Jailed, sdk.BondStatusToString(v.Status), v.Tokens, v.DelegatorShares, v.Description, - v.UnbondingHeight, v.UnbondingCompletionTime, v.Commission) + v.UnbondingHeight, v.UnbondingCompletionTime, v.MinSelfDelegation, v.Commission) } // this is a helper struct used for JSON de- and encoding only type bechValidator struct { - OperatorAddr sdk.ValAddress `json:"operator_address"` // the bech32 address of the validator's operator - ConsPubKey string `json:"consensus_pubkey"` // the bech32 consensus public key of the validator - Jailed bool `json:"jailed"` // has the validator been jailed from bonded status? - Status sdk.BondStatus `json:"status"` // validator status (bonded/unbonding/unbonded) - Tokens sdk.Int `json:"tokens"` // delegated tokens (incl. self-delegation) - DelegatorShares sdk.Dec `json:"delegator_shares"` // total shares issued to a validator's delegators - Description Description `json:"description"` // description terms for the validator - UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding - UnbondingCompletionTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding - Commission Commission `json:"commission"` // commission parameters + OperatorAddr sdk.ValAddress `json:"operator_address"` // the bech32 address of the validator's operator + ConsPubKey string `json:"consensus_pubkey"` // the bech32 consensus public key of the validator + Jailed bool `json:"jailed"` // has the validator been jailed from bonded status? + Status sdk.BondStatus `json:"status"` // validator status (bonded/unbonding/unbonded) + Tokens sdk.Int `json:"tokens"` // delegated tokens (incl. self-delegation) + DelegatorShares sdk.Dec `json:"delegator_shares"` // total shares issued to a validator's delegators + Description Description `json:"description"` // description terms for the validator + UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding + UnbondingCompletionTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding + Commission Commission `json:"commission"` // commission parameters + MinSelfDelegation sdk.Int `json:"min_self_delegation"` // minimum self delegation } // MarshalJSON marshals the validator to JSON using Bech32 @@ -143,6 +147,7 @@ func (v Validator) MarshalJSON() ([]byte, error) { Description: v.Description, UnbondingHeight: v.UnbondingHeight, UnbondingCompletionTime: v.UnbondingCompletionTime, + MinSelfDelegation: v.MinSelfDelegation, Commission: v.Commission, }) } @@ -168,6 +173,7 @@ func (v *Validator) UnmarshalJSON(data []byte) error { UnbondingHeight: bv.UnbondingHeight, UnbondingCompletionTime: bv.UnbondingCompletionTime, Commission: bv.Commission, + MinSelfDelegation: bv.MinSelfDelegation, } return nil } @@ -444,5 +450,6 @@ func (v Validator) GetTokens() sdk.Int { return v.Tokens } func (v Validator) GetBondedTokens() sdk.Int { return v.BondedTokens() } func (v Validator) GetTendermintPower() int64 { return v.TendermintPower() } func (v Validator) GetCommission() sdk.Dec { return v.Commission.Rate } +func (v Validator) GetMinSelfDelegation() sdk.Int { return v.MinSelfDelegation } func (v Validator) GetDelegatorShares() sdk.Dec { return v.DelegatorShares } func (v Validator) GetDelegatorShareExRate() sdk.Dec { return v.DelegatorShareExRate() }