From 3654579ea79473f7f306816f4fe0c3e0437c6d58 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Sat, 30 Jun 2018 05:34:55 +0200 Subject: [PATCH] Merge PR #1278: Slashing v2 Implement semifinal Gaia slashing spec (#1263), less #1348, #1378, and #1440 which are TBD. --- .circleci/config.yml | 3 +- CHANGELOG.md | 7 + Gopkg.lock | 2 +- client/lcd/lcd_test.go | 24 ++ client/lcd/root.go | 2 + docs/spec/slashing/end_block.md | 51 +-- examples/democoin/mock/validator.go | 7 +- types/stake.go | 8 +- x/slashing/client/rest/query.go | 62 ++++ x/slashing/client/rest/rest.go | 15 + x/slashing/client/rest/tx.go | 103 ++++++ x/slashing/handler.go | 2 +- x/slashing/keeper.go | 35 +- x/slashing/keeper_test.go | 83 +++-- x/slashing/params.go | 17 +- x/slashing/tick.go | 28 +- x/slashing/tick_test.go | 8 +- x/stake/handler_test.go | 85 ++++- x/stake/keeper/delegation.go | 54 +++- x/stake/keeper/slash.go | 228 +++++++++++-- x/stake/keeper/slash_test.go | 481 ++++++++++++++++++++++++++++ x/stake/types/delegation.go | 3 + x/stake/types/validator.go | 1 + 23 files changed, 1192 insertions(+), 117 deletions(-) create mode 100644 x/slashing/client/rest/query.go create mode 100644 x/slashing/client/rest/rest.go create mode 100644 x/slashing/client/rest/tx.go create mode 100644 x/stake/keeper/slash_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index d58cbad5fc27..cf679fe75f4c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -101,7 +101,7 @@ jobs: test_cover: <<: *defaults - parallelism: 1 + parallelism: 2 steps: - attach_workspace: at: /tmp/workspace @@ -126,6 +126,7 @@ jobs: upload_coverage: <<: *defaults + parallelism: 1 steps: - attach_workspace: at: /tmp/workspace diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0759e5a101..e0b543247a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,13 @@ BREAKING CHANGES * `gaiacli stake complete-unbonding` * `gaiacli stake begin-redelegation` * `gaiacli stake complete-redelegation` +* [slashing] update slashing for unbonding period + * Slash according to power at time of infraction instead of power at + time of discovery + * Iterate through unbonding delegations & redelegations which contributed + to an infraction, slash them proportional to their stake at the time + * Add REST endpoint to unrevoke a validator previously revoked for downtime + * Add REST endpoint to retrieve liveness signing information for a validator FEATURES * [gaiacli] You can now attach a simple text-only memo to any transaction, with the `--memo` flag diff --git a/Gopkg.lock b/Gopkg.lock index a974602f74e9..c48188cb8d72 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -443,7 +443,7 @@ "netutil", "trace" ] - revision = "e514e69ffb8bc3c76a71ae40de0118d794855992" + revision = "97aa3a539ec716117a9d15a4659a911f50d13c3c" [[projects]] branch = "master" diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 37d7e919166a..049fa04bef56 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -25,6 +25,7 @@ import ( "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/stake" stakerest "github.com/cosmos/cosmos-sdk/x/stake/client/rest" ) @@ -521,6 +522,19 @@ func TestVote(t *testing.T) { require.Equal(t, gov.VoteOptionToString(gov.OptionYes), vote.Option) } +func TestUnrevoke(t *testing.T) { + _, password := "test", "1234567890" + addr, _ := CreateAddr(t, "test", password, GetKB(t)) + cleanup, pks, port := InitializeTestLCD(t, 1, []sdk.Address{addr}) + defer cleanup() + + signingInfo := getSigningInfo(t, port, pks[0].Address()) + tests.WaitForHeight(4, port) + require.Equal(t, true, signingInfo.IndexOffset > 0) + require.Equal(t, int64(0), signingInfo.JailedUntil) + require.Equal(t, true, signingInfo.SignedBlocksCounter > 0) +} + func TestProposalsQuery(t *testing.T) { name, password1 := "test", "1234567890" name2, password2 := "test2", "1234567890" @@ -679,6 +693,16 @@ func doIBCTransfer(t *testing.T, port, seed, name, password string, addr sdk.Add return resultTx } +func getSigningInfo(t *testing.T, port string, validatorAddr sdk.Address) slashing.ValidatorSigningInfo { + validatorAddrBech := sdk.MustBech32ifyVal(validatorAddr) + res, body := Request(t, port, "GET", "/slashing/signing_info/"+validatorAddrBech, nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + var signingInfo slashing.ValidatorSigningInfo + err := cdc.UnmarshalJSON([]byte(body), &signingInfo) + require.Nil(t, err) + return signingInfo +} + func getDelegation(t *testing.T, port string, delegatorAddr, validatorAddr sdk.Address) stake.Delegation { delegatorAddrBech := sdk.MustBech32ifyAcc(delegatorAddr) diff --git a/client/lcd/root.go b/client/lcd/root.go index d84e7d9f1ccb..472c91406025 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -22,6 +22,7 @@ import ( bank "github.com/cosmos/cosmos-sdk/x/bank/client/rest" gov "github.com/cosmos/cosmos-sdk/x/gov/client/rest" ibc "github.com/cosmos/cosmos-sdk/x/ibc/client/rest" + slashing "github.com/cosmos/cosmos-sdk/x/slashing/client/rest" stake "github.com/cosmos/cosmos-sdk/x/stake/client/rest" ) @@ -84,6 +85,7 @@ func createHandler(cdc *wire.Codec) http.Handler { bank.RegisterRoutes(ctx, r, cdc, kb) ibc.RegisterRoutes(ctx, r, cdc, kb) stake.RegisterRoutes(ctx, r, cdc, kb) + slashing.RegisterRoutes(ctx, r, cdc, kb) gov.RegisterRoutes(ctx, r, cdc) return r } diff --git a/docs/spec/slashing/end_block.md b/docs/spec/slashing/end_block.md index 6ac24138baa4..e923fd8444d9 100644 --- a/docs/spec/slashing/end_block.md +++ b/docs/spec/slashing/end_block.md @@ -15,33 +15,28 @@ For some `evidence` to be valid, it must satisfy: where `evidence.Timestamp` is the timestamp in the block at height `evidence.Height` and `block.Timestamp` is the current block timestamp. -If valid evidence is included in a block, the validator's stake is reduced by `SLASH_PROPORTION` of -what their stake was when the equivocation occurred (rather than when the evidence was discovered): +If valid evidence is included in a block, the validator's stake is reduced by `SLASH_PROPORTION` of +what their stake was when the infraction occurred (rather than when the evidence was discovered). +We want to "follow the stake": the stake which contributed to the infraction should be +slashed, even if it has since been redelegated or started unbonding. -``` -curVal := validator -oldVal := loadValidator(evidence.Height, evidence.Address) - -slashAmount := SLASH_PROPORTION * oldVal.Shares +We first need to loop through the unbondings and redelegations from the slashed validator +and track how much stake has since moved: -curVal.Shares = max(0, curVal.Shares - slashAmount) ``` +slashAmountUnbondings := 0 +slashAmountRedelegations := 0 -This ensures that offending validators are punished the same amount whether they -act as a single validator with X stake or as N validators with collectively X -stake. - -We also need to loop through the unbondings and redelegations to slash them as -well: - -``` unbondings := getUnbondings(validator.Address) for unbond in unbondings { - if was not bonded before evidence.Height { + + if was not bonded before evidence.Height or started unbonding before unbonding period ago { continue } - unbond.InitialTokens + burn := unbond.InitialTokens * SLASH_PROPORTION + slashAmountUnbondings += burn + unbond.Tokens = max(0, unbond.Tokens - burn) } @@ -51,17 +46,35 @@ for unbond in unbondings { redels := getRedelegationsBySource(validator.Address) for redel in redels { - if was not bonded before evidence.Height { + if was not bonded before evidence.Height or started redelegating before unbonding period ago { continue } burn := redel.InitialTokens * SLASH_PROPORTION + slashAmountRedelegations += burn amount := unbondFromValidator(redel.Destination, burn) destroy(amount) } ``` +We then slash the validator: + +``` +curVal := validator +oldVal := loadValidator(evidence.Height, evidence.Address) + +slashAmount := SLASH_PROPORTION * oldVal.Shares +slashAmount -= slashAmountUnbondings +slashAmount -= slashAmountRedelegations + +curVal.Shares = max(0, curVal.Shares - slashAmount) +``` + +This ensures that offending validators are punished the same amount whether they +act as a single validator with X stake or as N validators with collectively X +stake. + ## Automatic Unbonding At the beginning of each block, we update the signing info for each validator and check if they should be automatically unbonded: diff --git a/examples/democoin/mock/validator.go b/examples/democoin/mock/validator.go index 869c48dcb651..8b1d34c7b43f 100644 --- a/examples/democoin/mock/validator.go +++ b/examples/democoin/mock/validator.go @@ -38,6 +38,11 @@ func (v Validator) GetDelegatorShares() sdk.Rat { return sdk.ZeroRat() } +// Implements sdk.Validator +func (v Validator) GetRevoked() bool { + return false +} + // Implements sdk.Validator func (v Validator) GetBondHeight() int64 { return 0 @@ -107,7 +112,7 @@ func (vs *ValidatorSet) RemoveValidator(addr sdk.Address) { } // Implements sdk.ValidatorSet -func (vs *ValidatorSet) Slash(ctx sdk.Context, pubkey crypto.PubKey, height int64, amt sdk.Rat) { +func (vs *ValidatorSet) Slash(ctx sdk.Context, pubkey crypto.PubKey, height int64, power int64, amt sdk.Rat) { panic("not implemented") } diff --git a/types/stake.go b/types/stake.go index 0f0855b37fd3..8625b617e5d0 100644 --- a/types/stake.go +++ b/types/stake.go @@ -32,6 +32,7 @@ func BondStatusToString(b BondStatus) string { // validator for a delegated proof of stake system type Validator interface { + GetRevoked() bool // whether the validator is revoked GetMoniker() string // moniker of the validator GetStatus() BondStatus // status of the validator GetOwner() Address // owner address to receive/return validators coins @@ -62,9 +63,10 @@ type ValidatorSet interface { Validator(Context, Address) Validator // get a particular validator by owner address TotalPower(Context) Rat // total power of the validator set - Slash(Context, crypto.PubKey, int64, Rat) // slash the validator and delegators of the validator, specifying offence height & slash fraction - Revoke(Context, crypto.PubKey) // revoke a validator - Unrevoke(Context, crypto.PubKey) // unrevoke a validator + // slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction + Slash(Context, crypto.PubKey, int64, int64, Rat) + Revoke(Context, crypto.PubKey) // revoke a validator + Unrevoke(Context, crypto.PubKey) // unrevoke a validator } //_______________________________________________________________________________ diff --git a/x/slashing/client/rest/query.go b/x/slashing/client/rest/query.go new file mode 100644 index 000000000000..9842ada73231 --- /dev/null +++ b/x/slashing/client/rest/query.go @@ -0,0 +1,62 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/slashing" +) + +func registerQueryRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec) { + r.HandleFunc( + "/slashing/signing_info/{validator}", + signingInfoHandlerFn(ctx, "slashing", cdc), + ).Methods("GET") +} + +// http request handler to query signing info +func signingInfoHandlerFn(ctx context.CoreContext, storeName string, cdc *wire.Codec) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + // read parameters + vars := mux.Vars(r) + bech32validator := vars["validator"] + + validatorAddr, err := sdk.GetValAddressBech32(bech32validator) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + key := slashing.GetValidatorSigningInfoKey(validatorAddr) + res, err := ctx.QueryStore(key, storeName) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("couldn't query signing info. Error: %s", err.Error()))) + return + } + + var signingInfo slashing.ValidatorSigningInfo + err = cdc.UnmarshalBinary(res, &signingInfo) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("couldn't decode signing info. Error: %s", err.Error()))) + return + } + + output, err := cdc.MarshalJSON(signingInfo) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.Write(output) + } +} diff --git a/x/slashing/client/rest/rest.go b/x/slashing/client/rest/rest.go new file mode 100644 index 000000000000..156d400334c7 --- /dev/null +++ b/x/slashing/client/rest/rest.go @@ -0,0 +1,15 @@ +package rest + +import ( + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/wire" +) + +// RegisterRoutes registers staking-related REST handlers to a router +func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { + registerQueryRoutes(ctx, r, cdc) + registerTxRoutes(ctx, r, cdc, kb) +} diff --git a/x/slashing/client/rest/tx.go b/x/slashing/client/rest/tx.go new file mode 100644 index 000000000000..212487ddf121 --- /dev/null +++ b/x/slashing/client/rest/tx.go @@ -0,0 +1,103 @@ +package rest + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/crypto/keys" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/slashing" +) + +func registerTxRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { + r.HandleFunc( + "/slashing/unrevoke", + unrevokeRequestHandlerFn(cdc, kb, ctx), + ).Methods("POST") +} + +// Unrevoke TX body +type UnrevokeBody struct { + LocalAccountName string `json:"name"` + Password string `json:"password"` + ChainID string `json:"chain_id"` + AccountNumber int64 `json:"account_number"` + Sequence int64 `json:"sequence"` + Gas int64 `json:"gas"` + ValidatorAddr string `json:"validator_addr"` +} + +func unrevokeRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, ctx context.CoreContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var m UnrevokeBody + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + err = json.Unmarshal(body, &m) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + info, err := kb.Get(m.LocalAccountName) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(err.Error())) + return + } + + validatorAddr, err := sdk.GetAccAddressBech32(m.ValidatorAddr) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Couldn't decode validator. Error: %s", err.Error()))) + return + } + + if !bytes.Equal(info.GetPubKey().Address(), validatorAddr) { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Must use own validator address")) + return + } + + ctx = ctx.WithGas(m.Gas) + ctx = ctx.WithChainID(m.ChainID) + ctx = ctx.WithAccountNumber(m.AccountNumber) + ctx = ctx.WithSequence(m.Sequence) + + msg := slashing.NewMsgUnrevoke(validatorAddr) + + txBytes, err := ctx.SignAndBuild(m.LocalAccountName, m.Password, []sdk.Msg{msg}, cdc) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(err.Error())) + return + } + + res, err := ctx.BroadcastTx(txBytes) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + output, err := json.MarshalIndent(res, "", " ") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.Write(output) + } +} diff --git a/x/slashing/handler.go b/x/slashing/handler.go index 5994bb8f197c..e786f34aca2f 100644 --- a/x/slashing/handler.go +++ b/x/slashing/handler.go @@ -50,7 +50,7 @@ func handleMsgUnrevoke(ctx sdk.Context, msg MsgUnrevoke, k Keeper) sdk.Result { // Unrevoke the validator k.validatorSet.Unrevoke(ctx, validator.GetPubKey()) - tags := sdk.NewTags("action", []byte("unrevoke"), "validator", msg.ValidatorAddr.Bytes()) + tags := sdk.NewTags("action", []byte("unrevoke"), "validator", []byte(msg.ValidatorAddr.String())) return sdk.Result{ Tags: tags, diff --git a/x/slashing/keeper.go b/x/slashing/keeper.go index 1d62e6dafffa..9ad1c1ea259f 100644 --- a/x/slashing/keeper.go +++ b/x/slashing/keeper.go @@ -30,28 +30,40 @@ func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, vs sdk.ValidatorSet, codespace } // handle a validator signing two blocks at the same height -func (k Keeper) handleDoubleSign(ctx sdk.Context, height int64, timestamp int64, pubkey crypto.PubKey) { +func (k Keeper) handleDoubleSign(ctx sdk.Context, pubkey crypto.PubKey, infractionHeight int64, timestamp int64, power int64) { logger := ctx.Logger().With("module", "x/slashing") - age := ctx.BlockHeader().Time - timestamp + time := ctx.BlockHeader().Time + age := time - timestamp + address := pubkey.Address() // Double sign too old if 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)) + logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, age of %d past max age of %d", pubkey.Address(), infractionHeight, age, MaxEvidenceAge)) return } // Double sign confirmed - 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.validatorSet.Slash(ctx, pubkey, height, SlashFractionDoubleSign) + logger.Info(fmt.Sprintf("Confirmed double sign from %s at height %d, age of %d less than max age of %d", pubkey.Address(), infractionHeight, age, MaxEvidenceAge)) + + // Slash validator + k.validatorSet.Slash(ctx, pubkey, infractionHeight, power, SlashFractionDoubleSign) + + // Revoke validator + k.validatorSet.Revoke(ctx, pubkey) + + // Jail validator + signInfo, found := k.getValidatorSigningInfo(ctx, address) + if !found { + panic(fmt.Sprintf("Expected signing info for validator %s but not found", address)) + } + signInfo.JailedUntil = time + DoubleSignUnbondDuration + k.setValidatorSigningInfo(ctx, address, signInfo) } // handle a validator signature, must be called once per validator per block -func (k Keeper) handleValidatorSignature(ctx sdk.Context, pubkey crypto.PubKey, signed bool) { +func (k Keeper) handleValidatorSignature(ctx sdk.Context, pubkey crypto.PubKey, power int64, signed bool) { logger := ctx.Logger().With("module", "x/slashing") height := ctx.BlockHeight() - if !signed { - logger.Info(fmt.Sprintf("Absent validator %s at height %d", pubkey.Address(), height)) - } address := pubkey.Address() // Local index, so counts blocks validator *should* have signed @@ -80,11 +92,14 @@ func (k Keeper) handleValidatorSignature(ctx sdk.Context, pubkey crypto.PubKey, signInfo.SignedBlocksCounter++ } + if !signed { + logger.Info(fmt.Sprintf("Absent validator %s at height %d, %d signed, threshold %d", pubkey.Address(), height, signInfo.SignedBlocksCounter, MinSignedPerWindow)) + } minHeight := signInfo.StartHeight + SignedBlocksWindow if height > minHeight && signInfo.SignedBlocksCounter < MinSignedPerWindow { // Downtime confirmed, slash, revoke, and jail the validator logger.Info(fmt.Sprintf("Validator %s past min height of %d and below signed blocks threshold of %d", pubkey.Address(), minHeight, MinSignedPerWindow)) - k.validatorSet.Slash(ctx, pubkey, height, SlashFractionDowntime) + k.validatorSet.Slash(ctx, pubkey, height, power, SlashFractionDowntime) k.validatorSet.Revoke(ctx, pubkey) signInfo.JailedUntil = ctx.BlockHeader().Time + DowntimeUnbondDuration } diff --git a/x/slashing/keeper_test.go b/x/slashing/keeper_test.go index 2a722a2badb1..debeda6cca69 100644 --- a/x/slashing/keeper_test.go +++ b/x/slashing/keeper_test.go @@ -11,26 +11,45 @@ import ( "github.com/cosmos/cosmos-sdk/x/stake" ) +// Have to change these parameters for tests +// lest the tests take forever +func init() { + SignedBlocksWindow = 1000 + MinSignedPerWindow = SignedBlocksWindow / 2 + DowntimeUnbondDuration = 60 * 60 + DoubleSignUnbondDuration = 60 * 60 +} + // Test that a validator is slashed correctly -// when we discover evidence of equivocation +// when we discover evidence of infraction func TestHandleDoubleSign(t *testing.T) { // initial setup ctx, ck, sk, keeper := createTestInput(t) - addr, val, amt := addrs[0], pks[0], sdk.NewInt(100) + amtInt := int64(100) + addr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) got := stake.NewHandler(sk)(ctx, newTestMsgCreateValidator(addr, val, amt)) require.True(t, got.IsOK()) stake.EndBlocker(ctx, sk) require.Equal(t, ck.GetCoins(ctx, addr), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) require.True(t, sdk.NewRatFromInt(amt).Equal(sk.Validator(ctx, addr).GetPower())) + // handle a signature to set signing info + keeper.handleValidatorSignature(ctx, val, amtInt, true) + // double sign less than max age - keeper.handleDoubleSign(ctx, 0, 0, val) + keeper.handleDoubleSign(ctx, val, 0, 0, amtInt) + + // should be revoked + require.True(t, sk.Validator(ctx, addr).GetRevoked()) + // unrevoke to measure power + sk.Unrevoke(ctx, val) + // power should be reduced require.Equal(t, sdk.NewRatFromInt(amt).Mul(sdk.NewRat(19).Quo(sdk.NewRat(20))), sk.Validator(ctx, addr).GetPower()) - ctx = ctx.WithBlockHeader(abci.Header{Time: 300}) + ctx = ctx.WithBlockHeader(abci.Header{Time: 1 + MaxEvidenceAge}) // double sign past max age - keeper.handleDoubleSign(ctx, 0, 0, val) + keeper.handleDoubleSign(ctx, val, 0, 0, amtInt) require.Equal(t, sdk.NewRatFromInt(amt).Mul(sdk.NewRat(19).Quo(sdk.NewRat(20))), sk.Validator(ctx, addr).GetPower()) } @@ -40,7 +59,8 @@ func TestHandleAbsentValidator(t *testing.T) { // initial setup ctx, ck, sk, keeper := createTestInput(t) - addr, val, amt := addrs[0], pks[0], sdk.NewInt(100) + amtInt := int64(100) + addr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) sh := stake.NewHandler(sk) slh := NewHandler(keeper) got := sh(ctx, newTestMsgCreateValidator(addr, val, amt)) @@ -57,38 +77,38 @@ func TestHandleAbsentValidator(t *testing.T) { height := int64(0) // 1000 first blocks OK - for ; height < 1000; height++ { + for ; height < SignedBlocksWindow; height++ { ctx = ctx.WithBlockHeight(height) - keeper.handleValidatorSignature(ctx, val, true) + keeper.handleValidatorSignature(ctx, val, amtInt, true) } info, found = keeper.getValidatorSigningInfo(ctx, val.Address()) require.True(t, found) require.Equal(t, int64(0), info.StartHeight) require.Equal(t, SignedBlocksWindow, info.SignedBlocksCounter) - // 50 blocks missed - for ; height < 1050; height++ { + // 500 blocks missed + for ; height < SignedBlocksWindow+(SignedBlocksWindow-MinSignedPerWindow); height++ { ctx = ctx.WithBlockHeight(height) - keeper.handleValidatorSignature(ctx, val, false) + keeper.handleValidatorSignature(ctx, val, amtInt, false) } info, found = keeper.getValidatorSigningInfo(ctx, val.Address()) require.True(t, found) require.Equal(t, int64(0), info.StartHeight) - require.Equal(t, SignedBlocksWindow-50, info.SignedBlocksCounter) + require.Equal(t, SignedBlocksWindow-MinSignedPerWindow, info.SignedBlocksCounter) // validator should be bonded still validator, _ := sk.GetValidatorByPubKey(ctx, val) require.Equal(t, sdk.Bonded, validator.GetStatus()) pool := sk.GetPool(ctx) - require.Equal(t, int64(100), pool.BondedTokens) + require.Equal(t, int64(amtInt), pool.BondedTokens) - // 51st block missed + // 501st block missed ctx = ctx.WithBlockHeight(height) - keeper.handleValidatorSignature(ctx, val, false) + keeper.handleValidatorSignature(ctx, val, amtInt, false) info, found = keeper.getValidatorSigningInfo(ctx, val.Address()) require.True(t, found) require.Equal(t, int64(0), info.StartHeight) - require.Equal(t, SignedBlocksWindow-51, info.SignedBlocksCounter) + require.Equal(t, SignedBlocksWindow-MinSignedPerWindow-1, info.SignedBlocksCounter) // validator should have been revoked validator, _ = sk.GetValidatorByPubKey(ctx, val) @@ -99,7 +119,7 @@ func TestHandleAbsentValidator(t *testing.T) { require.False(t, got.IsOK()) // unrevocation should succeed after jail expiration - ctx = ctx.WithBlockHeader(abci.Header{Time: int64(86400 * 2)}) + ctx = ctx.WithBlockHeader(abci.Header{Time: DowntimeUnbondDuration + 1}) got = slh(ctx, NewMsgUnrevoke(addr)) require.True(t, got.IsOK()) @@ -109,26 +129,33 @@ func TestHandleAbsentValidator(t *testing.T) { // validator should have been slashed pool = sk.GetPool(ctx) - require.Equal(t, int64(99), pool.BondedTokens) + require.Equal(t, int64(amtInt-1), pool.BondedTokens) // validator start height should have been changed info, found = keeper.getValidatorSigningInfo(ctx, val.Address()) require.True(t, found) require.Equal(t, height, info.StartHeight) - require.Equal(t, SignedBlocksWindow-51, info.SignedBlocksCounter) + require.Equal(t, SignedBlocksWindow-MinSignedPerWindow-1, info.SignedBlocksCounter) // validator should not be immediately revoked again height++ ctx = ctx.WithBlockHeight(height) - keeper.handleValidatorSignature(ctx, val, false) + keeper.handleValidatorSignature(ctx, val, amtInt, false) validator, _ = sk.GetValidatorByPubKey(ctx, val) require.Equal(t, sdk.Bonded, validator.GetStatus()) - // validator should be revoked again after 100 unsigned blocks - nextHeight := height + 100 + // 500 signed blocks + nextHeight := height + MinSignedPerWindow + 1 + for ; height < nextHeight; height++ { + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val, amtInt, false) + } + + // validator should be revoked again after 500 unsigned blocks + nextHeight = height + MinSignedPerWindow + 1 for ; height <= nextHeight; height++ { ctx = ctx.WithBlockHeight(height) - keeper.handleValidatorSignature(ctx, val, false) + keeper.handleValidatorSignature(ctx, val, amtInt, false) } validator, _ = sk.GetValidatorByPubKey(ctx, val) require.Equal(t, sdk.Unbonded, validator.GetStatus()) @@ -149,16 +176,16 @@ func TestHandleNewValidator(t *testing.T) { require.Equal(t, sdk.NewRat(amt), sk.Validator(ctx, addr).GetPower()) // 1000 first blocks not a validator - ctx = ctx.WithBlockHeight(1001) + ctx = ctx.WithBlockHeight(SignedBlocksWindow + 1) // Now a validator, for two blocks - keeper.handleValidatorSignature(ctx, val, true) - ctx = ctx.WithBlockHeight(1002) - keeper.handleValidatorSignature(ctx, val, false) + keeper.handleValidatorSignature(ctx, val, 100, true) + ctx = ctx.WithBlockHeight(SignedBlocksWindow + 2) + keeper.handleValidatorSignature(ctx, val, 100, false) info, found := keeper.getValidatorSigningInfo(ctx, val.Address()) require.True(t, found) - require.Equal(t, int64(1001), info.StartHeight) + require.Equal(t, int64(SignedBlocksWindow+1), info.StartHeight) require.Equal(t, int64(2), info.IndexOffset) require.Equal(t, int64(1), info.SignedBlocksCounter) require.Equal(t, int64(0), info.JailedUntil) diff --git a/x/slashing/params.go b/x/slashing/params.go index 3bba85fa6693..ebf14f283d4f 100644 --- a/x/slashing/params.go +++ b/x/slashing/params.go @@ -4,7 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -const ( +var ( // MaxEvidenceAge - Max age for evidence - 21 days (3 weeks) // TODO Should this be a governance parameter or just modifiable with SoftwareUpgradeProposals? // MaxEvidenceAge = 60 * 60 * 24 * 7 * 3 @@ -13,17 +13,22 @@ const ( // SignedBlocksWindow - sliding window for downtime slashing // TODO Governance parameter? - // TODO Temporarily set to 100 blocks for testnets - SignedBlocksWindow int64 = 100 + // TODO Temporarily set to 40000 blocks for testnets + SignedBlocksWindow int64 = 40000 // Downtime slashing threshold - 50% // TODO Governance parameter? - MinSignedPerWindow int64 = SignedBlocksWindow / 2 + MinSignedPerWindow = SignedBlocksWindow / 2 // Downtime unbond duration // TODO Governance parameter? - // TODO Temporarily set to 10 minutes for testnets - DowntimeUnbondDuration int64 = 60 * 10 + // TODO Temporarily set to five minutes for testnets + DowntimeUnbondDuration int64 = 60 * 5 + + // Double-sign unbond duration + // TODO Governance parameter? + // TODO Temporarily set to five minutes for testnets + DoubleSignUnbondDuration int64 = 60 * 5 ) var ( diff --git a/x/slashing/tick.go b/x/slashing/tick.go index 43ffdb947cb7..01984f870bb7 100644 --- a/x/slashing/tick.go +++ b/x/slashing/tick.go @@ -16,7 +16,21 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, sk Keeper) (tags binary.LittleEndian.PutUint64(heightBytes, uint64(req.Header.Height)) tags = sdk.NewTags("height", heightBytes) - // Deal with any equivocation evidence + // Iterate over all the validators which *should* have signed this block + // Store whether or not they have actually signed it and slash/unbond any + // which have missed too many blocks in a row (downtime slashing) + for _, signingValidator := range req.Validators { + present := signingValidator.SignedLastBlock + pubkey, err := tmtypes.PB2TM.PubKey(signingValidator.Validator.PubKey) + if err != nil { + panic(err) + } + sk.handleValidatorSignature(ctx, pubkey, signingValidator.Validator.Power, present) + } + + // Iterate through any newly discovered evidence of infraction + // Slash any validators (and since-unbonded stake within the unbonding period) + // who contributed to valid infractions for _, evidence := range req.ByzantineValidators { pk, err := tmtypes.PB2TM.PubKey(evidence.Validator.PubKey) if err != nil { @@ -24,21 +38,11 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, sk Keeper) (tags } switch evidence.Type { case tmtypes.ABCIEvidenceTypeDuplicateVote: - sk.handleDoubleSign(ctx, evidence.Height, evidence.Time, pk) + sk.handleDoubleSign(ctx, pk, evidence.Height, evidence.Time, evidence.Validator.Power) default: ctx.Logger().With("module", "x/slashing").Error(fmt.Sprintf("ignored unknown evidence type: %s", evidence.Type)) } } - // Iterate over all the validators which *should* have signed this block - for _, validator := range req.Validators { - present := validator.SignedLastBlock - pubkey, err := tmtypes.PB2TM.PubKey(validator.Validator.PubKey) - if err != nil { - panic(err) - } - sk.handleValidatorSignature(ctx, pubkey, present) - } - return } diff --git a/x/slashing/tick_test.go b/x/slashing/tick_test.go index a5cf47de5033..42a476a4ce86 100644 --- a/x/slashing/tick_test.go +++ b/x/slashing/tick_test.go @@ -46,8 +46,8 @@ func TestBeginBlocker(t *testing.T) { height := int64(0) - // for 50 blocks, mark the validator as having signed - for ; height < 50; height++ { + // for 1000 blocks, mark the validator as having signed + for ; height < SignedBlocksWindow; height++ { ctx = ctx.WithBlockHeight(height) req = abci.RequestBeginBlock{ Validators: []abci.SigningValidator{{ @@ -58,8 +58,8 @@ func TestBeginBlocker(t *testing.T) { BeginBlocker(ctx, req, keeper) } - // for 51 blocks, mark the validator as having not signed - for ; height < 102; height++ { + // for 500 blocks, mark the validator as having not signed + for ; height < ((SignedBlocksWindow * 2) - MinSignedPerWindow + 1); height++ { ctx = ctx.WithBlockHeight(height) req = abci.RequestBeginBlock{ Validators: []abci.SigningValidator{{ diff --git a/x/stake/handler_test.go b/x/stake/handler_test.go index ef47318c6872..e6cd6f4ca736 100644 --- a/x/stake/handler_test.go +++ b/x/stake/handler_test.go @@ -74,7 +74,7 @@ func TestValidatorByPowerIndex(t *testing.T) { require.True(t, got.IsOK(), "expected create-validator to be ok, got %v", got) // slash and revoke the first validator - keeper.Slash(ctx, keep.PKs[0], 0, sdk.NewRat(1, 2)) + keeper.Slash(ctx, keep.PKs[0], 0, initBond, sdk.NewRat(1, 2)) keeper.Revoke(ctx, keep.PKs[0]) validator, found = keeper.GetValidator(ctx, validatorAddr) require.True(t, found) @@ -559,3 +559,86 @@ func TestTransitiveRedelegation(t *testing.T) { got = handleMsgBeginRedelegate(ctx, msgBeginRedelegate, keeper) require.True(t, got.IsOK(), "expected no error") } + +func TestBondUnbondRedelegateSlashTwice(t *testing.T) { + ctx, _, keeper := keep.CreateTestInput(t, false, 1000) + valA, valB, del := keep.Addrs[0], keep.Addrs[1], keep.Addrs[2] + + msgCreateValidator := newTestMsgCreateValidator(valA, keep.PKs[0], 10) + got := handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + msgCreateValidator = newTestMsgCreateValidator(valB, keep.PKs[1], 10) + got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") + + // delegate 10 stake + msgDelegate := newTestMsgDelegate(del, valA, 10) + got = handleMsgDelegate(ctx, msgDelegate, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgDelegate") + + // a block passes + ctx = ctx.WithBlockHeight(1) + + // begin unbonding 4 stake + msgBeginUnbonding := NewMsgBeginUnbonding(del, valA, sdk.NewRat(4)) + got = handleMsgBeginUnbonding(ctx, msgBeginUnbonding, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgBeginUnbonding") + + // begin redelegate 6 stake + msgBeginRedelegate := NewMsgBeginRedelegate(del, valA, valB, sdk.NewRat(6)) + got = handleMsgBeginRedelegate(ctx, msgBeginRedelegate, keeper) + require.True(t, got.IsOK(), "expected no error on runMsgBeginRedelegate") + + // destination delegation should have 6 shares + delegation, found := keeper.GetDelegation(ctx, del, valB) + require.True(t, found) + require.Equal(t, sdk.NewRat(6), delegation.Shares) + + // slash the validator by half + keeper.Slash(ctx, keep.PKs[0], 0, 20, sdk.NewRat(1, 2)) + + // unbonding delegation should have been slashed by half + unbonding, found := keeper.GetUnbondingDelegation(ctx, del, valA) + require.True(t, found) + require.Equal(t, int64(2), unbonding.Balance.Amount.Int64()) + + // redelegation should have been slashed by half + redelegation, found := keeper.GetRedelegation(ctx, del, valA, valB) + require.True(t, found) + require.Equal(t, int64(3), redelegation.Balance.Amount.Int64()) + + // destination delegation should have been slashed by half + delegation, found = keeper.GetDelegation(ctx, del, valB) + require.True(t, found) + require.Equal(t, sdk.NewRat(3), delegation.Shares) + + // validator power should have been reduced by half + validator, found := keeper.GetValidator(ctx, valA) + require.True(t, found) + require.Equal(t, sdk.NewRat(5), validator.GetPower()) + + // slash the validator for an infraction committed after the unbonding and redelegation begin + ctx = ctx.WithBlockHeight(3) + keeper.Slash(ctx, keep.PKs[0], 2, 10, sdk.NewRat(1, 2)) + + // unbonding delegation should be unchanged + unbonding, found = keeper.GetUnbondingDelegation(ctx, del, valA) + require.True(t, found) + require.Equal(t, int64(2), unbonding.Balance.Amount.Int64()) + + // redelegation should be unchanged + redelegation, found = keeper.GetRedelegation(ctx, del, valA, valB) + require.True(t, found) + require.Equal(t, int64(3), redelegation.Balance.Amount.Int64()) + + // destination delegation should be unchanged + delegation, found = keeper.GetDelegation(ctx, del, valB) + require.True(t, found) + require.Equal(t, sdk.NewRat(3), delegation.Shares) + + // validator power should have been reduced to zero + validator, found = keeper.GetValidator(ctx, valA) + require.True(t, found) + require.Equal(t, sdk.NewRat(0), validator.GetPower()) +} diff --git a/x/stake/keeper/delegation.go b/x/stake/keeper/delegation.go index aab1dda90c96..514939e17f5e 100644 --- a/x/stake/keeper/delegation.go +++ b/x/stake/keeper/delegation.go @@ -95,6 +95,26 @@ func (k Keeper) GetUnbondingDelegation(ctx sdk.Context, return ubd, true } +// load all unbonding delegations from a particular validator +func (k Keeper) GetUnbondingDelegationsFromValidator(ctx sdk.Context, valAddr sdk.Address) (unbondingDelegations []types.UnbondingDelegation) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, GetUBDsByValIndexKey(valAddr, k.cdc)) + i := 0 + for ; ; i++ { + if !iterator.Valid() { + break + } + unbondingKey := iterator.Value() + unbondingBytes := store.Get(unbondingKey) + var unbondingDelegation types.UnbondingDelegation + k.cdc.MustUnmarshalBinary(unbondingBytes, &unbondingDelegation) + unbondingDelegations = append(unbondingDelegations, unbondingDelegation) + iterator.Next() + } + iterator.Close() + return unbondingDelegations +} + // set the unbonding delegation and associated index func (k Keeper) SetUnbondingDelegation(ctx sdk.Context, ubd types.UnbondingDelegation) { store := ctx.KVStore(k.storeKey) @@ -129,6 +149,26 @@ func (k Keeper) GetRedelegation(ctx sdk.Context, return red, true } +// load all redelegations from a particular validator +func (k Keeper) GetRedelegationsFromValidator(ctx sdk.Context, valAddr sdk.Address) (redelegations []types.Redelegation) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, GetREDsFromValSrcIndexKey(valAddr, k.cdc)) + i := 0 + for ; ; i++ { + if !iterator.Valid() { + break + } + redelegationKey := iterator.Value() + redelegationBytes := store.Get(redelegationKey) + var redelegation types.Redelegation + k.cdc.MustUnmarshalBinary(redelegationBytes, &redelegation) + redelegations = append(redelegations, redelegation) + iterator.Next() + } + iterator.Close() + return redelegations +} + // has a redelegation func (k Keeper) HasReceivingRedelegation(ctx sdk.Context, DelegatorAddr, ValidatorDstAddr sdk.Address) bool { @@ -254,7 +294,7 @@ func (k Keeper) unbond(ctx sdk.Context, delegatorAddr, validatorAddr sdk.Address k.RemoveValidator(ctx, validator.Owner) } - return amount, nil + return } //______________________________________________________________________________________________________ @@ -270,12 +310,14 @@ func (k Keeper) BeginUnbonding(ctx sdk.Context, delegatorAddr, validatorAddr sdk // create the unbonding delegation params := k.GetParams(ctx) minTime := ctx.BlockHeader().Time + params.UnbondingTime + balance := sdk.Coin{params.BondDenom, sdk.NewInt(returnAmount)} ubd := types.UnbondingDelegation{ - DelegatorAddr: delegatorAddr, - ValidatorAddr: validatorAddr, - MinTime: minTime, - Balance: sdk.Coin{params.BondDenom, sdk.NewInt(returnAmount)}, + DelegatorAddr: delegatorAddr, + ValidatorAddr: validatorAddr, + MinTime: minTime, + Balance: balance, + InitialBalance: balance, } k.SetUnbondingDelegation(ctx, ubd) return nil @@ -338,6 +380,8 @@ func (k Keeper) BeginRedelegation(ctx sdk.Context, delegatorAddr, validatorSrcAd MinTime: minTime, SharesDst: sharesCreated, SharesSrc: sharesAmount, + Balance: returnCoin, + InitialBalance: returnCoin, } k.SetRedelegation(ctx, red) return nil diff --git a/x/stake/keeper/slash.go b/x/stake/keeper/slash.go index 3abfe455cd03..9d4c9af62e4f 100644 --- a/x/stake/keeper/slash.go +++ b/x/stake/keeper/slash.go @@ -4,56 +4,234 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + types "github.com/cosmos/cosmos-sdk/x/stake/types" "github.com/tendermint/tendermint/crypto" ) -// NOTE the current slash functionality doesn't take into consideration unbonding/rebonding records -// or the time of breach. This will be updated in slashing v2 -// slash a validator -func (k Keeper) Slash(ctx sdk.Context, pubkey crypto.PubKey, height int64, fraction sdk.Rat) { +// 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: +// Validator exists and can be looked up by public key +// 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 + slashAmount := sdk.NewRat(power).Mul(slashFactor).EvaluateInt() + // ref https://github.com/cosmos/cosmos-sdk/issues/1348 + // ref https://github.com/cosmos/cosmos-sdk/issues/1471 - // TODO height ignored for now, see https://github.com/cosmos/cosmos-sdk/pull/1011#issuecomment-390253957 validator, found := k.GetValidatorByPubKey(ctx, pubkey) if !found { - panic(fmt.Errorf("Attempted to slash a nonexistent validator with address %s", pubkey.Address())) + panic(fmt.Errorf("attempted to slash a nonexistent validator with address %s", pubkey.Address())) + } + 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) + } + } - sharesToRemove := validator.PoolShares.Amount.Mul(fraction) + + // Cannot decrease balance below zero + sharesToRemove := remainingSlashAmount + if sharesToRemove.GT(validator.PoolShares.Amount.EvaluateInt()) { + sharesToRemove = validator.PoolShares.Amount.EvaluateInt() + } + + // Get the current pool pool := k.GetPool(ctx) - validator, pool, burned := validator.RemovePoolShares(pool, sharesToRemove) - k.SetPool(ctx, pool) // update the pool - k.UpdateValidator(ctx, validator) // update the validator, possibly kicking it out + // 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 + k.UpdateValidator(ctx, validator) - logger := ctx.Logger().With("module", "x/stake") - logger.Info(fmt.Sprintf("Validator %s slashed by fraction %v, removed %v shares and burned %d tokens", pubkey.Address(), fraction, sharesToRemove, burned)) + // 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 return } // revoke a validator func (k Keeper) Revoke(ctx sdk.Context, pubkey crypto.PubKey) { - - validator, found := k.GetValidatorByPubKey(ctx, pubkey) - if !found { - panic(fmt.Errorf("Validator with pubkey %s not found, cannot revoke", pubkey)) - } - validator.Revoked = true - k.UpdateValidator(ctx, validator) // update the validator, now revoked - + k.setRevoked(ctx, pubkey, true) logger := ctx.Logger().With("module", "x/stake") logger.Info(fmt.Sprintf("Validator %s revoked", pubkey.Address())) + // TODO Return event(s), blocked on https://github.com/tendermint/tendermint/pull/1803 return } // unrevoke a validator func (k Keeper) Unrevoke(ctx sdk.Context, pubkey crypto.PubKey) { + 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 +} +// set the revoked flag on a validator +func (k Keeper) setRevoked(ctx sdk.Context, pubkey crypto.PubKey, revoked bool) { validator, found := k.GetValidatorByPubKey(ctx, pubkey) if !found { - panic(fmt.Errorf("Validator with pubkey %s not found, cannot unrevoke", pubkey)) + 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 + slashAmount = sdk.NewRatFromInt(unbondingDelegation.InitialBalance.Amount, sdk.OneInt()).Mul(slashFactor).EvaluateInt() + + // 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 + unbondingSlashAmount := slashAmount + if unbondingSlashAmount.GT(unbondingDelegation.Balance.Amount) { + unbondingSlashAmount = unbondingDelegation.Balance.Amount + } + + // 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) } - validator.Revoked = false - k.UpdateValidator(ctx, validator) // update the validator, now unrevoked - logger := ctx.Logger().With("module", "x/stake") - logger.Info(fmt.Sprintf("Validator %s unrevoked", pubkey.Address())) return } + +// 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 + slashAmount = sdk.NewRatFromInt(redelegation.InitialBalance.Amount, sdk.OneInt()).Mul(slashFactor).EvaluateInt() + + // 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 + redelegationSlashAmount := slashAmount + if redelegationSlashAmount.GT(redelegation.Balance.Amount) { + redelegationSlashAmount = redelegation.Balance.Amount + } + + // 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 +} diff --git a/x/stake/keeper/slash_test.go b/x/stake/keeper/slash_test.go new file mode 100644 index 000000000000..9e53151a181d --- /dev/null +++ b/x/stake/keeper/slash_test.go @@ -0,0 +1,481 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/stake/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// setup helper function +// creates two validators +func setupHelper(t *testing.T, amt int64) (sdk.Context, Keeper, types.Params) { + // setup + ctx, _, keeper := CreateTestInput(t, false, amt) + params := keeper.GetParams(ctx) + pool := keeper.GetPool(ctx) + numVals := 3 + pool.LooseTokens = amt * int64(numVals) + + // add numVals validators + for i := 0; i < numVals; i++ { + validator := types.NewValidator(addrVals[i], PKs[i], types.Description{}) + validator, pool, _ = validator.AddTokensFromDel(pool, amt) + keeper.SetPool(ctx, pool) + keeper.UpdateValidator(ctx, validator) + keeper.SetValidatorByPubKeyIndex(ctx, validator) + } + + return ctx, keeper, params +} + +// tests Revoke, Unrevoke +func TestRevocation(t *testing.T) { + // setup + ctx, keeper, _ := setupHelper(t, 10) + addr := addrVals[0] + pk := PKs[0] + + // initial state + val, found := keeper.GetValidator(ctx, addr) + require.True(t, found) + require.False(t, val.GetRevoked()) + + // test revoke + keeper.Revoke(ctx, pk) + val, found = keeper.GetValidator(ctx, addr) + require.True(t, found) + require.True(t, val.GetRevoked()) + + // test unrevoke + keeper.Unrevoke(ctx, pk) + val, found = keeper.GetValidator(ctx, addr) + require.True(t, found) + require.False(t, val.GetRevoked()) + +} + +// tests slashUnbondingDelegation +func TestSlashUnbondingDelegation(t *testing.T) { + ctx, keeper, params := setupHelper(t, 10) + fraction := sdk.NewRat(1, 2) + + // set an unbonding delegation + ubd := types.UnbondingDelegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[0], + CreationHeight: 0, + // expiration timestamp (beyond which the unbonding delegation shouldn't be slashed) + MinTime: 0, + InitialBalance: sdk.NewCoin(params.BondDenom, 10), + Balance: sdk.NewCoin(params.BondDenom, 10), + } + keeper.SetUnbondingDelegation(ctx, ubd) + + // unbonding started prior to the infraction height, stake didn't contribute + slashAmount := keeper.slashUnbondingDelegation(ctx, ubd, 1, fraction) + require.Equal(t, int64(0), slashAmount.Int64()) + + // after the expiration time, no longer eligible for slashing + ctx = ctx.WithBlockHeader(abci.Header{Time: int64(10)}) + keeper.SetUnbondingDelegation(ctx, ubd) + slashAmount = keeper.slashUnbondingDelegation(ctx, ubd, 0, fraction) + require.Equal(t, int64(0), slashAmount.Int64()) + + // test valid slash, before expiration timestamp and to which stake contributed + oldPool := keeper.GetPool(ctx) + ctx = ctx.WithBlockHeader(abci.Header{Time: int64(0)}) + keeper.SetUnbondingDelegation(ctx, ubd) + slashAmount = keeper.slashUnbondingDelegation(ctx, ubd, 0, fraction) + require.Equal(t, int64(5), slashAmount.Int64()) + ubd, found := keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) + require.True(t, found) + // initialbalance unchanged + require.Equal(t, sdk.NewCoin(params.BondDenom, 10), ubd.InitialBalance) + // balance decreased + require.Equal(t, sdk.NewCoin(params.BondDenom, 5), ubd.Balance) + newPool := keeper.GetPool(ctx) + require.Equal(t, int64(5), oldPool.LooseTokens-newPool.LooseTokens) +} + +// tests slashRedelegation +func TestSlashRedelegation(t *testing.T) { + ctx, keeper, params := setupHelper(t, 10) + fraction := sdk.NewRat(1, 2) + + // set a redelegation + rd := types.Redelegation{ + DelegatorAddr: addrDels[0], + ValidatorSrcAddr: addrVals[0], + ValidatorDstAddr: addrVals[1], + CreationHeight: 0, + // expiration timestamp (beyond which the redelegation shouldn't be slashed) + MinTime: 0, + SharesSrc: sdk.NewRat(10), + SharesDst: sdk.NewRat(10), + InitialBalance: sdk.NewCoin(params.BondDenom, 10), + Balance: sdk.NewCoin(params.BondDenom, 10), + } + keeper.SetRedelegation(ctx, rd) + + // set the associated delegation + del := types.Delegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[1], + Shares: sdk.NewRat(10), + } + keeper.SetDelegation(ctx, del) + + // started redelegating prior to the current height, stake didn't contribute to infraction + validator, found := keeper.GetValidator(ctx, addrVals[1]) + require.True(t, found) + slashAmount := keeper.slashRedelegation(ctx, validator, rd, 1, fraction) + require.Equal(t, int64(0), slashAmount.Int64()) + + // after the expiration time, no longer eligible for slashing + ctx = ctx.WithBlockHeader(abci.Header{Time: int64(10)}) + keeper.SetRedelegation(ctx, rd) + validator, found = keeper.GetValidator(ctx, addrVals[1]) + require.True(t, found) + slashAmount = keeper.slashRedelegation(ctx, validator, rd, 0, fraction) + require.Equal(t, int64(0), slashAmount.Int64()) + + // test valid slash, before expiration timestamp and to which stake contributed + oldPool := keeper.GetPool(ctx) + ctx = ctx.WithBlockHeader(abci.Header{Time: int64(0)}) + keeper.SetRedelegation(ctx, rd) + validator, found = keeper.GetValidator(ctx, addrVals[1]) + require.True(t, found) + slashAmount = keeper.slashRedelegation(ctx, validator, rd, 0, fraction) + require.Equal(t, int64(5), slashAmount.Int64()) + rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.True(t, found) + // initialbalance unchanged + require.Equal(t, sdk.NewCoin(params.BondDenom, 10), rd.InitialBalance) + // balance decreased + require.Equal(t, sdk.NewCoin(params.BondDenom, 5), rd.Balance) + // shares decreased + del, found = keeper.GetDelegation(ctx, addrDels[0], addrVals[1]) + require.True(t, found) + require.Equal(t, int64(5), del.Shares.Evaluate()) + // pool bonded tokens decreased + newPool := keeper.GetPool(ctx) + require.Equal(t, int64(5), oldPool.BondedTokens-newPool.BondedTokens) +} + +// tests Slash at a future height (must panic) +func TestSlashAtFutureHeight(t *testing.T) { + ctx, keeper, _ := setupHelper(t, 10) + pk := PKs[0] + fraction := sdk.NewRat(1, 2) + require.Panics(t, func() { keeper.Slash(ctx, pk, 1, 10, fraction) }) +} + +// tests Slash at the current height +func TestSlashAtCurrentHeight(t *testing.T) { + ctx, keeper, _ := setupHelper(t, 10) + pk := PKs[0] + fraction := sdk.NewRat(1, 2) + + oldPool := keeper.GetPool(ctx) + validator, found := keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + keeper.Slash(ctx, pk, ctx.BlockHeight(), 10, fraction) + + // read updated state + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + newPool := keeper.GetPool(ctx) + + // power decreased + require.Equal(t, sdk.NewRat(5), validator.GetPower()) + // pool bonded shares decreased + require.Equal(t, sdk.NewRat(5).Evaluate(), oldPool.BondedShares.Sub(newPool.BondedShares).Evaluate()) +} + +// tests Slash at a previous height with an unbonding delegation +func TestSlashWithUnbondingDelegation(t *testing.T) { + ctx, keeper, params := setupHelper(t, 10) + pk := PKs[0] + fraction := sdk.NewRat(1, 2) + + // set an unbonding delegation + ubd := types.UnbondingDelegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[0], + CreationHeight: 11, + // expiration timestamp (beyond which the unbonding delegation shouldn't be slashed) + MinTime: 0, + InitialBalance: sdk.NewCoin(params.BondDenom, 4), + Balance: sdk.NewCoin(params.BondDenom, 4), + } + keeper.SetUnbondingDelegation(ctx, ubd) + + // slash validator for the first time + ctx = ctx.WithBlockHeight(12) + oldPool := keeper.GetPool(ctx) + validator, found := keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + keeper.Slash(ctx, pk, 10, 10, fraction) + + // read updating unbonding delegation + ubd, found = keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) + require.True(t, found) + // balance decreased + require.Equal(t, sdk.NewInt(2), ubd.Balance.Amount) + // read updated pool + newPool := keeper.GetPool(ctx) + // bonded tokens burned + require.Equal(t, int64(3), oldPool.BondedTokens-newPool.BondedTokens) + // read updated validator + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + // power decreased by 3 - 6 stake originally bonded at the time of infraction + // was still bonded at the time of discovery and was slashed by half, 4 stake + // bonded at the time of discovery hadn't been bonded at the time of infraction + // and wasn't slashed + require.Equal(t, sdk.NewRat(7), validator.GetPower()) + + // slash validator again + ctx = ctx.WithBlockHeight(13) + keeper.Slash(ctx, pk, 9, 10, fraction) + ubd, found = keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) + require.True(t, found) + // balance decreased again + require.Equal(t, sdk.NewInt(0), ubd.Balance.Amount) + // read updated pool + newPool = keeper.GetPool(ctx) + // bonded tokens burned again + require.Equal(t, int64(6), oldPool.BondedTokens-newPool.BondedTokens) + // read updated validator + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + // power decreased by 3 again + require.Equal(t, sdk.NewRat(4), validator.GetPower()) + + // slash validator again + // all originally bonded stake has been slashed, so this will have no effect + // on the unbonding delegation, but it will slash stake bonded since the infraction + // this may not be the desirable behaviour, ref https://github.com/cosmos/cosmos-sdk/issues/1440 + ctx = ctx.WithBlockHeight(13) + keeper.Slash(ctx, pk, 9, 10, fraction) + ubd, found = keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) + require.True(t, found) + // balance unchanged + require.Equal(t, sdk.NewInt(0), ubd.Balance.Amount) + // read updated pool + newPool = keeper.GetPool(ctx) + // bonded tokens burned again + require.Equal(t, int64(9), oldPool.BondedTokens-newPool.BondedTokens) + // read updated validator + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + // power decreased by 3 again + require.Equal(t, sdk.NewRat(1), validator.GetPower()) + + // slash validator again + // all originally bonded stake has been slashed, so this will have no effect + // on the unbonding delegation, but it will slash stake bonded since the infraction + // this may not be the desirable behaviour, ref https://github.com/cosmos/cosmos-sdk/issues/1440 + ctx = ctx.WithBlockHeight(13) + keeper.Slash(ctx, pk, 9, 10, fraction) + ubd, found = keeper.GetUnbondingDelegation(ctx, addrDels[0], addrVals[0]) + require.True(t, found) + // balance unchanged + require.Equal(t, sdk.NewInt(0), ubd.Balance.Amount) + // read updated pool + newPool = keeper.GetPool(ctx) + // just 1 bonded token burned again since that's all the validator now has + require.Equal(t, int64(10), oldPool.BondedTokens-newPool.BondedTokens) + // read updated validator + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + // power decreased by 1 again, validator is out of stake + require.Equal(t, sdk.NewRat(0), validator.GetPower()) +} + +// tests Slash at a previous height with a redelegation +func TestSlashWithRedelegation(t *testing.T) { + ctx, keeper, params := setupHelper(t, 10) + pk := PKs[0] + fraction := sdk.NewRat(1, 2) + + // set a redelegation + rd := types.Redelegation{ + DelegatorAddr: addrDels[0], + ValidatorSrcAddr: addrVals[0], + ValidatorDstAddr: addrVals[1], + CreationHeight: 11, + MinTime: 0, + SharesSrc: sdk.NewRat(6), + SharesDst: sdk.NewRat(6), + InitialBalance: sdk.NewCoin(params.BondDenom, 6), + Balance: sdk.NewCoin(params.BondDenom, 6), + } + keeper.SetRedelegation(ctx, rd) + + // set the associated delegation + del := types.Delegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[1], + Shares: sdk.NewRat(6), + } + keeper.SetDelegation(ctx, del) + + // slash validator + ctx = ctx.WithBlockHeight(12) + oldPool := keeper.GetPool(ctx) + validator, found := keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + keeper.Slash(ctx, pk, 10, 10, fraction) + + // read updating redelegation + rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.True(t, found) + // balance decreased + require.Equal(t, sdk.NewInt(3), rd.Balance.Amount) + // read updated pool + newPool := keeper.GetPool(ctx) + // bonded tokens burned + require.Equal(t, int64(5), oldPool.BondedTokens-newPool.BondedTokens) + // read updated validator + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + // power decreased by 2 - 4 stake originally bonded at the time of infraction + // was still bonded at the time of discovery and was slashed by half, 4 stake + // bonded at the time of discovery hadn't been bonded at the time of infraction + // and wasn't slashed + require.Equal(t, sdk.NewRat(8), validator.GetPower()) + + // slash the validator again + ctx = ctx.WithBlockHeight(12) + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + keeper.Slash(ctx, pk, 10, 10, sdk.NewRat(3, 4)) + + // read updating redelegation + rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.True(t, found) + // balance decreased, now zero + require.Equal(t, sdk.NewInt(0), rd.Balance.Amount) + // read updated pool + newPool = keeper.GetPool(ctx) + // 7 bonded tokens burned + require.Equal(t, int64(12), oldPool.BondedTokens-newPool.BondedTokens) + // read updated validator + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + // power decreased by 4 + require.Equal(t, sdk.NewRat(4), validator.GetPower()) + + // slash the validator again, by 100% + ctx = ctx.WithBlockHeight(12) + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + keeper.Slash(ctx, pk, 10, 10, sdk.OneRat()) + + // read updating redelegation + rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.True(t, found) + // balance still zero + require.Equal(t, sdk.NewInt(0), rd.Balance.Amount) + // read updated pool + newPool = keeper.GetPool(ctx) + // four more bonded tokens burned + require.Equal(t, int64(16), oldPool.BondedTokens-newPool.BondedTokens) + // read updated validator + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + // power decreased by 4, down to 0 + require.Equal(t, sdk.NewRat(0), validator.GetPower()) + + // slash the validator again, by 100% + // no stake remains to be slashed + ctx = ctx.WithBlockHeight(12) + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + keeper.Slash(ctx, pk, 10, 10, sdk.OneRat()) + + // read updating redelegation + rd, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.True(t, found) + // balance still zero + require.Equal(t, sdk.NewInt(0), rd.Balance.Amount) + // read updated pool + newPool = keeper.GetPool(ctx) + // no more bonded tokens burned + require.Equal(t, int64(16), oldPool.BondedTokens-newPool.BondedTokens) + // read updated validator + validator, found = keeper.GetValidatorByPubKey(ctx, pk) + require.True(t, found) + // power still zero + require.Equal(t, sdk.NewRat(0), validator.GetPower()) +} + +// tests Slash at a previous height with both an unbonding delegation and a redelegation +func TestSlashBoth(t *testing.T) { + ctx, keeper, params := setupHelper(t, 10) + fraction := sdk.NewRat(1, 2) + + // set a redelegation + rdA := types.Redelegation{ + DelegatorAddr: addrDels[0], + ValidatorSrcAddr: addrVals[0], + ValidatorDstAddr: addrVals[1], + CreationHeight: 11, + // expiration timestamp (beyond which the redelegation shouldn't be slashed) + MinTime: 0, + SharesSrc: sdk.NewRat(6), + SharesDst: sdk.NewRat(6), + InitialBalance: sdk.NewCoin(params.BondDenom, 6), + Balance: sdk.NewCoin(params.BondDenom, 6), + } + keeper.SetRedelegation(ctx, rdA) + + // set the associated delegation + delA := types.Delegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[1], + Shares: sdk.NewRat(6), + } + keeper.SetDelegation(ctx, delA) + + // set an unbonding delegation + ubdA := types.UnbondingDelegation{ + DelegatorAddr: addrDels[0], + ValidatorAddr: addrVals[0], + CreationHeight: 11, + // expiration timestamp (beyond which the unbonding delegation shouldn't be slashed) + MinTime: 0, + InitialBalance: sdk.NewCoin(params.BondDenom, 4), + Balance: sdk.NewCoin(params.BondDenom, 4), + } + keeper.SetUnbondingDelegation(ctx, ubdA) + + // slash validator + ctx = ctx.WithBlockHeight(12) + oldPool := keeper.GetPool(ctx) + validator, found := keeper.GetValidatorByPubKey(ctx, PKs[0]) + require.True(t, found) + keeper.Slash(ctx, PKs[0], 10, 10, fraction) + + // read updating redelegation + rdA, found = keeper.GetRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.True(t, found) + // balance decreased + require.Equal(t, sdk.NewInt(3), rdA.Balance.Amount) + // read updated pool + newPool := keeper.GetPool(ctx) + // loose tokens burned + require.Equal(t, int64(2), oldPool.LooseTokens-newPool.LooseTokens) + // bonded tokens burned + require.Equal(t, int64(3), oldPool.BondedTokens-newPool.BondedTokens) + // read updated validator + validator, found = keeper.GetValidatorByPubKey(ctx, PKs[0]) + require.True(t, found) + // power not decreased, all stake was bonded since + require.Equal(t, sdk.NewRat(10), validator.GetPower()) +} diff --git a/x/stake/types/delegation.go b/x/stake/types/delegation.go index 410cfbe1d85f..eb38a50a7a91 100644 --- a/x/stake/types/delegation.go +++ b/x/stake/types/delegation.go @@ -61,6 +61,7 @@ type UnbondingDelegation struct { ValidatorAddr sdk.Address `json:"validator_addr"` // validator unbonding from owner addr CreationHeight int64 `json:"creation_height"` // height which the unbonding took place MinTime int64 `json:"min_time"` // unix time for unbonding completion + InitialBalance sdk.Coin `json:"initial_balance"` // atoms initially scheduled to receive at completion Balance sdk.Coin `json:"balance"` // atoms to receive at completion } @@ -101,6 +102,8 @@ type Redelegation struct { ValidatorDstAddr sdk.Address `json:"validator_dst_addr"` // validator redelegation destination owner addr CreationHeight int64 `json:"creation_height"` // height which the redelegation took place MinTime int64 `json:"min_time"` // unix time for redelegation completion + InitialBalance sdk.Coin `json:"initial_balance"` // initial balance when redelegation started + Balance sdk.Coin `json:"balance"` // current balance SharesSrc sdk.Rat `json:"shares_src"` // amount of source shares redelegating SharesDst sdk.Rat `json:"shares_dst"` // amount of destination shares redelegating } diff --git a/x/stake/types/validator.go b/x/stake/types/validator.go index 652fd9e6e82f..c98ebce62ab0 100644 --- a/x/stake/types/validator.go +++ b/x/stake/types/validator.go @@ -284,6 +284,7 @@ func (v Validator) DelegatorShareExRate(pool Pool) sdk.Rat { var _ sdk.Validator = Validator{} // nolint - for sdk.Validator +func (v Validator) GetRevoked() bool { return v.Revoked } func (v Validator) GetMoniker() string { return v.Description.Moniker } func (v Validator) GetStatus() sdk.BondStatus { return v.Status() } func (v Validator) GetOwner() sdk.Address { return v.Owner }