Merge PR #1278: Slashing v2
Implement semifinal Gaia slashing spec (#1263), less #1348, #1378, and #1440 which are TBD.
This commit is contained in:
parent
2f508f5b28
commit
3654579ea7
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -443,7 +443,7 @@
|
|||
"netutil",
|
||||
"trace"
|
||||
]
|
||||
revision = "e514e69ffb8bc3c76a71ae40de0118d794855992"
|
||||
revision = "97aa3a539ec716117a9d15a4659a911f50d13c3c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -16,32 +16,27 @@ 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):
|
||||
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.
|
||||
|
||||
We first need to loop through the unbondings and redelegations from the slashed validator
|
||||
and track how much stake has since moved:
|
||||
|
||||
```
|
||||
curVal := validator
|
||||
oldVal := loadValidator(evidence.Height, evidence.Address)
|
||||
slashAmountUnbondings := 0
|
||||
slashAmountRedelegations := 0
|
||||
|
||||
slashAmount := SLASH_PROPORTION * oldVal.Shares
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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,7 +63,8 @@ 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
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{{
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)},
|
||||
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
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
sharesToRemove := validator.PoolShares.Amount.Mul(fraction)
|
||||
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
|
||||
ownerAddress := validator.GetOwner()
|
||||
|
||||
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))
|
||||
// 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
|
||||
// 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) {
|
||||
|
||||
validator, found := k.GetValidatorByPubKey(ctx, pubkey)
|
||||
if !found {
|
||||
panic(fmt.Errorf("Validator with pubkey %s not found, cannot unrevoke", pubkey))
|
||||
}
|
||||
validator.Revoked = false
|
||||
k.UpdateValidator(ctx, validator) // update the validator, now unrevoked
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
Loading…
Reference in New Issue