diff --git a/x/slashing/keeper.go b/x/slashing/keeper.go index c940f60c31..b64bd6e06b 100644 --- a/x/slashing/keeper.go +++ b/x/slashing/keeper.go @@ -67,10 +67,10 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, height int64, timestamp int64, logger := ctx.Logger().With("module", "x/slashing") age := ctx.BlockHeader().Time - timestamp if age > MaxEvidenceAge { - logger.Info(fmt.Sprintf("Ignored double sign from %v at height %d, age of %d past max age of %d", pubkey.Address(), height, age, MaxEvidenceAge)) + logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, age of %d past max age of %d", pubkey.Address(), height, age, MaxEvidenceAge)) return } - logger.Info(fmt.Sprintf("Confirmed double sign from %v at height %d, age of %d less than max age of %d", pubkey.Address(), height, age, MaxEvidenceAge)) + logger.Info(fmt.Sprintf("Confirmed double sign from %s at height %d, age of %d less than max age of %d", pubkey.Address(), height, age, MaxEvidenceAge)) k.stakeKeeper.Slash(ctx, pubkey, height, SlashFractionDoubleSign) } @@ -79,7 +79,7 @@ func (k Keeper) handleValidatorSignature(ctx sdk.Context, pubkey crypto.PubKey, logger := ctx.Logger().With("module", "x/slashing") height := ctx.BlockHeight() if !signed { - logger.Info(fmt.Sprintf("Absent validator %v at height %d", pubkey.Address(), height)) + logger.Info(fmt.Sprintf("Absent validator %s at height %d", pubkey.Address(), height)) } index := height % SignedBlocksWindow address := pubkey.Address() @@ -96,8 +96,9 @@ func (k Keeper) handleValidatorSignature(ctx sdk.Context, pubkey crypto.PubKey, } minHeight := signInfo.StartHeight + SignedBlocksWindow if height > minHeight && signInfo.SignedBlocksCounter < MinSignedPerWindow { + logger.Info(fmt.Sprintf("Validator %s past min height of %d and below signed blocks threshold of %d", pubkey.Address(), minHeight, MinSignedPerWindow)) k.stakeKeeper.Slash(ctx, pubkey, height, SlashFractionDowntime) - k.stakeKeeper.ForceUnbond(ctx, pubkey, DowntimeUnbondDuration) + k.stakeKeeper.ForceUnbond(ctx, pubkey, DowntimeUnbondDuration) // TODO } } diff --git a/x/slashing/keeper_test.go b/x/slashing/keeper_test.go index 429b14f937..dd4bb4cd69 100644 --- a/x/slashing/keeper_test.go +++ b/x/slashing/keeper_test.go @@ -58,7 +58,9 @@ func createTestInput(t *testing.T) (sdk.Context, bank.Keeper, stake.Keeper, Keep accountMapper := auth.NewAccountMapper(cdc, keyAcc, &auth.BaseAccount{}) ck := bank.NewKeeper(accountMapper) sk := stake.NewKeeper(cdc, keyStake, ck, stake.DefaultCodespace) - stake.InitGenesis(ctx, sk, stake.DefaultGenesisState()) + genesis := stake.DefaultGenesisState() + genesis.Pool.BondedTokens = initCoins * int64(len(addrs)) + stake.InitGenesis(ctx, sk, genesis) for _, addr := range addrs { ck.AddCoins(ctx, addr, sdk.Coins{ {sk.GetParams(ctx).BondDenom, initCoins}, @@ -86,7 +88,8 @@ func TestHandleDoubleSign(t *testing.T) { func TestHandleAbsentValidator(t *testing.T) { ctx, ck, sk, keeper := createTestInput(t) addr, val, amt := addrs[0], pks[0], int64(10) - got := stake.NewHandler(sk)(ctx, newTestMsgDeclareCandidacy(addr, val, amt)) + sh := stake.NewHandler(sk) + got := sh(ctx, newTestMsgDeclareCandidacy(addr, val, amt)) require.True(t, got.IsOK()) _ = sk.Tick(ctx) require.Equal(t, ck.GetCoins(ctx, addr), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins - amt}}) @@ -114,6 +117,9 @@ func TestHandleAbsentValidator(t *testing.T) { require.True(t, found) require.Equal(t, int64(0), info.StartHeight) require.Equal(t, SignedBlocksWindow-50, info.SignedBlocksCounter) + // should be bonded still + validator := sk.ValidatorByPubKey(ctx, val) + require.Equal(t, sdk.Bonded, validator.GetStatus()) // 51st block missed ctx = ctx.WithBlockHeight(height) keeper.handleValidatorSignature(ctx, val, false) @@ -122,6 +128,16 @@ func TestHandleAbsentValidator(t *testing.T) { require.Equal(t, int64(0), info.StartHeight) require.Equal(t, SignedBlocksWindow-51, info.SignedBlocksCounter) height++ + // should have been revoked + validator = sk.ValidatorByPubKey(ctx, val) + require.Equal(t, sdk.Unbonded, validator.GetStatus()) + got = sh(ctx, stake.NewMsgUnrevoke(addr)) + require.False(t, got.IsOK()) // should fail prior to jail expiration + ctx = ctx.WithBlockHeader(abci.Header{Time: int64(86400 * 2)}) + got = sh(ctx, stake.NewMsgUnrevoke(addr)) + require.True(t, got.IsOK()) // should succeed after jail expiration + validator = sk.ValidatorByPubKey(ctx, val) + require.Equal(t, sdk.Bonded, validator.GetStatus()) // should have been slashed require.Equal(t, sdk.NewRat(amt).Mul(sdk.NewRat(99).Quo(sdk.NewRat(100))), sk.Validator(ctx, addr).GetPower()) } diff --git a/x/stake/errors.go b/x/stake/errors.go index 83c38d528d..2664a56cad 100644 --- a/x/stake/errors.go +++ b/x/stake/errors.go @@ -16,6 +16,7 @@ const ( CodeInvalidValidator CodeType = 201 CodeInvalidBond CodeType = 202 CodeInvalidInput CodeType = 203 + CodeValidatorJailed CodeType = 204 CodeUnauthorized CodeType = sdk.CodeUnauthorized CodeInternal CodeType = sdk.CodeInternal CodeUnknownRequest CodeType = sdk.CodeUnknownRequest @@ -30,6 +31,8 @@ func codeToDefaultMsg(code CodeType) string { return "Invalid Bond" case CodeInvalidInput: return "Invalid Input" + case CodeValidatorJailed: + return "Validator Jailed" case CodeUnauthorized: return "Unauthorized" case CodeInternal: @@ -98,6 +101,9 @@ func ErrBadShares(codespace sdk.CodespaceType) sdk.Error { func ErrBadRemoveValidator(codespace sdk.CodespaceType) sdk.Error { return newError(codespace, CodeInvalidValidator, "Error removing validator") } +func ErrValidatorJailed(codespace sdk.CodespaceType) sdk.Error { + return newError(codespace, CodeValidatorJailed, "Validator jailed, cannot yet be unrevoked") +} //---------------------------------------- diff --git a/x/stake/handler.go b/x/stake/handler.go index 53653557cc..62c4f27df4 100644 --- a/x/stake/handler.go +++ b/x/stake/handler.go @@ -19,6 +19,8 @@ func NewHandler(k Keeper) sdk.Handler { return handleMsgDelegate(ctx, msg, k) case MsgUnbond: return handleMsgUnbond(ctx, msg, k) + case MsgUnrevoke: + return handleMsgUnrevoke(ctx, msg, k) default: return sdk.ErrTxDecode("invalid message parse in staking module").Result() } @@ -247,3 +249,26 @@ func handleMsgUnbond(ctx sdk.Context, msg MsgUnbond, k Keeper) sdk.Result { Tags: tags, } } + +func handleMsgUnrevoke(ctx sdk.Context, msg MsgUnrevoke, k Keeper) sdk.Result { + validator, found := k.GetValidator(ctx, msg.ValidatorAddr) + if !found { + return ErrNoValidatorForAddress(k.codespace).Result() + } + + if ctx.BlockHeader().Time < validator.RevokedUntilTime { + return ErrValidatorJailed(k.codespace).Result() + } + + if ctx.IsCheckTx() { + return sdk.Result{} + } + + validator.Revoked = false + k.updateValidator(ctx, validator) + + tags := sdk.NewTags("action", []byte("unrevoke"), "validator", msg.ValidatorAddr.Bytes()) + return sdk.Result{ + Tags: tags, + } +} diff --git a/x/stake/keeper.go b/x/stake/keeper.go index 00930aaa64..887cdab192 100644 --- a/x/stake/keeper.go +++ b/x/stake/keeper.go @@ -220,7 +220,9 @@ func (k Keeper) updateValidator(ctx sdk.Context, validator Validator) Validator oldValidator, oldFound := k.GetValidator(ctx, ownerAddr) if validator.Revoked && oldValidator.Status() == sdk.Bonded { + fmt.Printf("val preupdate: %v\n", validator) validator, pool = validator.UpdateStatus(pool, sdk.Unbonded) + fmt.Printf("val postupdate: %v\n", validator) k.setPool(ctx, pool) } @@ -783,25 +785,29 @@ func (k Keeper) Slash(ctx sdk.Context, pubkey crypto.PubKey, height int64, fract logger := ctx.Logger().With("module", "x/stake") val, found := k.GetValidatorByPubKey(ctx, pubkey) if !found { - panic(fmt.Errorf("Attempted to slash a nonexistent validator with pubkey %s", pubkey)) + panic(fmt.Errorf("Attempted to slash a nonexistent validator with address %s", pubkey.Address())) } sharesToRemove := val.PoolShares.Amount.Mul(fraction) pool := k.GetPool(ctx) val, pool, burned := val.removePoolShares(pool, sharesToRemove) k.setPool(ctx, pool) // update the pool k.updateValidator(ctx, val) // update the validator, possibly kicking it out - logger.Info(fmt.Sprintf("Validator %v slashed by fraction %v, removed %v shares and burned %d tokens", pubkey, fraction, sharesToRemove, burned)) + logger.Info(fmt.Sprintf("Validator %s slashed by fraction %v, removed %v shares and burned %d tokens", pubkey.Address(), fraction, sharesToRemove, burned)) return } // force unbond a validator func (k Keeper) ForceUnbond(ctx sdk.Context, pubkey crypto.PubKey, jailDuration int64) { - // TODO Implement - /* - val, found := k.GetValidatorByPubKey(ctx, pubkey) - if !found { - ctx.Logger().Info("Validator with pubkey %s not found, cannot force unbond", pubkey) - return - } - */ + logger := ctx.Logger().With("module", "x/stake") + val, found := k.GetValidatorByPubKey(ctx, pubkey) + if !found { + ctx.Logger().Info("Validator with pubkey %s not found, cannot force unbond", pubkey) + return + } + val.Revoked = true + val.RevokedUntilTime = ctx.BlockHeader().Time + jailDuration + k.updateValidator(ctx, val) // update the validator, now revoked + val, _ = k.GetValidatorByPubKey(ctx, pubkey) + logger.Info(fmt.Sprintf("Validator %s revoked for minimum duration %d", pubkey.Address(), jailDuration)) + return } diff --git a/x/stake/msg.go b/x/stake/msg.go index 0adff84d9b..a668d47d18 100644 --- a/x/stake/msg.go +++ b/x/stake/msg.go @@ -209,3 +209,36 @@ func (msg MsgUnbond) ValidateBasic() sdk.Error { } return nil } + +//______________________________________________________________________ + +// MsgUnrevoke - struct for unrevoking revoked validator +type MsgUnrevoke struct { + ValidatorAddr sdk.Address `json:"address"` +} + +func NewMsgUnrevoke(validatorAddr sdk.Address) MsgUnrevoke { + return MsgUnrevoke{ + ValidatorAddr: validatorAddr, + } +} + +func (msg MsgUnrevoke) Type() string { return MsgType } +func (msg MsgUnrevoke) GetSigners() []sdk.Address { return []sdk.Address{msg.ValidatorAddr} } + +// get the bytes for the message signer to sign on +func (msg MsgUnrevoke) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} + +// quick validity check +func (msg MsgUnrevoke) ValidateBasic() sdk.Error { + if msg.ValidatorAddr == nil { + return ErrBadValidatorAddr(DefaultCodespace) + } + return nil +} diff --git a/x/stake/validator.go b/x/stake/validator.go index 31ef290618..76905212f1 100644 --- a/x/stake/validator.go +++ b/x/stake/validator.go @@ -17,9 +17,10 @@ import ( // exchange rate. Voting power can be calculated as total bonds multiplied by // exchange rate. type Validator struct { - Owner sdk.Address `json:"owner"` // sender of BondTx - UnbondTx returns here - PubKey crypto.PubKey `json:"pub_key"` // pubkey of validator - Revoked bool `json:"revoked"` // has the validator been revoked from bonded status? + Owner sdk.Address `json:"owner"` // sender of BondTx - UnbondTx returns here + PubKey crypto.PubKey `json:"pub_key"` // pubkey of validator + Revoked bool `json:"revoked"` // has the validator been revoked from bonded status? + RevokedUntilTime int64 `json:"revoked_until_time"` // timestamp before which the validator cannot unrevoke PoolShares PoolShares `json:"pool_shares"` // total shares for tokens held in the pool DelegatorShares sdk.Rat `json:"delegator_shares"` // total shares issued to a validator's delegators @@ -46,6 +47,8 @@ func NewValidator(owner sdk.Address, pubKey crypto.PubKey, description Descripti return Validator{ Owner: owner, PubKey: pubKey, + Revoked: false, + RevokedUntilTime: int64(0), PoolShares: NewUnbondedShares(sdk.ZeroRat()), DelegatorShares: sdk.ZeroRat(), Description: description, diff --git a/x/stake/wire.go b/x/stake/wire.go index 6e6e382606..ac382ff14d 100644 --- a/x/stake/wire.go +++ b/x/stake/wire.go @@ -10,6 +10,7 @@ func RegisterWire(cdc *wire.Codec) { cdc.RegisterConcrete(MsgEditCandidacy{}, "cosmos-sdk/MsgEditCandidacy", nil) cdc.RegisterConcrete(MsgDelegate{}, "cosmos-sdk/MsgDelegate", nil) cdc.RegisterConcrete(MsgUnbond{}, "cosmos-sdk/MsgUnbond", nil) + cdc.RegisterConcrete(MsgUnrevoke{}, "cosmos-sdk/MsgUnrevoke", nil) } var cdcEmpty = wire.NewCodec()