Merge PR #1278: Slashing v2

Implement semifinal Gaia slashing spec (#1263), less #1348, #1378, and #1440 which are TBD.
This commit is contained in:
Christopher Goes 2018-06-30 05:34:55 +02:00 committed by GitHub
parent 2f508f5b28
commit 3654579ea7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1197 additions and 122 deletions

View File

@ -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

View File

@ -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

2
Gopkg.lock generated
View File

@ -443,7 +443,7 @@
"netutil",
"trace"
]
revision = "e514e69ffb8bc3c76a71ae40de0118d794855992"
revision = "97aa3a539ec716117a9d15a4659a911f50d13c3c"
[[projects]]
branch = "master"

View File

@ -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)

View File

@ -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
}

View File

@ -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.
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:

View File

@ -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")
}

View File

@ -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
}
//_______________________________________________________________________________

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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
}

View File

@ -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)

View File

@ -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 (

View File

@ -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
}

View File

@ -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{{

View File

@ -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())
}

View File

@ -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

View File

@ -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
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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 }