2018-06-26 19:00:12 -07:00
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
2018-06-29 20:34:55 -07:00
types "github.com/cosmos/cosmos-sdk/x/stake/types"
2018-06-28 17:54:47 -07:00
"github.com/tendermint/tendermint/crypto"
2018-06-26 19:00:12 -07:00
)
2018-06-29 20:34:55 -07:00
// Slash a validator for an infraction committed at a known height
// Find the contributing stake at that height and burn the specified slashFactor
// of it, updating unbonding delegation & redelegations appropriately
//
// CONTRACT:
// slashFactor is non-negative
// CONTRACT:
// Infraction committed equal to or less than an unbonding period in the past,
// so all unbonding delegations and redelegations from that height are stored
// CONTRACT:
// Infraction committed at the current height or at a past height,
// not at a height in the future
func ( k Keeper ) Slash ( ctx sdk . Context , pubkey crypto . PubKey , infractionHeight int64 , power int64 , slashFactor sdk . Rat ) {
logger := ctx . Logger ( ) . With ( "module" , "x/stake" )
if slashFactor . LT ( sdk . ZeroRat ( ) ) {
panic ( fmt . Errorf ( "attempted to slash with a negative slashFactor: %v" , slashFactor ) )
}
// Amount of slashing = slash slashFactor * power at time of infraction
2018-07-02 08:57:33 -07:00
slashAmount := sdk . NewRat ( power ) . Mul ( slashFactor ) . RoundInt ( )
2018-06-29 20:34:55 -07:00
// ref https://github.com/cosmos/cosmos-sdk/issues/1348
// ref https://github.com/cosmos/cosmos-sdk/issues/1471
2018-06-26 19:00:12 -07:00
validator , found := k . GetValidatorByPubKey ( ctx , pubkey )
if ! found {
2018-07-06 16:53:40 -07:00
// If not found, the validator must have been overslashed and removed - so we don't need to do anything
// NOTE: Correctness dependent on invariant that unbonding delegations / redelegations must also have been completely
// slashed in this case - which we don't explicitly check, but should be true.
// Log the slash attempt for future reference (maybe we should tag it too)
2018-07-06 17:20:37 -07:00
logger . Error ( fmt . Sprintf ( "WARNING: Ignored attempt to slash a nonexistent validator with address %s, we recommend you investigate immediately" , pubkey . Address ( ) ) )
2018-07-06 16:49:17 -07:00
return
2018-06-29 20:34:55 -07:00
}
ownerAddress := validator . GetOwner ( )
// Track remaining slash amount for the validator
// This will decrease when we slash unbondings and
// redelegations, as that stake has since unbonded
remainingSlashAmount := slashAmount
switch {
case infractionHeight > ctx . BlockHeight ( ) :
// Can't slash infractions in the future
panic ( fmt . Sprintf ( "impossible attempt to slash future infraction at height %d but we are at height %d" , infractionHeight , ctx . BlockHeight ( ) ) )
case infractionHeight == ctx . BlockHeight ( ) :
// Special-case slash at current height for efficiency - we don't need to look through unbonding delegations or redelegations
logger . Info ( fmt . Sprintf ( "Slashing at current height %d, not scanning unbonding delegations & redelegations" , infractionHeight ) )
case infractionHeight < ctx . BlockHeight ( ) :
// Iterate through unbonding delegations from slashed validator
unbondingDelegations := k . GetUnbondingDelegationsFromValidator ( ctx , ownerAddress )
for _ , unbondingDelegation := range unbondingDelegations {
amountSlashed := k . slashUnbondingDelegation ( ctx , unbondingDelegation , infractionHeight , slashFactor )
if amountSlashed . IsZero ( ) {
continue
}
remainingSlashAmount = remainingSlashAmount . Sub ( amountSlashed )
}
// Iterate through redelegations from slashed validator
redelegations := k . GetRedelegationsFromValidator ( ctx , ownerAddress )
for _ , redelegation := range redelegations {
amountSlashed := k . slashRedelegation ( ctx , validator , redelegation , infractionHeight , slashFactor )
if amountSlashed . IsZero ( ) {
continue
}
remainingSlashAmount = remainingSlashAmount . Sub ( amountSlashed )
}
2018-06-26 19:00:12 -07:00
}
2018-06-29 20:34:55 -07:00
// Cannot decrease balance below zero
2018-07-09 13:54:28 -07:00
sharesToRemove := sdk . MinInt ( remainingSlashAmount , validator . PoolShares . Amount . RoundInt ( ) )
2018-06-29 20:34:55 -07:00
// Get the current pool
2018-06-26 19:00:12 -07:00
pool := k . GetPool ( ctx )
2018-06-29 20:34:55 -07:00
// remove shares from the validator
validator , pool , burned := validator . RemovePoolShares ( pool , sdk . NewRatFromInt ( sharesToRemove ) )
// burn tokens
pool . LooseTokens -= burned
// update the pool
k . SetPool ( ctx , pool )
// update the validator, possibly kicking it out
2018-07-05 11:21:56 -07:00
validator = k . UpdateValidator ( ctx , validator )
// remove validator if it has been reduced to zero shares
if validator . PoolShares . Amount . IsZero ( ) {
k . RemoveValidator ( ctx , validator . Owner )
}
2018-06-26 19:00:12 -07:00
2018-06-29 20:34:55 -07:00
// Log that a slash occurred!
logger . Info ( fmt . Sprintf ( "Validator %s slashed by slashFactor %v, removed %v shares and burned %d tokens" , pubkey . Address ( ) , slashFactor , sharesToRemove , burned ) )
// TODO Return event(s), blocked on https://github.com/tendermint/tendermint/pull/1803
2018-06-26 19:00:12 -07:00
return
}
// revoke a validator
func ( k Keeper ) Revoke ( ctx sdk . Context , pubkey crypto . PubKey ) {
2018-06-29 20:34:55 -07:00
k . setRevoked ( ctx , pubkey , true )
2018-06-26 19:00:12 -07:00
logger := ctx . Logger ( ) . With ( "module" , "x/stake" )
logger . Info ( fmt . Sprintf ( "Validator %s revoked" , pubkey . Address ( ) ) )
2018-06-29 20:34:55 -07:00
// TODO Return event(s), blocked on https://github.com/tendermint/tendermint/pull/1803
2018-06-26 19:00:12 -07:00
return
}
// unrevoke a validator
func ( k Keeper ) Unrevoke ( ctx sdk . Context , pubkey crypto . PubKey ) {
2018-06-29 20:34:55 -07:00
k . setRevoked ( ctx , pubkey , false )
logger := ctx . Logger ( ) . With ( "module" , "x/stake" )
logger . Info ( fmt . Sprintf ( "Validator %s unrevoked" , pubkey . Address ( ) ) )
// TODO Return event(s), blocked on https://github.com/tendermint/tendermint/pull/1803
return
}
2018-06-26 19:00:12 -07:00
2018-06-29 20:34:55 -07:00
// set the revoked flag on a validator
func ( k Keeper ) setRevoked ( ctx sdk . Context , pubkey crypto . PubKey , revoked bool ) {
2018-06-26 19:00:12 -07:00
validator , found := k . GetValidatorByPubKey ( ctx , pubkey )
if ! found {
2018-06-29 20:34:55 -07:00
panic ( fmt . Errorf ( "Validator with pubkey %s not found, cannot set revoked to %v" , pubkey , revoked ) )
}
validator . Revoked = revoked
k . UpdateValidator ( ctx , validator ) // update validator, possibly unbonding or bonding it
return
}
// slash an unbonding delegation and update the pool
// return the amount that would have been slashed assuming
// the unbonding delegation had enough stake to slash
// (the amount actually slashed may be less if there's
// insufficient stake remaining)
func ( k Keeper ) slashUnbondingDelegation ( ctx sdk . Context , unbondingDelegation types . UnbondingDelegation , infractionHeight int64 , slashFactor sdk . Rat ) ( slashAmount sdk . Int ) {
now := ctx . BlockHeader ( ) . Time
// If unbonding started before this height, stake didn't contribute to infraction
if unbondingDelegation . CreationHeight < infractionHeight {
return sdk . ZeroInt ( )
}
if unbondingDelegation . MinTime < now {
// Unbonding delegation no longer eligible for slashing, skip it
// TODO Settle and delete it automatically?
return sdk . ZeroInt ( )
}
// Calculate slash amount proportional to stake contributing to infraction
2018-07-02 08:57:33 -07:00
slashAmount = sdk . NewRatFromInt ( unbondingDelegation . InitialBalance . Amount , sdk . OneInt ( ) ) . Mul ( slashFactor ) . RoundInt ( )
2018-06-29 20:34:55 -07:00
// Don't slash more tokens than held
// Possible since the unbonding delegation may already
// have been slashed, and slash amounts are calculated
// according to stake held at time of infraction
2018-07-09 13:54:28 -07:00
unbondingSlashAmount := sdk . MinInt ( slashAmount , unbondingDelegation . Balance . Amount )
2018-06-29 20:34:55 -07:00
// Update unbonding delegation if necessary
if ! unbondingSlashAmount . IsZero ( ) {
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 -= slashAmount . Int64 ( )
k . SetPool ( ctx , pool )
2018-06-26 19:00:12 -07:00
}
return
}
2018-06-29 20:34:55 -07:00
// slash a redelegation and update the pool
// return the amount that would have been slashed assuming
// the unbonding delegation had enough stake to slash
// (the amount actually slashed may be less if there's
// insufficient stake remaining)
func ( k Keeper ) slashRedelegation ( ctx sdk . Context , validator types . Validator , redelegation types . Redelegation , infractionHeight int64 , slashFactor sdk . Rat ) ( slashAmount sdk . Int ) {
now := ctx . BlockHeader ( ) . Time
// If redelegation started before this height, stake didn't contribute to infraction
if redelegation . CreationHeight < infractionHeight {
return sdk . ZeroInt ( )
}
if redelegation . MinTime < now {
// Redelegation no longer eligible for slashing, skip it
// TODO Delete it automatically?
return sdk . ZeroInt ( )
}
// Calculate slash amount proportional to stake contributing to infraction
2018-07-02 08:57:33 -07:00
slashAmount = sdk . NewRatFromInt ( redelegation . InitialBalance . Amount , sdk . OneInt ( ) ) . Mul ( slashFactor ) . RoundInt ( )
2018-06-29 20:34:55 -07:00
// Don't slash more tokens than held
// Possible since the redelegation may already
// have been slashed, and slash amounts are calculated
// according to stake held at time of infraction
2018-07-09 14:11:51 -07:00
redelegationSlashAmount := sdk . MinInt ( slashAmount , redelegation . Balance . Amount )
2018-06-29 20:34:55 -07:00
// Update redelegation if necessary
if ! redelegationSlashAmount . IsZero ( ) {
redelegation . Balance . Amount = redelegation . Balance . Amount . Sub ( redelegationSlashAmount )
k . SetRedelegation ( ctx , redelegation )
}
// Unbond from target validator
sharesToUnbond := slashFactor . Mul ( redelegation . SharesDst )
if ! sharesToUnbond . IsZero ( ) {
delegation , found := k . GetDelegation ( ctx , redelegation . DelegatorAddr , redelegation . ValidatorDstAddr )
if ! found {
// If deleted, delegation has zero shares, and we can't unbond any more
return slashAmount
}
if sharesToUnbond . GT ( delegation . Shares ) {
sharesToUnbond = delegation . Shares
}
tokensToBurn , err := k . unbond ( ctx , redelegation . DelegatorAddr , redelegation . ValidatorDstAddr , sharesToUnbond )
if err != nil {
panic ( fmt . Errorf ( "error unbonding delegator: %v" , err ) )
}
// Burn loose tokens
pool := k . GetPool ( ctx )
pool . LooseTokens -= tokensToBurn
k . SetPool ( ctx , pool )
}
return slashAmount
}