diff --git a/x/stake/keeper/delegation.go b/x/stake/keeper/delegation.go index 2bad79c20..107665a9b 100644 --- a/x/stake/keeper/delegation.go +++ b/x/stake/keeper/delegation.go @@ -2,6 +2,7 @@ package keeper import ( "bytes" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/stake/types" @@ -311,6 +312,32 @@ func (k Keeper) unbond(ctx sdk.Context, delegatorAddr, validatorAddr sdk.AccAddr //______________________________________________________________________________________________________ +// get info for begin functions: MinTime and CreationHeight +func (k Keeper) getBeginInfo(ctx sdk.Context, params types.Params, validatorSrcAddr sdk.AccAddress) ( + minTime time.Time, height int64, completeNow bool) { + + validator, found := k.GetValidator(ctx, validatorSrcAddr) + switch { + case !found || validator.Status == sdk.Bonded: + + // longest wait - just unbonding period from now + minTime = ctx.BlockHeader().Time.Add(params.UnbondingTime) + height = ctx.BlockHeader().Height + return minTime, height, false + + case validator.Status == sdk.Unbonding: + minTime = validator.UnbondingMinTime + height = validator.UnbondingHeight + return minTime, height, false + + case validator.Status == sdk.Unbonded: + return minTime, height, true + + default: + panic("unknown validator status") + } +} + // complete unbonding an unbonding record func (k Keeper) BeginUnbonding(ctx sdk.Context, delegatorAddr, validatorAddr sdk.AccAddress, sharesAmount sdk.Dec) sdk.Error { @@ -327,12 +354,22 @@ func (k Keeper) BeginUnbonding(ctx sdk.Context, delegatorAddr, validatorAddr sdk // create the unbonding delegation params := k.GetParams(ctx) - minTime := ctx.BlockHeader().Time.Add(params.UnbondingTime) + minTime, height, completeNow := k.getBeginInfo(ctx, params, validatorAddr) balance := sdk.Coin{params.BondDenom, returnAmount.RoundInt()} + // no need to create the ubd object just complete now + if completeNow { + _, _, err := k.coinKeeper.AddCoins(ctx, delegatorAddr, sdk.Coins{balance}) + if err != nil { + return err + } + return nil + } + ubd := types.UnbondingDelegation{ DelegatorAddr: delegatorAddr, ValidatorAddr: validatorAddr, + CreationHeight: height, MinTime: minTime, Balance: balance, InitialBalance: balance, @@ -390,11 +427,19 @@ func (k Keeper) BeginRedelegation(ctx sdk.Context, delegatorAddr, validatorSrcAd // create the unbonding delegation minTime := ctx.BlockHeader().Time.Add(params.UnbondingTime) + height := ctx.BlockHeader().Height + + minTime, height, completeNow := k.getBeginInfo(ctx, params, validatorSrcAddr) + + if completeNow { // no need to create the redelegation object + return nil + } red := types.Redelegation{ DelegatorAddr: delegatorAddr, ValidatorSrcAddr: validatorSrcAddr, ValidatorDstAddr: validatorDstAddr, + CreationHeight: height, MinTime: minTime, SharesDst: sharesCreated, SharesSrc: sharesAmount, diff --git a/x/stake/keeper/slash.go b/x/stake/keeper/slash.go index aa6fe974d..b936e585d 100644 --- a/x/stake/keeper/slash.go +++ b/x/stake/keeper/slash.go @@ -43,6 +43,26 @@ func (k Keeper) Slash(ctx sdk.Context, pubkey crypto.PubKey, infractionHeight in pubkey.Address())) return } + + // do not slash if unbonded + unbonded := false + if validator.Status == sdk.Unbonded { + unbonded = true + } else if validator.Status == sdk.Unbonding { + ctxTime := ctx.BlockHeader().Time + if ctxTime.After(validator.UnbondingMinTime) { + + // TODO should we also just update the + // validator status to unbonded here? + unbonded = true + } + } + if unbonded { + logger.Info(fmt.Sprintf( + "failed attempt to slash an unbonded validator")) + return + } + operatorAddress := validator.GetOperator() // Track remaining slash amount for the validator @@ -91,17 +111,16 @@ func (k Keeper) Slash(ctx sdk.Context, pubkey crypto.PubKey, infractionHeight in // Cannot decrease balance below zero tokensToBurn := sdk.MinDec(remainingSlashAmount, validator.Tokens) - // Get the current pool + // burn validator's tokens pool := k.GetPool(ctx) - // remove tokens from the validator validator, pool = validator.RemoveTokens(pool, tokensToBurn) - // burn tokens pool.LooseTokens = pool.LooseTokens.Sub(tokensToBurn) - // update the pool k.SetPool(ctx, pool) + // update the validator, possibly kicking it out validator = k.UpdateValidator(ctx, validator) - // remove validator if it has been reduced to zero shares + + // remove validator if it has no more tokens if validator.Tokens.IsZero() { k.RemoveValidator(ctx, validator.Operator) } @@ -134,12 +153,12 @@ func (k Keeper) Unjail(ctx sdk.Context, pubkey crypto.PubKey) { } // set the jailed flag on a validator -func (k Keeper) setJailed(ctx sdk.Context, pubkey crypto.PubKey, jailed bool) { +func (k Keeper) setJailed(ctx sdk.Context, pubkey crypto.PubKey, isJailed bool) { validator, found := k.GetValidatorByPubKey(ctx, pubkey) if !found { - panic(fmt.Errorf("Validator with pubkey %s not found, cannot set jailed to %v", pubkey, jailed)) + panic(fmt.Errorf("Validator with pubkey %s not found, cannot set jailed to %v", pubkey, isJailed)) } - validator.Jailed = jailed + validator.Jailed = isJailed k.UpdateValidator(ctx, validator) // update validator, possibly unbonding or bonding it return } @@ -179,6 +198,7 @@ func (k Keeper) slashUnbondingDelegation(ctx sdk.Context, unbondingDelegation ty unbondingDelegation.Balance.Amount = unbondingDelegation.Balance.Amount.Sub(unbondingSlashAmount) k.SetUnbondingDelegation(ctx, unbondingDelegation) pool := k.GetPool(ctx) + // Burn loose tokens // Ref https://github.com/cosmos/cosmos-sdk/pull/1278#discussion_r198657760 pool.LooseTokens = pool.LooseTokens.Sub(slashAmount) @@ -239,6 +259,7 @@ func (k Keeper) slashRedelegation(ctx sdk.Context, validator types.Validator, re if err != nil { panic(fmt.Errorf("error unbonding delegator: %v", err)) } + // Burn loose tokens pool := k.GetPool(ctx) pool.LooseTokens = pool.LooseTokens.Sub(tokensToBurn) diff --git a/x/stake/keeper/validator.go b/x/stake/keeper/validator.go index e933a6d23..72567576b 100644 --- a/x/stake/keeper/validator.go +++ b/x/stake/keeper/validator.go @@ -212,6 +212,7 @@ func (k Keeper) UpdateValidator(ctx sdk.Context, validator types.Validator) type cliffPower := k.GetCliffValidatorPower(ctx) switch { + // if the validator is already bonded and the power is increasing, we need // perform the following: // a) update Tendermint @@ -240,10 +241,11 @@ func (k Keeper) UpdateValidator(ctx sdk.Context, validator types.Validator) type bytes.Compare(valPower, cliffPower) == -1: //(valPower < cliffPower // skip to completion - // default case - validator was either: + default: + // default case - validator was either: // a) not-bonded and now has power-rank greater than cliff validator // b) bonded and now has decreased in power - default: + // update the validator set for this validator updatedVal, updated := k.UpdateBondedValidators(ctx, validator) if updated { @@ -307,10 +309,13 @@ func (k Keeper) updateCliffValidator(ctx sdk.Context, affectedVal types.Validato newCliffValRank := GetValidatorsByPowerIndexKey(newCliffVal, pool) if bytes.Equal(affectedVal.Operator, newCliffVal.Operator) { + // The affected validator remains the cliff validator, however, since // the store does not contain the new power, update the new power rank. store.Set(ValidatorPowerCliffKey, affectedValRank) + } else if bytes.Compare(affectedValRank, newCliffValRank) > 0 { + // The affected validator no longer remains the cliff validator as it's // power is greater than the new cliff validator. k.setCliffValidator(ctx, newCliffVal, pool) @@ -321,7 +326,7 @@ func (k Keeper) updateCliffValidator(ctx sdk.Context, affectedVal types.Validato func (k Keeper) updateForJailing(ctx sdk.Context, oldFound bool, oldValidator, newValidator types.Validator) types.Validator { if newValidator.Jailed && oldFound && oldValidator.Status == sdk.Bonded { - newValidator = k.unbondValidator(ctx, newValidator) + newValidator = k.beginUnbondingValidator(ctx, newValidator) // need to also clear the cliff validator spot because the jail has // opened up a new spot which will be filled when @@ -416,20 +421,20 @@ func (k Keeper) UpdateBondedValidators( } } - // increment bondedValidatorsCount / get the validator to bond - if !validator.Jailed { - if validator.Status != sdk.Bonded { - validatorToBond = validator - if newValidatorBonded { - panic("already decided to bond a validator, can't bond another!") - } - newValidatorBonded = true - } - } else { - // TODO: document why we must break here. + // if we've reached jailed validators no further bonded validators exist + if validator.Jailed { break } + // increment bondedValidatorsCount / get the validator to bond + if validator.Status != sdk.Bonded { + validatorToBond = validator + if newValidatorBonded { + panic("already decided to bond a validator, can't bond another!") + } + newValidatorBonded = true + } + // increment the total number of bonded validators and potentially mark // the validator to bond if validator.Status != sdk.Bonded { @@ -464,13 +469,15 @@ func (k Keeper) UpdateBondedValidators( } if bytes.Equal(validatorToBond.Operator, affectedValidator.Operator) { - // unbond the old cliff validator iff the affected validator was - // newly bonded and has greater power - k.unbondValidator(ctx, oldCliffVal) + + // begin unbonding the old cliff validator iff the affected + // validator was newly bonded and has greater power + k.beginUnbondingValidator(ctx, oldCliffVal) } else { - // otherwise unbond the affected validator, which must have been - // kicked out - affectedValidator = k.unbondValidator(ctx, affectedValidator) + + // otherwise begin unbonding the affected validator, which must + // have been kicked out + affectedValidator = k.beginUnbondingValidator(ctx, affectedValidator) } } @@ -563,25 +570,30 @@ func kickOutValidators(k Keeper, ctx sdk.Context, toKickOut map[string]byte) { if !found { panic(fmt.Sprintf("validator record not found for address: %v\n", ownerAddr)) } - k.unbondValidator(ctx, validator) + k.beginUnbondingValidator(ctx, validator) } } // perform all the store operations for when a validator status becomes unbonded -func (k Keeper) unbondValidator(ctx sdk.Context, validator types.Validator) types.Validator { +func (k Keeper) beginUnbondingValidator(ctx sdk.Context, validator types.Validator) types.Validator { store := ctx.KVStore(k.storeKey) pool := k.GetPool(ctx) + params := k.GetParams(ctx) // sanity check - if validator.Status == sdk.Unbonded { - panic(fmt.Sprintf("should not already be unbonded, validator: %v\n", validator)) + if validator.Status == sdk.Unbonded || + validator.Status == sdk.Unbonding { + panic(fmt.Sprintf("should not already be unbonded or unbonding, validator: %v\n", validator)) } // set the status - validator, pool = validator.UpdateStatus(pool, sdk.Unbonded) + validator, pool = validator.UpdateStatus(pool, sdk.Unbonding) k.SetPool(ctx, pool) + validator.UnbondingMinTime = ctx.BlockHeader().Time.Add(params.UnbondingTime) + validator.UnbondingHeight = ctx.BlockHeader().Height + // save the now unbonded validator record k.SetValidator(ctx, validator) diff --git a/x/stake/types/validator.go b/x/stake/types/validator.go index f8404b596..db9c8afd5 100644 --- a/x/stake/types/validator.go +++ b/x/stake/types/validator.go @@ -3,6 +3,7 @@ package types import ( "bytes" "fmt" + "time" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" @@ -31,15 +32,14 @@ type Validator struct { Description Description `json:"description"` // description terms for the validator BondHeight int64 `json:"bond_height"` // earliest height as a bonded validator BondIntraTxCounter int16 `json:"bond_intra_tx_counter"` // block-local tx index of validator change - ProposerRewardPool sdk.Coins `json:"proposer_reward_pool"` // XXX reward pool collected from being the proposer + + 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) - - // fee related - LastBondedTokens sdk.Dec `json:"prev_bonded_tokens"` // Previous bonded tokens held } // NewValidator - initialize a new validator @@ -54,12 +54,12 @@ func NewValidator(operator sdk.AccAddress, pubKey crypto.PubKey, description Des Description: description, BondHeight: int64(0), BondIntraTxCounter: int16(0), - ProposerRewardPool: sdk.Coins{}, + UnbondingHeight: int64(0), + UnbondingMinTime: time.Unix(0, 0), Commission: sdk.ZeroDec(), CommissionMax: sdk.ZeroDec(), CommissionChangeRate: sdk.ZeroDec(), CommissionChangeToday: sdk.ZeroDec(), - LastBondedTokens: sdk.ZeroDec(), } } @@ -73,12 +73,12 @@ type validatorValue struct { Description Description BondHeight int64 BondIntraTxCounter int16 - ProposerRewardPool sdk.Coins + UnbondingHeight int64 + UnbondingMinTime time.Time Commission sdk.Dec CommissionMax sdk.Dec CommissionChangeRate sdk.Dec CommissionChangeToday sdk.Dec - LastBondedTokens sdk.Dec } // return the redelegation without fields contained within the key for the store @@ -92,12 +92,12 @@ func MustMarshalValidator(cdc *wire.Codec, validator Validator) []byte { Description: validator.Description, BondHeight: validator.BondHeight, BondIntraTxCounter: validator.BondIntraTxCounter, - ProposerRewardPool: validator.ProposerRewardPool, + UnbondingHeight: validator.UnbondingHeight, + UnbondingMinTime: validator.UnbondingMinTime, Commission: validator.Commission, CommissionMax: validator.CommissionMax, CommissionChangeRate: validator.CommissionChangeRate, CommissionChangeToday: validator.CommissionChangeToday, - LastBondedTokens: validator.LastBondedTokens, } return cdc.MustMarshalBinary(val) } @@ -108,7 +108,6 @@ func MustUnmarshalValidator(cdc *wire.Codec, operatorAddr, value []byte) Validat if err != nil { panic(err) } - return validator } @@ -134,12 +133,12 @@ func UnmarshalValidator(cdc *wire.Codec, operatorAddr, value []byte) (validator Description: storeValue.Description, BondHeight: storeValue.BondHeight, BondIntraTxCounter: storeValue.BondIntraTxCounter, - ProposerRewardPool: storeValue.ProposerRewardPool, + UnbondingHeight: storeValue.UnbondingHeight, + UnbondingMinTime: storeValue.UnbondingMinTime, Commission: storeValue.Commission, CommissionMax: storeValue.CommissionMax, CommissionChangeRate: storeValue.CommissionChangeRate, CommissionChangeToday: storeValue.CommissionChangeToday, - LastBondedTokens: storeValue.LastBondedTokens, }, nil } @@ -161,12 +160,12 @@ func (v Validator) HumanReadableString() (string, error) { resp += fmt.Sprintf("Delegator Shares: %s\n", v.DelegatorShares.String()) resp += fmt.Sprintf("Description: %s\n", v.Description) resp += fmt.Sprintf("Bond Height: %d\n", v.BondHeight) - resp += fmt.Sprintf("Proposer Reward Pool: %s\n", v.ProposerRewardPool.String()) + resp += fmt.Sprintf("Unbonding Height: %d\n", v.UnbondingHeight) + resp += fmt.Sprintf("Minimum Unbonding Time: %d\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("Previous Bonded Tokens: %s\n", v.LastBondedTokens.String()) return resp, nil } @@ -186,15 +185,14 @@ type BechValidator struct { Description Description `json:"description"` // description terms for the validator BondHeight int64 `json:"bond_height"` // earliest height as a bonded validator BondIntraTxCounter int16 `json:"bond_intra_tx_counter"` // block-local tx index of validator change - ProposerRewardPool sdk.Coins `json:"proposer_reward_pool"` // XXX reward pool collected from being the proposer + + 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) - - // fee related - LastBondedTokens sdk.Dec `json:"prev_bonded_shares"` // last bonded token amount } // get the bech validator from the the regular validator @@ -216,14 +214,13 @@ func (v Validator) Bech32Validator() (BechValidator, error) { Description: v.Description, BondHeight: v.BondHeight, BondIntraTxCounter: v.BondIntraTxCounter, - ProposerRewardPool: v.ProposerRewardPool, + UnbondingHeight: v.UnbondingHeight, + UnbondingMinTime: v.UnbondingMinTime, Commission: v.Commission, CommissionMax: v.CommissionMax, CommissionChangeRate: v.CommissionChangeRate, CommissionChangeToday: v.CommissionChangeToday, - - LastBondedTokens: v.LastBondedTokens, }, nil } @@ -238,12 +235,10 @@ func (v Validator) Equal(c2 Validator) bool { v.Tokens.Equal(c2.Tokens) && v.DelegatorShares.Equal(c2.DelegatorShares) && v.Description == c2.Description && - v.ProposerRewardPool.IsEqual(c2.ProposerRewardPool) && v.Commission.Equal(c2.Commission) && v.CommissionMax.Equal(c2.CommissionMax) && v.CommissionChangeRate.Equal(c2.CommissionChangeRate) && - v.CommissionChangeToday.Equal(c2.CommissionChangeToday) && - v.LastBondedTokens.Equal(c2.LastBondedTokens) + v.CommissionChangeToday.Equal(c2.CommissionChangeToday) } // constant used in flags to indicate that description field should not be updated