diff --git a/x/slashing/keeper/hooks.go b/x/slashing/keeper/hooks.go index c51adae071c7..a7a0d5b6c8de 100644 --- a/x/slashing/keeper/hooks.go +++ b/x/slashing/keeper/hooks.go @@ -9,6 +9,16 @@ import ( "github.com/cosmos/cosmos-sdk/x/slashing/types" ) +// Implements SlashingHooks interface +var _ types.SlashingHooks = Keeper{} + +// AfterValidatorDowntime - call hook if registered +func (k Keeper) AfterValidatorDowntime(ctx sdk.Context, consAddress sdk.ConsAddress, power int64) { + if k.hooks != nil { + k.hooks.AfterValidatorDowntime(ctx, consAddress, power) + } +} + func (k Keeper) AfterValidatorBonded(ctx sdk.Context, address sdk.ConsAddress, _ sdk.ValAddress) { // Update the signing info start height or create a new signing info _, found := k.GetValidatorSigningInfo(ctx, address) diff --git a/x/slashing/keeper/infractions.go b/x/slashing/keeper/infractions.go index 06baa48f82b4..3d4b35d719c1 100644 --- a/x/slashing/keeper/infractions.go +++ b/x/slashing/keeper/infractions.go @@ -15,6 +15,7 @@ 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)) } @@ -101,7 +102,7 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre // We need to reset the counter & array so that the validator won't be immediately slashed for downtime upon rebonding. signInfo.MissedBlocksCounter = 0 signInfo.IndexOffset = 0 - k.clearValidatorMissedBlockBitArray(ctx, consAddr) + k.ClearValidatorMissedBlockBitArray(ctx, consAddr) logger.Info( "slashing and jailing validator due to liveness fault", @@ -119,6 +120,10 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre "validator", consAddr.String(), ) } + + // hook is triggered for each downtime detection + // and defered to keep safe the write operations on SignInfo + defer k.AfterValidatorDowntime(ctx, consAddr, power) } // Set the updated signing info diff --git a/x/slashing/keeper/keeper.go b/x/slashing/keeper/keeper.go index 12a943c84c69..0df7fefa8760 100644 --- a/x/slashing/keeper/keeper.go +++ b/x/slashing/keeper/keeper.go @@ -17,6 +17,7 @@ type Keeper struct { cdc codec.BinaryCodec sk types.StakingKeeper paramspace types.ParamSubspace + hooks types.SlashingHooks } // NewKeeper creates a slashing keeper @@ -94,3 +95,13 @@ func (k Keeper) deleteAddrPubkeyRelation(ctx sdk.Context, addr cryptotypes.Addre store := ctx.KVStore(k.storeKey) store.Delete(types.AddrPubkeyRelationKey(addr)) } + +func (k *Keeper) SetHooks(sh types.SlashingHooks) *Keeper { + if k.hooks != nil { + panic("cannot set validator hooks twice") + } + + k.hooks = sh + + return k +} diff --git a/x/slashing/keeper/keeper_test.go b/x/slashing/keeper/keeper_test.go index 7f3d42b0a7ed..daa6c791ea20 100644 --- a/x/slashing/keeper/keeper_test.go +++ b/x/slashing/keeper/keeper_test.go @@ -4,12 +4,15 @@ import ( "testing" "time" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/stretchr/testify/require" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/slashing/testslashing" + "github.com/cosmos/cosmos-sdk/x/slashing/types" "github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/staking/teststaking" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -276,3 +279,52 @@ func TestValidatorDippingInAndOut(t *testing.T) { staking.EndBlocker(ctx, app.StakingKeeper) tstaking.CheckValidator(valAddr, stakingtypes.Unbonding, true) } + +type MockSlashingHooks struct { + triggered bool +} + +func (h *MockSlashingHooks) AfterValidatorDowntime(_ sdk.Context, _ sdk.ConsAddress, _ int64) { + h.triggered = true +} + +// Test hook is triggered when validator is down +func TestValidatorDowntimedHook(t *testing.T) { + // initial setup + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + // create a validator pubkey and address + pubkey := ed25519.GenPrivKey().PubKey() + consAddr := sdk.ConsAddress(pubkey.Address()) + + // store the validator pubkey and signing info + app.SlashingKeeper.AddPubkey(ctx, pubkey) + valInfo := types.NewValidatorSigningInfo(consAddr, ctx.BlockHeight(), ctx.BlockHeight()-1, + time.Time{}.UTC(), false, int64(0)) + app.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr, valInfo) + + // define a slashing hook mock + + mh := MockSlashingHooks{} + app.SlashingKeeper.SetHooks(&mh) + + // 1000 first blocks OK + height := int64(0) + power := int64(1) + + for ; height < app.SlashingKeeper.SignedBlocksWindow(ctx); height++ { + ctx = ctx.WithBlockHeight(height) + app.SlashingKeeper.HandleValidatorSignature(ctx, pubkey.Address(), power, true) + } + // hook shouldn't be triggered + require.False(t, mh.triggered) + + // 501 blocks missed + for ; height < app.SlashingKeeper.SignedBlocksWindow(ctx)+(app.SlashingKeeper.SignedBlocksWindow(ctx)-app.SlashingKeeper.MinSignedPerWindow(ctx))+1; height++ { + ctx = ctx.WithBlockHeight(height) + app.SlashingKeeper.HandleValidatorSignature(ctx, pubkey.Address(), power, false) + } + + require.True(t, mh.triggered) +} diff --git a/x/slashing/keeper/signing_info.go b/x/slashing/keeper/signing_info.go index ed15ae3e4ff3..c3b9e1d6421a 100644 --- a/x/slashing/keeper/signing_info.go +++ b/x/slashing/keeper/signing_info.go @@ -148,7 +148,7 @@ func (k Keeper) SetValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.Con } // clearValidatorMissedBlockBitArray deletes every instance of ValidatorMissedBlockBitArray in the store -func (k Keeper) clearValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress) { +func (k Keeper) ClearValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress) { store := ctx.KVStore(k.storeKey) iter := sdk.KVStorePrefixIterator(store, types.ValidatorMissedBlockBitArrayPrefixKey(address)) defer iter.Close() diff --git a/x/slashing/types/expected_keepers.go b/x/slashing/types/expected_keepers.go index 9710ad1786e7..98acd937b3e8 100644 --- a/x/slashing/types/expected_keepers.go +++ b/x/slashing/types/expected_keepers.go @@ -61,3 +61,9 @@ type StakingHooks interface { AfterValidatorBonded(ctx sdk.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) // Must be called when a validator is bonded } + +// SlashingHooks event hooks for jailing and slashing validator +type SlashingHooks interface { + // Is triggered when the validator missed too many blocks + AfterValidatorDowntime(ctx sdk.Context, consAddr sdk.ConsAddress, power int64) +}