diff --git a/types/module/module.go b/types/module/module.go index 95e35f3546e..0b6983eed8d 100644 --- a/types/module/module.go +++ b/types/module/module.go @@ -367,11 +367,6 @@ func (m *Manager) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, genesisData } } - // a chain must initialize with a non-empty validator set - if len(validatorUpdates) == 0 { - panic(fmt.Sprintf("validator set is empty after InitGenesis, please ensure at least one validator is initialized with a delegation greater than or equal to the DefaultPowerReduction (%d)", sdk.DefaultPowerReduction)) - } - return abci.ResponseInitChain{ Validators: validatorUpdates, } diff --git a/x/slashing/keeper/infractions.go b/x/slashing/keeper/infractions.go index 17f56d04c6c..6817faa6fe7 100644 --- a/x/slashing/keeper/infractions.go +++ b/x/slashing/keeper/infractions.go @@ -6,7 +6,6 @@ import ( cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/slashing/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) // HandleValidatorSignature handles a validator signature, must be called once per validator per block. @@ -16,9 +15,6 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre // fetch the validator public key consAddr := sdk.ConsAddress(addr) - if _, err := k.GetPubkey(ctx, addr); err != nil { - panic(fmt.Sprintf("Validator consensus-address %s not found", consAddr)) - } // don't update missed blocks when validator's jailed if k.sk.IsValidatorJailed(ctx, consAddr) { @@ -88,9 +84,6 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre // Note that this *can* result in a negative "distributionHeight" up to -ValidatorUpdateDelay-1, // i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block. // That's fine since this is just used to filter unbonding delegations & redelegations. - distributionHeight := height - sdk.ValidatorUpdateDelay - 1 - - coinsBurned := k.sk.SlashWithInfractionReason(ctx, consAddr, distributionHeight, power, k.SlashFractionDowntime(ctx), stakingtypes.Infraction_INFRACTION_DOWNTIME) ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeSlash, @@ -98,7 +91,6 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre sdk.NewAttribute(types.AttributeKeyPower, fmt.Sprintf("%d", power)), sdk.NewAttribute(types.AttributeKeyReason, types.AttributeValueMissingSignature), sdk.NewAttribute(types.AttributeKeyJailed, consAddr.String()), - sdk.NewAttribute(types.AttributeKeyBurnedCoins, coinsBurned.String()), ), ) k.sk.Jail(ctx, consAddr) diff --git a/x/slashing/testutil/expected_keepers_mocks.go b/x/slashing/testutil/expected_keepers_mocks.go index 86d57c5e2d6..ed49dea4e85 100644 --- a/x/slashing/testutil/expected_keepers_mocks.go +++ b/x/slashing/testutil/expected_keepers_mocks.go @@ -334,7 +334,7 @@ func (mr *MockStakingKeeperMockRecorder) MaxValidators(arg0 interface{}) *gomock } // Slash mocks base method. -func (m *MockStakingKeeper) Slash(arg0 types.Context, arg1 types.ConsAddress, arg2, arg3 int64, arg4 types.Dec) math.Int { +func (m *MockStakingKeeper) Slash(arg0 types.Context, arg1 types.ConsAddress, arg2, arg3 int64, arg4 types.Dec, arg5 stakingtypes.Infraction) math.Int { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Slash", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(math.Int) diff --git a/x/slashing/types/expected_keepers.go b/x/slashing/types/expected_keepers.go index b506b4450de..482a6df17ef 100644 --- a/x/slashing/types/expected_keepers.go +++ b/x/slashing/types/expected_keepers.go @@ -41,7 +41,7 @@ type StakingKeeper interface { ValidatorByConsAddr(sdk.Context, sdk.ConsAddress) stakingtypes.ValidatorI // get a particular validator by consensus address // slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction - Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec) math.Int + Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec, stakingtypes.Infraction) SlashWithInfractionReason(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec, stakingtypes.Infraction) math.Int Jail(sdk.Context, sdk.ConsAddress) // jail a validator Unjail(sdk.Context, sdk.ConsAddress) // unjail a validator diff --git a/x/staking/keeper/slash.go b/x/staking/keeper/slash.go index e5b4e1f87e6..3557469d957 100644 --- a/x/staking/keeper/slash.go +++ b/x/staking/keeper/slash.go @@ -30,7 +30,132 @@ import ( // // Infraction was committed at the current height or at a past height, // not at a height in the future -func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec) math.Int { +func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec, _ types.Infraction) { + logger := k.Logger(ctx) + + if slashFactor.IsNegative() { + panic(fmt.Errorf("attempted to slash with a negative slash factor: %v", slashFactor)) + } + + // Amount of slashing = slash slashFactor * power at time of infraction + amount := k.TokensFromConsensusPower(ctx, power) + slashAmountDec := sdk.NewDecFromInt(amount).Mul(slashFactor) + slashAmount := slashAmountDec.TruncateInt() + + // ref https://github.com/cosmos/cosmos-sdk/issues/1348 + + validator, found := k.GetValidatorByConsAddr(ctx, consAddr) + if !found { + // 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) + logger.Error( + "WARNING: ignored attempt to slash a nonexistent validator; we recommend you investigate immediately", + "validator", consAddr.String(), + ) + return + } + + // should not be slashing an unbonded validator + if validator.IsUnbonded() { + panic(fmt.Sprintf("should not be slashing unbonded validator: %s", validator.GetOperator())) + } + + operatorAddress := validator.GetOperator() + + // call the before-modification hook + k.hooks.BeforeValidatorModified(ctx, operatorAddress) + + // 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( + "slashing at current height; not scanning unbonding delegations & redelegations", + "height", infractionHeight, + ) + + case infractionHeight < ctx.BlockHeight(): + // Iterate through unbonding delegations from slashed validator + unbondingDelegations := k.GetUnbondingDelegationsFromValidator(ctx, operatorAddress) + for _, unbondingDelegation := range unbondingDelegations { + amountSlashed := k.SlashUnbondingDelegation(ctx, unbondingDelegation, infractionHeight, slashFactor) + if amountSlashed.IsZero() { + continue + } + remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed) + } + + // Iterate through redelegations from slashed source validator + redelegations := k.GetRedelegationsFromSrcValidator(ctx, operatorAddress) + for _, redelegation := range redelegations { + amountSlashed := k.SlashRedelegation(ctx, validator, redelegation, infractionHeight, slashFactor) + if amountSlashed.IsZero() { + continue + } + + remainingSlashAmount = remainingSlashAmount.Sub(amountSlashed) + } + } + + // cannot decrease balance below zero + tokensToBurn := sdk.MinInt(remainingSlashAmount, validator.Tokens) + tokensToBurn = sdk.MaxInt(tokensToBurn, sdk.ZeroInt()) // defensive. + + // we need to calculate the *effective* slash fraction for distribution + if validator.Tokens.IsPositive() { + effectiveFraction := sdk.NewDecFromInt(tokensToBurn).QuoRoundUp(sdk.NewDecFromInt(validator.Tokens)) + // possible if power has changed + if effectiveFraction.GT(math.LegacyOneDec()) { + effectiveFraction = math.LegacyOneDec() + } + // call the before-slashed hook + k.hooks.BeforeValidatorSlashed(ctx, operatorAddress, effectiveFraction) + } + + // Deduct from validator's bonded tokens and update the validator. + // Burn the slashed tokens from the pool account and decrease the total supply. + validator = k.RemoveValidatorTokens(ctx, validator, tokensToBurn) + + switch validator.GetStatus() { + case types.Bonded: + if err := k.burnBondedTokens(ctx, tokensToBurn); err != nil { + panic(err) + } + case types.Unbonding, types.Unbonded: + if err := k.burnNotBondedTokens(ctx, tokensToBurn); err != nil { + panic(err) + } + default: + panic("invalid validator status") + } + + logger.Info( + "validator slashed by slash factor", + "validator", validator.GetOperator().String(), + "slash_factor", slashFactor.String(), + "burned", tokensToBurn, + ) +} + +// SlashWithInfractionReason implementation doesn't require the infraction (types.Infraction) to work but is required by Interchain Security. +func (k Keeper) SlashWithInfractionReason(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec, _ types.Infraction) math.Int { + return k.LegacySlash(ctx, consAddr, infractionHeight, power, slashFactor) +} + +func (k Keeper) LegacySlash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec) math.Int { logger := k.Logger(ctx) if slashFactor.IsNegative() { @@ -156,11 +281,6 @@ func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeigh return tokensToBurn } -// SlashWithInfractionReason implementation doesn't require the infraction (types.Infraction) to work but is required by Interchain Security. -func (k Keeper) SlashWithInfractionReason(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec, _ types.Infraction) math.Int { - return k.Slash(ctx, consAddr, infractionHeight, power, slashFactor) -} - // jail a validator func (k Keeper) Jail(ctx sdk.Context, consAddr sdk.ConsAddress) { validator := k.mustGetValidatorByConsAddr(ctx, consAddr) diff --git a/x/staking/keeper/slash_test.go b/x/staking/keeper/slash_test.go index b1f6d8c873c..f3cc4ed43cb 100644 --- a/x/staking/keeper/slash_test.go +++ b/x/staking/keeper/slash_test.go @@ -3,6 +3,7 @@ package keeper_test import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/staking/testutil" + "github.com/cosmos/cosmos-sdk/x/staking/types" ) // tests Jail, Unjail @@ -46,5 +47,5 @@ func (s *KeeperTestSuite) TestSlashAtFutureHeight() { require.NoError(err) fraction := sdk.NewDecWithPrec(5, 1) - require.Panics(func() { keeper.Slash(ctx, consAddr, 1, 10, fraction) }) + require.Panics(func() { keeper.Slash(ctx, consAddr, 1, 10, fraction, types.Infraction_INFRACTION_UNSPECIFIED) }) } diff --git a/x/staking/types/expected_keepers.go b/x/staking/types/expected_keepers.go index 05672ee256d..7686ffe715b 100644 --- a/x/staking/types/expected_keepers.go +++ b/x/staking/types/expected_keepers.go @@ -61,7 +61,7 @@ type ValidatorSet interface { StakingTokenSupply(sdk.Context) math.Int // total staking token supply // slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction - Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec) math.Int + Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec, Infraction) SlashWithInfractionReason(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec, Infraction) math.Int Jail(sdk.Context, sdk.ConsAddress) // jail a validator Unjail(sdk.Context, sdk.ConsAddress) // unjail a validator