Merge PR #2365: Validator Commission Model

* Update validator commission fields

* Remove CommissionChangeToday and update to use CommissionChangeTime

* Implement commission as a first class citizen type

* Implement stringer for Comission

* Move commission type and logic to new  file

* Add new commission errors

* Add commission to create validator message

* Implement and call UpdateValidatorCommission

* Update godoc for UpdateValidatorCommission

* Add Abs to the decimal type

* Implement new SetValidatorCommission

* Update decimal short godocs

* Move set initial commission logic

* Move initial commission validation to Commission type

* Update initial validator commission logic and unit tests

* Remove commission update time from struct and move to validator

* Update validator create handler tests

* Implement commission logic for CLI

* Fix make lint failure

* Fix make cover failure

* Update edit validator logic to handle new commission rate

* Fix lint and cover

* Update create/edit validator simulation to include commission params

* Update MsgEditValidator godoc

* Update pending log

* Update staking tx docs

* Fix CLI create validator test

* Update variables names for commission  strings

* Merge UpdateTime into Commission type

* Update create-validator usage in docs

* Update more docs with examples

* More doc updates
This commit is contained in:
Alexander Bezobchuk 2018-09-24 22:23:58 +00:00 committed by Rigel
parent 8774adc7de
commit 9dafa3252d
25 changed files with 728 additions and 282 deletions

View File

@ -76,6 +76,8 @@ FEATURES
* [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add --generate-only flag to build an unsigned transaction and write it to STDOUT.
* [\#1953](https://github.com/cosmos/cosmos-sdk/issues/1953) New `sign` command to sign transactions generated with the --generate-only flag.
* [\#1954](https://github.com/cosmos/cosmos-sdk/issues/1954) New `broadcast` command to broadcast transactions generated offline and signed with the `sign` command.
* [stake][cli] [\#1672](https://github.com/cosmos/cosmos-sdk/issues/1672) Introduced
new commission flags for validator commands `create-validator` and `edit-validator`.
* Gaia
* [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address`
@ -88,6 +90,8 @@ FEATURES
* [simulation] [\#1924](https://github.com/cosmos/cosmos-sdk/issues/1924) allow operations to specify future operations
* [simulation] [\#1924](https://github.com/cosmos/cosmos-sdk/issues/1924) Add benchmarking capabilities, with makefile commands "test_sim_gaia_benchmark, test_sim_gaia_profile"
* [simulation] [\#2349](https://github.com/cosmos/cosmos-sdk/issues/2349) Add time-based future scheduled operations to simulator
* [x/stake] [\#1672](https://github.com/cosmos/cosmos-sdk/issues/1672) Implement
basis for the validator commission model.
* Tendermint

View File

@ -238,6 +238,9 @@ func TestGaiaCLICreateValidator(t *testing.T) {
cvStr += fmt.Sprintf(" --pubkey=%s", barCeshPubKey)
cvStr += fmt.Sprintf(" --amount=%v", "2steak")
cvStr += fmt.Sprintf(" --moniker=%v", "bar-vally")
cvStr += fmt.Sprintf(" --commission-rate=%v", "0.05")
cvStr += fmt.Sprintf(" --commission-max-rate=%v", "0.20")
cvStr += fmt.Sprintf(" --commission-max-change-rate=%v", "0.10")
initialPool.BondedTokens = initialPool.BondedTokens.Add(sdk.NewDec(1))

View File

@ -1,86 +1,107 @@
## Transaction Overview
# Transaction Overview
In this section we describe the processing of the transactions and the
corresponding updates to the state. Transactions:
- TxCreateValidator
- TxEditValidator
- TxDelegation
- TxStartUnbonding
- TxCompleteUnbonding
- TxRedelegate
- TxCompleteRedelegation
* TxCreateValidator
* TxEditValidator
* TxDelegation
* TxStartUnbonding
* TxCompleteUnbonding
* TxRedelegate
* TxCompleteRedelegation
Other important state changes:
- Update Validators
* Update Validators
Other notes:
- `tx` denotes a reference to the transaction being processed
- `sender` denotes the address of the sender of the transaction
- `getXxx`, `setXxx`, and `removeXxx` functions are used to retrieve and
modify objects from the store
- `sdk.Dec` refers to a decimal type specified by the SDK.
### TxCreateValidator
* `tx` denotes a reference to the transaction being processed
* `sender` denotes the address of the sender of the transaction
* `getXxx`, `setXxx`, and `removeXxx` functions are used to retrieve and
modify objects from the store
* `sdk.Dec` refers to a decimal type specified by the SDK.
- triggers: `distribution.CreateValidatorDistribution`
## TxCreateValidator
* triggers: `distribution.CreateValidatorDistribution`
A validator is created using the `TxCreateValidator` transaction.
```golang
type TxCreateValidator struct {
Operator sdk.Address
ConsensusPubKey crypto.PubKey
GovernancePubKey crypto.PubKey
SelfDelegation coin.Coin
Description Description
Commission Commission
Description Description
Commission sdk.Dec
CommissionMax sdk.Dec
CommissionMaxChange sdk.Dec
DelegatorAddr sdk.AccAddress
ValidatorAddr sdk.ValAddress
PubKey crypto.PubKey
Delegation sdk.Coin
}
createValidator(tx TxCreateValidator):
validator = getValidator(tx.Operator)
if validator != nil return // only one validator per address
ok := validatorExists(tx.ValidatorAddr)
if ok return err // only one validator per address
validator = NewValidator(operatorAddr, ConsensusPubKey, GovernancePubKey, Description)
init validator poolShares, delegatorShares set to 0
init validator commision fields from tx
validator.PoolShares = 0
ok := validatorByPubKeyExists(tx.PubKey)
if ok return err // only one validator per public key
err := validateDenom(tx.Delegation.Denom)
if err != nil return err // denomination must be valid
validator := NewValidator(tx.ValidatorAddr, tx.PubKey, tx.Description)
err := setInitialCommission(validator, tx.Commission, blockTime)
if err != nil return err // must be able to set initial commission correctly
// set the validator and public key
setValidator(validator)
setValidatorByPubKeyIndex(validator)
txDelegate = TxDelegate(tx.Operator, tx.Operator, tx.SelfDelegation)
delegate(txDelegate, validator) // see delegate function in [TxDelegate](TxDelegate)
return
// delegate coins from tx.DelegatorAddr to the validator
err := delegate(tx.DelegatorAddr, tx.Delegation, validator)
if err != nil return err // must be able to set delegation correctly
tags := createTags(tx)
return tags
```
### TxEditValidator
## TxEditValidator
If either the `Description` (excluding `DateBonded` which is constant),
`Commission`, or the `GovernancePubKey` need to be updated, the
`TxEditCandidacy` transaction should be sent from the operator account:
If either the `Description`, `Commission`, or the `ValidatorAddr` need to be
updated, the `TxEditCandidacy` transaction should be sent from the operator
account:
```golang
type TxEditCandidacy struct {
GovernancePubKey crypto.PubKey
Commission sdk.Dec
Description Description
Description Description
ValidatorAddr sdk.ValAddress
CommissionRate sdk.Dec
}
editCandidacy(tx TxEditCandidacy):
validator = getValidator(tx.ValidatorAddr)
validator, ok := getValidator(tx.ValidatorAddr)
if !ok return err // validator must exist
if tx.Commission > CommissionMax || tx.Commission < 0 then fail
if rateChange(tx.Commission) > CommissionMaxChange then fail
validator.Commission = tx.Commission
// Attempt to update the validator's description. The description provided
// must be valid.
description, err := updateDescription(validator, tx.Description)
if err != nil return err
if tx.GovernancePubKey != nil validator.GovernancePubKey = tx.GovernancePubKey
if tx.Description != nil validator.Description = tx.Description
// a validator is not required to update it's commission rate
if tx.CommissionRate != nil {
// Attempt to update a validator's commission rate. The rate provided
// must be valid. It's rate can only be updated once a day.
err := updateValidatorCommission(validator, tx.CommissionRate)
if err != nil return err
}
setValidator(store, validator)
return
// set the validator and public key
setValidator(validator)
tags := createTags(tx)
return tags
```
### TxDelegate

View File

@ -35,9 +35,16 @@ gaiacli stake create-validator \
--address-validator=<account_cosmosval>
--moniker="choose a moniker" \
--chain-id=<chain_id> \
--name=<key_name>
--name=<key_name> \
--commission-rate="0.10" \
--commission-max-rate="0.20" \
--commission-max-change-rate="0.01"
```
__Note__: When specifying commission parameters, the `commission-max-change-rate`
is used to measure % _point_ change over the `commission-rate`. E.g. 1% to 2% is
a 100% rate increase, but only 1 percentage point.
### Edit Validator Description
You can edit your validator's public description. This info is to identify your validator, and will be relied on by delegators to decide which validators to stake to. Make sure to provide input for every flag below, otherwise the field will default to empty (`--moniker` defaults to the machine name).
@ -52,9 +59,17 @@ gaiacli stake edit-validator
--identity=6A0D65E29A4CBC8E
--details="To infinity and beyond!"
--chain-id=<chain_id> \
--name=<key_name>
--name=<key_name> \
--commission-rate="0.10"
```
__Note__: The `commission-rate` value must adhere to the following invariants:
- Must be between 0 and the validator's `commission-max-rate`
- Must not exceed the validator's `commission-max-change-rate` which is maximum
% point change rate **per day**. In other words, a validator can only change
its commission once per day and within `commission-max-change-rate` bounds.
### View Validator Description
View the validator's information with this command:

View File

@ -174,13 +174,15 @@ func NewDecFromStr(str string) (d Dec, err Error) {
//______________________________________________________________________________________________
//nolint
func (d Dec) IsZero() bool { return (d.Int).Sign() == 0 } // Is equal to zero
func (d Dec) Equal(d2 Dec) bool { return (d.Int).Cmp(d2.Int) == 0 }
func (d Dec) IsNil() bool { return d.Int == nil } // is decimal nil
func (d Dec) IsZero() bool { return (d.Int).Sign() == 0 } // is equal to zero
func (d Dec) Equal(d2 Dec) bool { return (d.Int).Cmp(d2.Int) == 0 } // equal decimals
func (d Dec) GT(d2 Dec) bool { return (d.Int).Cmp(d2.Int) > 0 } // greater than
func (d Dec) GTE(d2 Dec) bool { return (d.Int).Cmp(d2.Int) >= 0 } // greater than or equal
func (d Dec) LT(d2 Dec) bool { return (d.Int).Cmp(d2.Int) < 0 } // less than
func (d Dec) LTE(d2 Dec) bool { return (d.Int).Cmp(d2.Int) <= 0 } // less than or equal
func (d Dec) Neg() Dec { return Dec{new(big.Int).Neg(d.Int)} } // reverse the decimal sign
func (d Dec) Abs() Dec { return Dec{new(big.Int).Abs(d.Int)} } // absolute value
// addition
func (d Dec) Add(d2 Dec) Dec {

View File

@ -15,13 +15,19 @@ import (
var (
pubkeys = []crypto.PubKey{ed25519.GenPrivKey().PubKey(), ed25519.GenPrivKey().PubKey(), ed25519.GenPrivKey().PubKey()}
testDescription = stake.NewDescription("T", "E", "S", "T")
testCommissionMsg = stake.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec())
)
func createValidators(t *testing.T, stakeHandler sdk.Handler, ctx sdk.Context, addrs []sdk.ValAddress, coinAmt []int64) {
require.True(t, len(addrs) <= len(pubkeys), "Not enough pubkeys specified at top of file.")
dummyDescription := stake.NewDescription("T", "E", "S", "T")
for i := 0; i < len(addrs); i++ {
valCreateMsg := stake.NewMsgCreateValidator(addrs[i], pubkeys[i], sdk.NewInt64Coin("steak", coinAmt[i]), dummyDescription)
valCreateMsg := stake.NewMsgCreateValidator(
addrs[i], pubkeys[i], sdk.NewInt64Coin("steak", coinAmt[i]), testDescription, testCommissionMsg,
)
res := stakeHandler(ctx, valCreateMsg)
require.True(t, res.IsOK())
}
@ -378,20 +384,18 @@ func TestTallyDelgatorMultipleInherit(t *testing.T) {
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(
sdk.ValAddress(addrs[0]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 25), dummyDescription,
sdk.ValAddress(addrs[0]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 25), testDescription, testCommissionMsg,
)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(
sdk.ValAddress(addrs[1]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 6), dummyDescription,
sdk.ValAddress(addrs[1]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 6), testDescription, testCommissionMsg,
)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(
sdk.ValAddress(addrs[2]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 7), dummyDescription,
sdk.ValAddress(addrs[2]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 7), testDescription, testCommissionMsg,
)
stakeHandler(ctx, val3CreateMsg)

View File

@ -99,9 +99,12 @@ func TestSlashingMsgs(t *testing.T) {
}
accs := []auth.Account{acc1}
mock.SetGenesis(mapp, accs)
description := stake.NewDescription("foo_moniker", "", "", "")
commission := stake.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec())
createValidatorMsg := stake.NewMsgCreateValidator(
sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description,
sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, commission,
)
mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{createValidatorMsg}, []int64{0}, []int64{0}, true, true, priv1)
mock.CheckBalance(t, mapp, addr1, sdk.Coins{genCoin.Minus(bondCoin)})

View File

@ -103,12 +103,14 @@ func testAddr(addr string) sdk.AccAddress {
}
func newTestMsgCreateValidator(address sdk.ValAddress, pubKey crypto.PubKey, amt sdk.Int) stake.MsgCreateValidator {
commission := stake.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec())
return stake.MsgCreateValidator{
Description: stake.Description{},
Commission: commission,
DelegatorAddr: sdk.AccAddress(address),
ValidatorAddr: address,
PubKey: pubKey,
Delegation: sdk.Coin{"steak", amt},
Delegation: sdk.NewCoin("steak", amt),
}
}
@ -116,6 +118,6 @@ func newTestMsgDelegate(delAddr sdk.AccAddress, valAddr sdk.ValAddress, delAmoun
return stake.MsgDelegate{
DelegatorAddr: delAddr,
ValidatorAddr: valAddr,
Delegation: sdk.Coin{"steak", delAmount},
Delegation: sdk.NewCoin("steak", delAmount),
}
}

View File

@ -21,10 +21,12 @@ var (
priv4 = ed25519.GenPrivKey()
addr4 = sdk.AccAddress(priv4.PubKey().Address())
coins = sdk.Coins{sdk.NewCoin("foocoin", sdk.NewInt(10))}
fee = auth.StdFee{
sdk.Coins{sdk.NewCoin("foocoin", sdk.NewInt(0))},
fee = auth.NewStdFee(
100000,
}
sdk.Coins{sdk.NewCoin("foocoin", sdk.NewInt(0))}...,
)
commissionMsg = NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec())
)
// getMockApp returns an initialized mock application for this module.
@ -129,7 +131,7 @@ func TestStakeMsgs(t *testing.T) {
// create validator
description := NewDescription("foo_moniker", "", "", "")
createValidatorMsg := NewMsgCreateValidator(
sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description,
sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, commissionMsg,
)
mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{createValidatorMsg}, []int64{0}, []int64{0}, true, true, priv1)
@ -143,7 +145,7 @@ func TestStakeMsgs(t *testing.T) {
// addr1 create validator on behalf of addr2
createValidatorMsgOnBehalfOf := NewMsgCreateValidatorOnBehalfOf(
addr1, sdk.ValAddress(addr2), priv2.PubKey(), bondCoin, description,
addr1, sdk.ValAddress(addr2), priv2.PubKey(), bondCoin, description, commissionMsg,
)
mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{createValidatorMsgOnBehalfOf}, []int64{0, 1}, []int64{1, 0}, true, true, priv1, priv2)
@ -160,7 +162,7 @@ func TestStakeMsgs(t *testing.T) {
// edit the validator
description = NewDescription("bar_moniker", "", "", "")
editValidatorMsg := NewMsgEditValidator(sdk.ValAddress(addr1), description)
editValidatorMsg := NewMsgEditValidator(sdk.ValAddress(addr1), description, nil)
mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{editValidatorMsg}, []int64{0}, []int64{2}, true, true, priv1)
validator = checkValidator(t, mApp, keeper, sdk.ValAddress(addr1), true)

View File

@ -21,6 +21,10 @@ const (
FlagIdentity = "identity"
FlagWebsite = "website"
FlagDetails = "details"
FlagCommissionRate = "commission-rate"
FlagCommissionMaxRate = "commission-max-rate"
FlagCommissionMaxChangeRate = "commission-max-change-rate"
)
// common flagsets to add to various functions
@ -29,6 +33,8 @@ var (
fsAmount = flag.NewFlagSet("", flag.ContinueOnError)
fsShares = flag.NewFlagSet("", flag.ContinueOnError)
fsDescriptionCreate = flag.NewFlagSet("", flag.ContinueOnError)
fsCommissionCreate = flag.NewFlagSet("", flag.ContinueOnError)
fsCommissionUpdate = flag.NewFlagSet("", flag.ContinueOnError)
fsDescriptionEdit = flag.NewFlagSet("", flag.ContinueOnError)
fsValidator = flag.NewFlagSet("", flag.ContinueOnError)
fsDelegator = flag.NewFlagSet("", flag.ContinueOnError)
@ -44,6 +50,10 @@ func init() {
fsDescriptionCreate.String(FlagIdentity, "", "optional identity signature (ex. UPort or Keybase)")
fsDescriptionCreate.String(FlagWebsite, "", "optional website")
fsDescriptionCreate.String(FlagDetails, "", "optional details")
fsCommissionUpdate.String(FlagCommissionRate, "", "The new commission rate percentage")
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)")
fsDescriptionEdit.String(FlagMoniker, types.DoNotModifyDesc, "validator name")
fsDescriptionEdit.String(FlagIdentity, types.DoNotModifyDesc, "optional identity signature (ex. UPort or Keybase)")
fsDescriptionEdit.String(FlagWebsite, types.DoNotModifyDesc, "optional website")

View File

@ -12,9 +12,7 @@ import (
authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli"
authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder"
"github.com/cosmos/cosmos-sdk/x/stake"
"github.com/cosmos/cosmos-sdk/x/stake/types"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -66,6 +64,15 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command {
Details: viper.GetString(FlagDetails),
}
// get the initial validator commission parameters
rateStr := viper.GetString(FlagCommissionRate)
maxRateStr := viper.GetString(FlagCommissionMaxRate)
maxChangeRateStr := viper.GetString(FlagCommissionMaxChangeRate)
commissionMsg, err := buildCommissionMsg(rateStr, maxRateStr, maxChangeRateStr)
if err != nil {
return err
}
var msg sdk.Msg
if viper.GetString(FlagAddressDelegator) != "" {
delAddr, err := sdk.AccAddressFromBech32(viper.GetString(FlagAddressDelegator))
@ -73,13 +80,19 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command {
return err
}
msg = stake.NewMsgCreateValidatorOnBehalfOf(delAddr, sdk.ValAddress(valAddr), pk, amount, description)
msg = stake.NewMsgCreateValidatorOnBehalfOf(
delAddr, sdk.ValAddress(valAddr), pk, amount, description, commissionMsg,
)
} else {
msg = stake.NewMsgCreateValidator(sdk.ValAddress(valAddr), pk, amount, description)
msg = stake.NewMsgCreateValidator(
sdk.ValAddress(valAddr), pk, amount, description, commissionMsg,
)
}
if cliCtx.GenerateOnly {
return utils.PrintUnsignedStdTx(txBldr, cliCtx, []sdk.Msg{msg})
}
// build and sign the transaction, then broadcast to Tendermint
return utils.SendTx(txBldr, cliCtx, []sdk.Msg{msg})
},
@ -88,6 +101,7 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command {
cmd.Flags().AddFlagSet(fsPk)
cmd.Flags().AddFlagSet(fsAmount)
cmd.Flags().AddFlagSet(fsDescriptionCreate)
cmd.Flags().AddFlagSet(fsCommissionCreate)
cmd.Flags().AddFlagSet(fsDelegator)
return cmd
@ -117,17 +131,31 @@ func GetCmdEditValidator(cdc *codec.Codec) *cobra.Command {
Details: viper.GetString(FlagDetails),
}
msg := stake.NewMsgEditValidator(sdk.ValAddress(valAddr), description)
var newRate *sdk.Dec
commissionRate := viper.GetString(FlagCommissionRate)
if commissionRate != "" {
rate, err := sdk.NewDecFromStr(commissionRate)
if err != nil {
return fmt.Errorf("invalid new commission rate: %v", err)
}
newRate = &rate
}
msg := stake.NewMsgEditValidator(sdk.ValAddress(valAddr), description, newRate)
if cliCtx.GenerateOnly {
return utils.PrintUnsignedStdTx(txBldr, cliCtx, []sdk.Msg{msg})
}
// build and sign the transaction, then broadcast to Tendermint
return utils.SendTx(txBldr, cliCtx, []sdk.Msg{msg})
},
}
cmd.Flags().AddFlagSet(fsDescriptionEdit)
cmd.Flags().AddFlagSet(fsCommissionUpdate)
return cmd
}
@ -247,54 +275,6 @@ func GetCmdBeginRedelegate(storeName string, cdc *codec.Codec) *cobra.Command {
return cmd
}
// nolint: gocyclo
// TODO: Make this pass gocyclo linting
func getShares(
storeName string, cdc *codec.Codec, sharesAmountStr,
sharesPercentStr string, delAddr sdk.AccAddress, valAddr sdk.ValAddress,
) (sharesAmount sdk.Dec, err error) {
switch {
case sharesAmountStr != "" && sharesPercentStr != "":
return sharesAmount, errors.Errorf("can either specify the amount OR the percent of the shares, not both")
case sharesAmountStr == "" && sharesPercentStr == "":
return sharesAmount, errors.Errorf("can either specify the amount OR the percent of the shares, not both")
case sharesAmountStr != "":
sharesAmount, err = sdk.NewDecFromStr(sharesAmountStr)
if err != nil {
return sharesAmount, err
}
if !sharesAmount.GT(sdk.ZeroDec()) {
return sharesAmount, errors.Errorf("shares amount must be positive number (ex. 123, 1.23456789)")
}
case sharesPercentStr != "":
var sharesPercent sdk.Dec
sharesPercent, err = sdk.NewDecFromStr(sharesPercentStr)
if err != nil {
return sharesAmount, err
}
if !sharesPercent.GT(sdk.ZeroDec()) || !sharesPercent.LTE(sdk.OneDec()) {
return sharesAmount, errors.Errorf("shares percent must be >0 and <=1 (ex. 0.01, 0.75, 1)")
}
// make a query to get the existing delegation shares
key := stake.GetDelegationKey(delAddr, valAddr)
cliCtx := context.NewCLIContext().
WithCodec(cdc).
WithAccountDecoder(authcmd.GetAccountDecoder(cdc))
resQuery, err := cliCtx.QueryStore(key, storeName)
if err != nil {
return sharesAmount, errors.Errorf("cannot find delegation to determine percent Error: %v", err)
}
delegation, err := types.UnmarshalDelegation(cdc, key, resQuery)
if err != nil {
return sdk.ZeroDec(), err
}
sharesAmount = sharesPercent.Mul(delegation.Shares)
}
return
}
// GetCmdCompleteRedelegate implements the complete redelegation command.
func GetCmdCompleteRedelegate(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{

View File

@ -0,0 +1,88 @@
package cli
import (
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli"
"github.com/cosmos/cosmos-sdk/x/stake"
"github.com/cosmos/cosmos-sdk/x/stake/types"
"github.com/pkg/errors"
)
func getShares(
storeName string, cdc *codec.Codec, sharesAmountStr,
sharesPercentStr string, delAddr sdk.AccAddress, valAddr sdk.ValAddress,
) (sharesAmount sdk.Dec, err error) {
switch {
case sharesAmountStr != "" && sharesPercentStr != "":
return sharesAmount, errors.Errorf("can either specify the amount OR the percent of the shares, not both")
case sharesAmountStr == "" && sharesPercentStr == "":
return sharesAmount, errors.Errorf("can either specify the amount OR the percent of the shares, not both")
case sharesAmountStr != "":
sharesAmount, err = sdk.NewDecFromStr(sharesAmountStr)
if err != nil {
return sharesAmount, err
}
if !sharesAmount.GT(sdk.ZeroDec()) {
return sharesAmount, errors.Errorf("shares amount must be positive number (ex. 123, 1.23456789)")
}
case sharesPercentStr != "":
var sharesPercent sdk.Dec
sharesPercent, err = sdk.NewDecFromStr(sharesPercentStr)
if err != nil {
return sharesAmount, err
}
if !sharesPercent.GT(sdk.ZeroDec()) || !sharesPercent.LTE(sdk.OneDec()) {
return sharesAmount, errors.Errorf("shares percent must be >0 and <=1 (ex. 0.01, 0.75, 1)")
}
// make a query to get the existing delegation shares
key := stake.GetDelegationKey(delAddr, valAddr)
cliCtx := context.NewCLIContext().
WithCodec(cdc).
WithAccountDecoder(authcmd.GetAccountDecoder(cdc))
resQuery, err := cliCtx.QueryStore(key, storeName)
if err != nil {
return sharesAmount, errors.Errorf("cannot find delegation to determine percent Error: %v", err)
}
delegation, err := types.UnmarshalDelegation(cdc, key, resQuery)
if err != nil {
return sdk.ZeroDec(), err
}
sharesAmount = sharesPercent.Mul(delegation.Shares)
}
return
}
func buildCommissionMsg(rateStr, maxRateStr, maxChangeRateStr string) (commission types.CommissionMsg, err error) {
if rateStr == "" || maxRateStr == "" || maxChangeRateStr == "" {
return commission, errors.Errorf("must specify all validator commission parameters")
}
rate, err := sdk.NewDecFromStr(rateStr)
if err != nil {
return commission, err
}
maxRate, err := sdk.NewDecFromStr(maxRateStr)
if err != nil {
return commission, err
}
maxChangeRate, err := sdk.NewDecFromStr(maxChangeRateStr)
if err != nil {
return commission, err
}
commission = types.NewCommissionMsg(rate, maxRate, maxChangeRate)
return commission, nil
}

View File

@ -3,11 +3,12 @@ package rest
import (
"fmt"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/stake/tags"
rpcclient "github.com/tendermint/tendermint/rpc/client"
"github.com/cosmos/cosmos-sdk/client/context"
)
// contains checks if the a given query contains one of the tx types

View File

@ -62,27 +62,38 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) (ValidatorUpdates []abci.Valid
// now we just perform action and save
func handleMsgCreateValidator(ctx sdk.Context, msg types.MsgCreateValidator, k keeper.Keeper) sdk.Result {
// check to see if the pubkey or sender has been registered before
_, found := k.GetValidator(ctx, msg.ValidatorAddr)
if found {
return ErrValidatorOwnerExists(k.Codespace()).Result()
}
_, found = k.GetValidatorByPubKey(ctx, msg.PubKey)
if found {
return ErrValidatorPubKeyExists(k.Codespace()).Result()
}
if msg.Delegation.Denom != k.GetParams(ctx).BondDenom {
return ErrBadDenom(k.Codespace()).Result()
}
validator := NewValidator(msg.ValidatorAddr, msg.PubKey, msg.Description)
commission := NewCommissionWithTime(
msg.Commission.Rate, msg.Commission.MaxChangeRate,
msg.Commission.MaxChangeRate, ctx.BlockHeader().Time,
)
validator, err := validator.SetInitialCommission(commission)
if err != nil {
return err.Result()
}
k.SetValidator(ctx, validator)
k.SetValidatorByPubKeyIndex(ctx, validator)
// move coins from the msg.Address account to a (self-delegation) delegator account
// the validator account and global shares are updated within here
_, err := k.Delegate(ctx, msg.DelegatorAddr, msg.Delegation, validator, true)
_, err = k.Delegate(ctx, msg.DelegatorAddr, msg.Delegation, validator, true)
if err != nil {
return err.Result()
}
@ -93,13 +104,13 @@ func handleMsgCreateValidator(ctx sdk.Context, msg types.MsgCreateValidator, k k
tags.Moniker, []byte(msg.Description.Moniker),
tags.Identity, []byte(msg.Description.Identity),
)
return sdk.Result{
Tags: tags,
}
}
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)
if !found {
@ -111,17 +122,26 @@ func handleMsgEditValidator(ctx sdk.Context, msg types.MsgEditValidator, k keepe
if err != nil {
return err.Result()
}
validator.Description = description
if msg.CommissionRate != nil {
if err := k.UpdateValidatorCommission(ctx, validator, *msg.CommissionRate); err != nil {
return err.Result()
}
}
// We don't need to run through all the power update logic within k.UpdateValidator
// We just need to override the entry in state, since only the description has changed.
k.SetValidator(ctx, validator)
tags := sdk.NewTags(
tags.Action, tags.ActionEditValidator,
tags.DstValidator, []byte(msg.ValidatorAddr.String()),
tags.Moniker, []byte(description.Moniker),
tags.Identity, []byte(description.Identity),
)
return sdk.Result{
Tags: tags,
}

View File

@ -17,7 +17,9 @@ import (
//______________________________________________________________________
func newTestMsgCreateValidator(address sdk.ValAddress, pubKey crypto.PubKey, amt int64) MsgCreateValidator {
return types.NewMsgCreateValidator(address, pubKey, sdk.NewCoin("steak", sdk.NewInt(amt)), Description{})
return types.NewMsgCreateValidator(
address, pubKey, sdk.NewCoin("steak", sdk.NewInt(amt)), Description{}, commissionMsg,
)
}
func newTestMsgDelegate(delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt int64) MsgDelegate {
@ -31,6 +33,7 @@ func newTestMsgDelegate(delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt int6
func newTestMsgCreateValidatorOnBehalfOf(delAddr sdk.AccAddress, valAddr sdk.ValAddress, valPubKey crypto.PubKey, amt int64) MsgCreateValidator {
return MsgCreateValidator{
Description: Description{},
Commission: commissionMsg,
DelegatorAddr: delAddr,
ValidatorAddr: valAddr,
PubKey: valPubKey,

View File

@ -693,6 +693,23 @@ func (k Keeper) RemoveValidator(ctx sdk.Context, address sdk.ValAddress) {
tstore.Set(GetTendermintUpdatesTKey(address), bz)
}
// UpdateValidatorCommission attempts to update a validator's commission rate.
// An error is returned if the new commission rate is invalid.
func (k Keeper) UpdateValidatorCommission(ctx sdk.Context, validator types.Validator, newRate sdk.Dec) sdk.Error {
commission := validator.Commission
blockTime := ctx.BlockHeader().Time
if err := commission.ValidateNewRate(newRate, blockTime); err != nil {
return err
}
validator.Commission.Rate = newRate
validator.Commission.UpdateTime = blockTime
k.SetValidator(ctx, validator)
return nil
}
//__________________________________________________________________________
// get the current validator on the cliff

View File

@ -3,9 +3,11 @@ package keeper
import (
"fmt"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/stake/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -1043,3 +1045,55 @@ func TestGetValidTendermintUpdatesBondTransition(t *testing.T) {
clearTendermintUpdates(ctx, keeper)
require.Equal(t, 0, len(keeper.GetValidTendermintUpdates(ctx)))
}
func TestUpdateValidatorCommission(t *testing.T) {
ctx, _, keeper := CreateTestInput(t, false, 1000)
ctx = ctx.WithBlockHeader(abci.Header{Time: time.Now().UTC()})
commission1 := types.NewCommissionWithTime(
sdk.NewDecWithPrec(1, 1), sdk.NewDecWithPrec(3, 1),
sdk.NewDecWithPrec(1, 1), time.Now().UTC().Add(time.Duration(-1)*time.Hour),
)
commission2 := types.NewCommission(sdk.NewDecWithPrec(1, 1), sdk.NewDecWithPrec(3, 1), sdk.NewDecWithPrec(1, 1))
val1 := types.NewValidator(addrVals[0], PKs[0], types.Description{})
val2 := types.NewValidator(addrVals[1], PKs[1], types.Description{})
val1, _ = val1.SetInitialCommission(commission1)
val2, _ = val2.SetInitialCommission(commission2)
testCases := []struct {
validator types.Validator
newRate sdk.Dec
expectedErr bool
}{
{val1, sdk.ZeroDec(), true},
{val2, sdk.NewDecWithPrec(-1, 1), true},
{val2, sdk.NewDecWithPrec(4, 1), true},
{val2, sdk.NewDecWithPrec(3, 1), true},
{val2, sdk.NewDecWithPrec(2, 1), false},
}
for i, tc := range testCases {
err := keeper.UpdateValidatorCommission(ctx, tc.validator, tc.newRate)
if tc.expectedErr {
require.Error(t, err, "expected error for test case #%d with rate: %s", i, tc.newRate)
} else {
val, found := keeper.GetValidator(ctx, tc.validator.OperatorAddr)
require.True(t, found,
"expected to find validator for test case #%d with rate: %s", i, tc.newRate,
)
require.NoError(t, err,
"unexpected error for test case #%d with rate: %s", i, tc.newRate,
)
require.Equal(t, tc.newRate, val.Commission.Rate,
"expected new validator commission rate for test case #%d with rate: %s", i, tc.newRate,
)
require.Equal(t, ctx.BlockHeader().Time, val.Commission.UpdateTime,
"expected new validator commission update time for test case #%d with rate: %s", i, tc.newRate,
)
}
}
}

View File

@ -23,32 +23,48 @@ func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation
description := stake.Description{
Moniker: simulation.RandStringOfLength(r, 10),
}
maxCommission := sdk.NewInt(10)
commission := stake.NewCommissionMsg(
sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1),
sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1),
sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1),
)
key := simulation.RandomKey(r, keys)
pubkey := key.PubKey()
address := sdk.ValAddress(pubkey.Address())
amount := m.GetAccount(ctx, sdk.AccAddress(address)).GetCoins().AmountOf(denom)
if amount.GT(sdk.ZeroInt()) {
amount = simulation.RandomAmount(r, amount)
}
if amount.Equal(sdk.ZeroInt()) {
return "no-operation", nil, nil
}
msg := stake.MsgCreateValidator{
Description: description,
Commission: commission,
ValidatorAddr: address,
DelegatorAddr: sdk.AccAddress(address),
PubKey: pubkey,
Delegation: sdk.NewCoin(denom, amount),
}
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() {
write()
}
event(fmt.Sprintf("stake/MsgCreateValidator/%v", result.IsOK()))
// require.True(t, result.IsOK(), "expected OK result but instead got %v", result)
action = fmt.Sprintf("TestMsgCreateValidator: ok %v, msg %s", result.IsOK(), msg.GetSignBytes())
return action, nil, nil
@ -66,16 +82,24 @@ func SimulateMsgEditValidator(k stake.Keeper) simulation.Operation {
Website: simulation.RandStringOfLength(r, 10),
Details: simulation.RandStringOfLength(r, 10),
}
maxCommission := sdk.NewInt(10)
newCommissionRate := sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1)
key := simulation.RandomKey(r, keys)
pubkey := key.PubKey()
address := sdk.ValAddress(pubkey.Address())
msg := stake.MsgEditValidator{
Description: description,
ValidatorAddr: address,
Description: description,
ValidatorAddr: address,
CommissionRate: &newCommissionRate,
}
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() {

View File

@ -12,6 +12,7 @@ type (
Keeper = keeper.Keeper
Validator = types.Validator
Description = types.Description
Commission = types.Commission
Delegation = types.Delegation
DelegationSummary = types.DelegationSummary
UnbondingDelegation = types.UnbondingDelegation
@ -64,13 +65,16 @@ var (
GetREDsToValDstIndexKey = keeper.GetREDsToValDstIndexKey
GetREDsByDelToValDstIndexKey = keeper.GetREDsByDelToValDstIndexKey
DefaultParams = types.DefaultParams
InitialPool = types.InitialPool
NewValidator = types.NewValidator
NewDescription = types.NewDescription
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
RegisterCodec = types.RegisterCodec
DefaultParams = types.DefaultParams
InitialPool = types.InitialPool
NewValidator = types.NewValidator
NewDescription = types.NewDescription
NewCommission = types.NewCommission
NewCommissionMsg = types.NewCommissionMsg
NewCommissionWithTime = types.NewCommissionWithTime
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
RegisterCodec = types.RegisterCodec
NewMsgCreateValidator = types.NewMsgCreateValidator
NewMsgCreateValidatorOnBehalfOf = types.NewMsgCreateValidatorOnBehalfOf

128
x/stake/types/commission.go Normal file
View File

@ -0,0 +1,128 @@
package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type (
// Commission defines a commission parameters for a given validator.
Commission struct {
Rate sdk.Dec `json:"rate"` // the commission rate charged to delegators
MaxRate sdk.Dec `json:"max_rate"` // maximum commission rate which validator can ever charge
MaxChangeRate sdk.Dec `json:"max_change_rate"` // maximum daily increase of the validator commission
UpdateTime time.Time `json:"update_time"` // the last time the commission rate was changed
}
// CommissionMsg defines a commission message to be used for creating a
// validator.
CommissionMsg struct {
Rate sdk.Dec `json:"rate"` // the commission rate charged to delegators
MaxRate sdk.Dec `json:"max_rate"` // maximum commission rate which validator can ever charge
MaxChangeRate sdk.Dec `json:"max_change_rate"` // maximum daily increase of the validator commission
}
)
// NewCommissionMsg returns an initialized validator commission message.
func NewCommissionMsg(rate, maxRate, maxChangeRate sdk.Dec) CommissionMsg {
return CommissionMsg{
Rate: rate,
MaxRate: maxRate,
MaxChangeRate: maxChangeRate,
}
}
// NewCommission returns an initialized validator commission.
func NewCommission(rate, maxRate, maxChangeRate sdk.Dec) Commission {
return Commission{
Rate: rate,
MaxRate: maxRate,
MaxChangeRate: maxChangeRate,
UpdateTime: time.Unix(0, 0).UTC(),
}
}
// NewCommission returns an initialized validator commission with a specified
// update time which should be the current block BFT time.
func NewCommissionWithTime(rate, maxRate, maxChangeRate sdk.Dec, updatedAt time.Time) Commission {
return Commission{
Rate: rate,
MaxRate: maxRate,
MaxChangeRate: maxChangeRate,
UpdateTime: updatedAt,
}
}
// Equal checks if the given Commission object is equal to the receiving
// Commission object.
func (c Commission) Equal(c2 Commission) bool {
return c.Rate.Equal(c2.Rate) &&
c.MaxRate.Equal(c2.MaxRate) &&
c.MaxChangeRate.Equal(c2.MaxChangeRate) &&
c.UpdateTime.Equal(c2.UpdateTime)
}
// String implements the Stringer interface for a Commission.
func (c Commission) String() string {
return fmt.Sprintf("rate: %s, maxRate: %s, maxChangeRate: %s, updateTime: %s",
c.Rate, c.MaxRate, c.MaxChangeRate, c.UpdateTime,
)
}
// Validate performs basic sanity validation checks of initial commission
// parameters. If validation fails, an SDK error is returned.
func (c Commission) Validate() sdk.Error {
switch {
case c.MaxRate.LT(sdk.ZeroDec()):
// max rate cannot be negative
return ErrCommissionNegative(DefaultCodespace)
case c.MaxRate.GT(sdk.OneDec()):
// max rate cannot be greater than 100%
return ErrCommissionHuge(DefaultCodespace)
case c.Rate.LT(sdk.ZeroDec()):
// rate cannot be negative
return ErrCommissionNegative(DefaultCodespace)
case c.Rate.GT(c.MaxRate):
// rate cannot be greater than the max rate
return ErrCommissionGTMaxRate(DefaultCodespace)
case c.MaxChangeRate.LT(sdk.ZeroDec()):
// change rate cannot be negative
return ErrCommissionChangeRateNegative(DefaultCodespace)
case c.MaxChangeRate.GT(c.MaxRate):
// change rate cannot be greater than the max rate
return ErrCommissionChangeRateGTMaxRate(DefaultCodespace)
}
return nil
}
// ValidateNewRate performs basic sanity validation checks of a new commission
// rate. If validation fails, an SDK error is returned.
func (c Commission) ValidateNewRate(newRate sdk.Dec, blockTime time.Time) sdk.Error {
switch {
case blockTime.Sub(c.UpdateTime).Hours() < 24:
// new rate cannot be changed more than once within 24 hours
return ErrCommissionUpdateTime(DefaultCodespace)
case newRate.LT(sdk.ZeroDec()):
// new rate cannot be negative
return ErrCommissionNegative(DefaultCodespace)
case newRate.GT(c.MaxRate):
// new rate cannot be greater than the max rate
return ErrCommissionGTMaxRate(DefaultCodespace)
case newRate.Sub(c.Rate).Abs().GT(c.MaxChangeRate):
// new rate % points change cannot be greater than the max change rate
return ErrCommissionGTMaxChangeRate(DefaultCodespace)
}
return nil
}

View File

@ -65,6 +65,26 @@ func ErrCommissionHuge(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidValidator, "commission cannot be more than 100%")
}
func ErrCommissionGTMaxRate(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidValidator, "commission cannot be more than the max rate")
}
func ErrCommissionUpdateTime(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidValidator, "commission cannot be changed more than once in 24h")
}
func ErrCommissionChangeRateNegative(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidValidator, "commission change rate must be positive")
}
func ErrCommissionChangeRateGTMaxRate(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidValidator, "commission change rate cannot be more than the max rate")
}
func ErrCommissionGTMaxChangeRate(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidValidator, "commission cannot be changed more than max change rate")
}
func ErrNilDelegatorAddr(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidInput, "delegator address is nil")
}

View File

@ -20,6 +20,7 @@ var _, _ sdk.Msg = &MsgBeginRedelegate{}, &MsgCompleteRedelegate{}
// MsgCreateValidator - struct for unbonding transactions
type MsgCreateValidator struct {
Description
Commission CommissionMsg
DelegatorAddr sdk.AccAddress `json:"delegator_address"`
ValidatorAddr sdk.ValAddress `json:"validator_address"`
PubKey crypto.PubKey `json:"pubkey"`
@ -28,22 +29,23 @@ type MsgCreateValidator struct {
// 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) MsgCreateValidator {
selfDelegation sdk.Coin, description Description, commission CommissionMsg) MsgCreateValidator {
return NewMsgCreateValidatorOnBehalfOf(
sdk.AccAddress(valAddr), valAddr, pubkey, selfDelegation, description,
sdk.AccAddress(valAddr), valAddr, pubkey, selfDelegation, description, commission,
)
}
// Creates validator msg by delegator address on behalf of validator address
func NewMsgCreateValidatorOnBehalfOf(delAddr sdk.AccAddress, valAddr sdk.ValAddress,
pubkey crypto.PubKey, delegation sdk.Coin, description Description) MsgCreateValidator {
pubkey crypto.PubKey, delegation sdk.Coin, description Description, commission CommissionMsg) MsgCreateValidator {
return MsgCreateValidator{
Description: description,
DelegatorAddr: delAddr,
ValidatorAddr: valAddr,
PubKey: pubkey,
Delegation: delegation,
Commission: commission,
}
}
@ -95,10 +97,13 @@ func (msg MsgCreateValidator) ValidateBasic() sdk.Error {
if !(msg.Delegation.Amount.GT(sdk.ZeroInt())) {
return ErrBadDelegationAmount(DefaultCodespace)
}
empty := Description{}
if msg.Description == empty {
if msg.Description == (Description{}) {
return sdk.NewError(DefaultCodespace, CodeInvalidInput, "description must be included")
}
if msg.Commission == (CommissionMsg{}) {
return sdk.NewError(DefaultCodespace, CodeInvalidInput, "commission must be included")
}
return nil
}
@ -108,12 +113,20 @@ func (msg MsgCreateValidator) ValidateBasic() sdk.Error {
type MsgEditValidator struct {
Description
ValidatorAddr sdk.ValAddress `json:"address"`
// We pass a reference to the new commission rate 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"`
}
func NewMsgEditValidator(valAddr sdk.ValAddress, description Description) MsgEditValidator {
func NewMsgEditValidator(valAddr sdk.ValAddress, description Description, newRate *sdk.Dec) MsgEditValidator {
return MsgEditValidator{
Description: description,
ValidatorAddr: valAddr,
Description: description,
CommissionRate: newRate,
ValidatorAddr: valAddr,
}
}
@ -144,10 +157,11 @@ func (msg MsgEditValidator) ValidateBasic() sdk.Error {
if msg.ValidatorAddr == nil {
return sdk.NewError(DefaultCodespace, CodeInvalidInput, "nil validator address")
}
empty := Description{}
if msg.Description == empty {
if msg.Description == (Description{}) {
return sdk.NewError(DefaultCodespace, CodeInvalidInput, "transaction must include some information to modify")
}
return nil
}

View File

@ -17,26 +17,30 @@ var (
// test ValidateBasic for MsgCreateValidator
func TestMsgCreateValidator(t *testing.T) {
commission1 := NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec())
commission2 := NewCommissionMsg(sdk.NewDec(5), sdk.NewDec(5), sdk.NewDec(5))
tests := []struct {
name, moniker, identity, website, details string
commissionMsg CommissionMsg
validatorAddr sdk.ValAddress
pubkey crypto.PubKey
bond sdk.Coin
expectPass bool
}{
{"basic good", "a", "b", "c", "d", addr1, pk1, coinPos, true},
{"partial description", "", "", "c", "", addr1, pk1, coinPos, true},
{"empty description", "", "", "", "", addr1, pk1, coinPos, false},
{"empty address", "a", "b", "c", "d", emptyAddr, pk1, coinPos, false},
{"empty pubkey", "a", "b", "c", "d", addr1, emptyPubkey, coinPos, true},
{"empty bond", "a", "b", "c", "d", addr1, pk1, coinZero, false},
{"negative bond", "a", "b", "c", "d", addr1, pk1, coinNeg, false},
{"negative bond", "a", "b", "c", "d", addr1, pk1, coinNeg, false},
{"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},
{"negative bond", "a", "b", "c", "d", commission2, addr1, pk1, coinNeg, false},
{"negative bond", "a", "b", "c", "d", commission1, addr1, pk1, coinNeg, false},
}
for _, tc := range tests {
description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details)
msg := NewMsgCreateValidator(tc.validatorAddr, tc.pubkey, tc.bond, description)
msg := NewMsgCreateValidator(tc.validatorAddr, tc.pubkey, tc.bond, description, tc.commissionMsg)
if tc.expectPass {
require.Nil(t, msg.ValidateBasic(), "test: %v", tc.name)
} else {
@ -60,7 +64,9 @@ func TestMsgEditValidator(t *testing.T) {
for _, tc := range tests {
description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details)
msg := NewMsgEditValidator(tc.validatorAddr, description)
newRate := sdk.ZeroDec()
msg := NewMsgEditValidator(tc.validatorAddr, description, &newRate)
if tc.expectPass {
require.Nil(t, msg.ValidateBasic(), "test: %v", tc.name)
} else {
@ -71,28 +77,35 @@ func TestMsgEditValidator(t *testing.T) {
// test ValidateBasic and GetSigners for MsgCreateValidatorOnBehalfOf
func TestMsgCreateValidatorOnBehalfOf(t *testing.T) {
commission1 := NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec())
commission2 := NewCommissionMsg(sdk.NewDec(5), sdk.NewDec(5), sdk.NewDec(5))
tests := []struct {
name, moniker, identity, website, details string
commissionMsg CommissionMsg
delegatorAddr sdk.AccAddress
validatorAddr sdk.ValAddress
validatorPubKey crypto.PubKey
bond sdk.Coin
expectPass bool
}{
{"basic good", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, pk2, coinPos, true},
{"partial description", "", "", "c", "", sdk.AccAddress(addr1), addr2, pk2, coinPos, true},
{"empty description", "", "", "", "", sdk.AccAddress(addr1), addr2, pk2, coinPos, false},
{"empty delegator address", "a", "b", "c", "d", sdk.AccAddress(emptyAddr), addr2, pk2, coinPos, false},
{"empty validator address", "a", "b", "c", "d", sdk.AccAddress(addr1), emptyAddr, pk2, coinPos, false},
{"empty pubkey", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, emptyPubkey, coinPos, true},
{"empty bond", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, pk2, coinZero, false},
{"negative bond", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, pk2, coinNeg, false},
{"negative bond", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, pk2, coinNeg, false},
{"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},
{"negative bond", "a", "b", "c", "d", commission1, sdk.AccAddress(addr1), addr2, pk2, coinNeg, false},
{"negative bond", "a", "b", "c", "d", commission2, sdk.AccAddress(addr1), addr2, pk2, coinNeg, 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)
msg := NewMsgCreateValidatorOnBehalfOf(
tc.delegatorAddr, tc.validatorAddr, tc.validatorPubKey, tc.bond, description, tc.commissionMsg,
)
if tc.expectPass {
require.Nil(t, msg.ValidateBasic(), "test: %v", tc.name)
} else {
@ -100,11 +113,11 @@ func TestMsgCreateValidatorOnBehalfOf(t *testing.T) {
}
}
msg := NewMsgCreateValidator(addr1, pk1, coinPos, Description{})
msg := NewMsgCreateValidator(addr1, pk1, coinPos, Description{}, CommissionMsg{})
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{})
msg = NewMsgCreateValidatorOnBehalfOf(sdk.AccAddress(addr2), addr1, pk1, coinPos, Description{}, CommissionMsg{})
addrs = msg.GetSigners()
require.Equal(t, []sdk.AccAddress{sdk.AccAddress(addr2), sdk.AccAddress(addr1)}, addrs, "Signers for onbehalfof msg is wrong")
}

View File

@ -36,68 +36,56 @@ type Validator struct {
UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding
UnbondingMinTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding
Commission sdk.Dec `json:"commission"` // XXX the commission rate of fees charged to any delegators
CommissionMax sdk.Dec `json:"commission_max"` // XXX maximum commission rate which this validator can ever charge
CommissionChangeRate sdk.Dec `json:"commission_change_rate"` // XXX maximum daily increase of the validator commission
CommissionChangeToday sdk.Dec `json:"commission_change_today"` // XXX commission rate change today, reset each day (UTC time)
Commission Commission `json:"commission"` // commission parameters
}
// NewValidator - initialize a new validator
func NewValidator(operator sdk.ValAddress, pubKey crypto.PubKey, description Description) Validator {
return Validator{
OperatorAddr: operator,
ConsPubKey: pubKey,
Jailed: false,
Status: sdk.Unbonded,
Tokens: sdk.ZeroDec(),
DelegatorShares: sdk.ZeroDec(),
Description: description,
BondHeight: int64(0),
BondIntraTxCounter: int16(0),
UnbondingHeight: int64(0),
UnbondingMinTime: time.Unix(0, 0).UTC(),
Commission: sdk.ZeroDec(),
CommissionMax: sdk.ZeroDec(),
CommissionChangeRate: sdk.ZeroDec(),
CommissionChangeToday: sdk.ZeroDec(),
OperatorAddr: operator,
ConsPubKey: pubKey,
Jailed: false,
Status: sdk.Unbonded,
Tokens: sdk.ZeroDec(),
DelegatorShares: sdk.ZeroDec(),
Description: description,
BondHeight: int64(0),
BondIntraTxCounter: int16(0),
UnbondingHeight: int64(0),
UnbondingMinTime: time.Unix(0, 0).UTC(),
Commission: NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()),
}
}
// what's kept in the store value
type validatorValue struct {
ConsPubKey crypto.PubKey
Jailed bool
Status sdk.BondStatus
Tokens sdk.Dec
DelegatorShares sdk.Dec
Description Description
BondHeight int64
BondIntraTxCounter int16
UnbondingHeight int64
UnbondingMinTime time.Time
Commission sdk.Dec
CommissionMax sdk.Dec
CommissionChangeRate sdk.Dec
CommissionChangeToday sdk.Dec
ConsPubKey crypto.PubKey
Jailed bool
Status sdk.BondStatus
Tokens sdk.Dec
DelegatorShares sdk.Dec
Description Description
BondHeight int64
BondIntraTxCounter int16
UnbondingHeight int64
UnbondingMinTime time.Time
Commission Commission
}
// return the redelegation without fields contained within the key for the store
func MustMarshalValidator(cdc *codec.Codec, validator Validator) []byte {
val := validatorValue{
ConsPubKey: validator.ConsPubKey,
Jailed: validator.Jailed,
Status: validator.Status,
Tokens: validator.Tokens,
DelegatorShares: validator.DelegatorShares,
Description: validator.Description,
BondHeight: validator.BondHeight,
BondIntraTxCounter: validator.BondIntraTxCounter,
UnbondingHeight: validator.UnbondingHeight,
UnbondingMinTime: validator.UnbondingMinTime,
Commission: validator.Commission,
CommissionMax: validator.CommissionMax,
CommissionChangeRate: validator.CommissionChangeRate,
CommissionChangeToday: validator.CommissionChangeToday,
ConsPubKey: validator.ConsPubKey,
Jailed: validator.Jailed,
Status: validator.Status,
Tokens: validator.Tokens,
DelegatorShares: validator.DelegatorShares,
Description: validator.Description,
BondHeight: validator.BondHeight,
BondIntraTxCounter: validator.BondIntraTxCounter,
UnbondingHeight: validator.UnbondingHeight,
UnbondingMinTime: validator.UnbondingMinTime,
Commission: validator.Commission,
}
return cdc.MustMarshalBinary(val)
}
@ -124,21 +112,18 @@ func UnmarshalValidator(cdc *codec.Codec, operatorAddr, value []byte) (validator
}
return Validator{
OperatorAddr: operatorAddr,
ConsPubKey: storeValue.ConsPubKey,
Jailed: storeValue.Jailed,
Tokens: storeValue.Tokens,
Status: storeValue.Status,
DelegatorShares: storeValue.DelegatorShares,
Description: storeValue.Description,
BondHeight: storeValue.BondHeight,
BondIntraTxCounter: storeValue.BondIntraTxCounter,
UnbondingHeight: storeValue.UnbondingHeight,
UnbondingMinTime: storeValue.UnbondingMinTime,
Commission: storeValue.Commission,
CommissionMax: storeValue.CommissionMax,
CommissionChangeRate: storeValue.CommissionChangeRate,
CommissionChangeToday: storeValue.CommissionChangeToday,
OperatorAddr: operatorAddr,
ConsPubKey: storeValue.ConsPubKey,
Jailed: storeValue.Jailed,
Tokens: storeValue.Tokens,
Status: storeValue.Status,
DelegatorShares: storeValue.DelegatorShares,
Description: storeValue.Description,
BondHeight: storeValue.BondHeight,
BondIntraTxCounter: storeValue.BondIntraTxCounter,
UnbondingHeight: storeValue.UnbondingHeight,
UnbondingMinTime: storeValue.UnbondingMinTime,
Commission: storeValue.Commission,
}, nil
}
@ -156,16 +141,13 @@ func (v Validator) HumanReadableString() (string, error) {
resp += fmt.Sprintf("Validator Consensus Pubkey: %s\n", bechConsPubKey)
resp += fmt.Sprintf("Jailed: %v\n", v.Jailed)
resp += fmt.Sprintf("Status: %s\n", sdk.BondStatusToString(v.Status))
resp += fmt.Sprintf("Tokens: %s\n", v.Tokens.String())
resp += fmt.Sprintf("Delegator Shares: %s\n", v.DelegatorShares.String())
resp += fmt.Sprintf("Tokens: %s\n", v.Tokens)
resp += fmt.Sprintf("Delegator Shares: %s\n", v.DelegatorShares)
resp += fmt.Sprintf("Description: %s\n", v.Description)
resp += fmt.Sprintf("Bond Height: %d\n", v.BondHeight)
resp += fmt.Sprintf("Unbonding Height: %d\n", v.UnbondingHeight)
resp += fmt.Sprintf("Minimum Unbonding Time: %v\n", v.UnbondingMinTime)
resp += fmt.Sprintf("Commission: %s\n", v.Commission.String())
resp += fmt.Sprintf("Max Commission Rate: %s\n", v.CommissionMax.String())
resp += fmt.Sprintf("Commission Change Rate: %s\n", v.CommissionChangeRate.String())
resp += fmt.Sprintf("Commission Change Today: %s\n", v.CommissionChangeToday.String())
resp += fmt.Sprintf("Commission: {%s}\n", v.Commission)
return resp, nil
}
@ -189,10 +171,7 @@ type bechValidator struct {
UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding
UnbondingMinTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding
Commission sdk.Dec `json:"commission"` // XXX the commission rate of fees charged to any delegators
CommissionMax sdk.Dec `json:"commission_max"` // XXX maximum commission rate which this validator can ever charge
CommissionChangeRate sdk.Dec `json:"commission_change_rate"` // XXX maximum daily increase of the validator commission
CommissionChangeToday sdk.Dec `json:"commission_change_today"` // XXX commission rate change today, reset each day (UTC time)
Commission Commission `json:"commission"` // commission parameters
}
// MarshalJSON marshals the validator to JSON using Bech32
@ -203,21 +182,18 @@ func (v Validator) MarshalJSON() ([]byte, error) {
}
return codec.Cdc.MarshalJSON(bechValidator{
OperatorAddr: v.OperatorAddr,
ConsPubKey: bechConsPubKey,
Jailed: v.Jailed,
Status: v.Status,
Tokens: v.Tokens,
DelegatorShares: v.DelegatorShares,
Description: v.Description,
BondHeight: v.BondHeight,
BondIntraTxCounter: v.BondIntraTxCounter,
UnbondingHeight: v.UnbondingHeight,
UnbondingMinTime: v.UnbondingMinTime,
Commission: v.Commission,
CommissionMax: v.CommissionMax,
CommissionChangeRate: v.CommissionChangeRate,
CommissionChangeToday: v.CommissionChangeToday,
OperatorAddr: v.OperatorAddr,
ConsPubKey: bechConsPubKey,
Jailed: v.Jailed,
Status: v.Status,
Tokens: v.Tokens,
DelegatorShares: v.DelegatorShares,
Description: v.Description,
BondHeight: v.BondHeight,
BondIntraTxCounter: v.BondIntraTxCounter,
UnbondingHeight: v.UnbondingHeight,
UnbondingMinTime: v.UnbondingMinTime,
Commission: v.Commission,
})
}
@ -232,21 +208,18 @@ func (v *Validator) UnmarshalJSON(data []byte) error {
return err
}
*v = Validator{
OperatorAddr: bv.OperatorAddr,
ConsPubKey: consPubKey,
Jailed: bv.Jailed,
Tokens: bv.Tokens,
Status: bv.Status,
DelegatorShares: bv.DelegatorShares,
Description: bv.Description,
BondHeight: bv.BondHeight,
BondIntraTxCounter: bv.BondIntraTxCounter,
UnbondingHeight: bv.UnbondingHeight,
UnbondingMinTime: bv.UnbondingMinTime,
Commission: bv.Commission,
CommissionMax: bv.CommissionMax,
CommissionChangeRate: bv.CommissionChangeRate,
CommissionChangeToday: bv.CommissionChangeToday,
OperatorAddr: bv.OperatorAddr,
ConsPubKey: consPubKey,
Jailed: bv.Jailed,
Tokens: bv.Tokens,
Status: bv.Status,
DelegatorShares: bv.DelegatorShares,
Description: bv.Description,
BondHeight: bv.BondHeight,
BondIntraTxCounter: bv.BondIntraTxCounter,
UnbondingHeight: bv.UnbondingHeight,
UnbondingMinTime: bv.UnbondingMinTime,
Commission: bv.Commission,
}
return nil
}
@ -254,18 +227,14 @@ func (v *Validator) UnmarshalJSON(data []byte) error {
//___________________________________________________________________
// only the vitals - does not check bond height of IntraTxCounter
// nolint gocyclo - why dis fail?
func (v Validator) Equal(c2 Validator) bool {
return v.ConsPubKey.Equals(c2.ConsPubKey) &&
bytes.Equal(v.OperatorAddr, c2.OperatorAddr) &&
v.Status.Equal(c2.Status) &&
v.Tokens.Equal(c2.Tokens) &&
v.DelegatorShares.Equal(c2.DelegatorShares) &&
v.Description == c2.Description &&
v.Commission.Equal(c2.Commission) &&
v.CommissionMax.Equal(c2.CommissionMax) &&
v.CommissionChangeRate.Equal(c2.CommissionChangeRate) &&
v.CommissionChangeToday.Equal(c2.CommissionChangeToday)
func (v Validator) Equal(v2 Validator) bool {
return v.ConsPubKey.Equals(v2.ConsPubKey) &&
bytes.Equal(v.OperatorAddr, v2.OperatorAddr) &&
v.Status.Equal(v2.Status) &&
v.Tokens.Equal(v2.Tokens) &&
v.DelegatorShares.Equal(v2.DelegatorShares) &&
v.Description == v2.Description &&
v.Commission.Equal(v2.Commission)
}
// return the TM validator address
@ -400,6 +369,17 @@ func (v Validator) RemoveTokens(pool Pool, tokens sdk.Dec) (Validator, Pool) {
return v, pool
}
// SetInitialCommission attempts to set a validator's initial commission. An
// error is returned if the commission is invalid.
func (v Validator) SetInitialCommission(commission Commission) (Validator, sdk.Error) {
if err := commission.Validate(); err != nil {
return v, err
}
v.Commission = commission
return v, nil
}
//_________________________________________________________________________________________________________
// AddTokensFromDel adds tokens to a validator

View File

@ -274,3 +274,37 @@ func TestValidatorMarshalUnmarshalJSON(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, validator, *got)
}
func TestValidatorSetInitialCommission(t *testing.T) {
val := NewValidator(addr1, pk1, Description{})
testCases := []struct {
validator Validator
commission Commission
expectedErr bool
}{
{val, NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), false},
{val, NewCommission(sdk.ZeroDec(), sdk.NewDecWithPrec(-1, 1), sdk.ZeroDec()), true},
{val, NewCommission(sdk.ZeroDec(), sdk.NewDec(15000000000), sdk.ZeroDec()), true},
{val, NewCommission(sdk.NewDecWithPrec(-1, 1), sdk.ZeroDec(), sdk.ZeroDec()), true},
{val, NewCommission(sdk.NewDecWithPrec(2, 1), sdk.NewDecWithPrec(1, 1), sdk.ZeroDec()), true},
{val, NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.NewDecWithPrec(-1, 1)), true},
{val, NewCommission(sdk.ZeroDec(), sdk.NewDecWithPrec(1, 1), sdk.NewDecWithPrec(2, 1)), true},
}
for i, tc := range testCases {
val, err := tc.validator.SetInitialCommission(tc.commission)
if tc.expectedErr {
require.Error(t, err,
"expected error for test case #%d with commission: %s", i, tc.commission,
)
} else {
require.NoError(t, err,
"unexpected error for test case #%d with commission: %s", i, tc.commission,
)
require.Equal(t, tc.commission, val.Commission,
"invalid validator commission for test case #%d with commission: %s", i, tc.commission,
)
}
}
}