Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update x/slashing to match module spec #4717

Merged
merged 12 commits into from
Jul 19, 2019
Prev Previous commit
Next Next commit
restructure
  • Loading branch information
fedekunze committed Jul 12, 2019
commit b26007cc680ac6fb6fc8d4d5fc963673b4ed93ef
42 changes: 24 additions & 18 deletions x/slashing/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,29 @@
package slashing

import (
"github.com/cosmos/cosmos-sdk/x/slashing/keeper"
"github.com/cosmos/cosmos-sdk/x/slashing/types"
)

const (
DefaultCodespace = types.DefaultCodespace
CodeInvalidValidator = types.CodeInvalidValidator
CodeValidatorJailed = types.CodeValidatorJailed
CodeValidatorNotJailed = types.CodeValidatorNotJailed
CodeMissingSelfDelegation = types.CodeMissingSelfDelegation
CodeSelfDelegationTooLow = types.CodeSelfDelegationTooLow
CodeMissingSigningInfo = types.CodeMissingSigningInfo
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute
QueryParameters = types.QueryParameters
QuerySigningInfo = types.QuerySigningInfo
QuerySigningInfos = types.QuerySigningInfos
DefaultParamspace = types.DefaultParamspace
DefaultMaxEvidenceAge = types.DefaultMaxEvidenceAge
DefaultSignedBlocksWindow = types.DefaultSignedBlocksWindow
DefaultDowntimeJailDuration = types.DefaultDowntimeJailDuration
DefaultCodespace = types.DefaultCodespace
CodeInvalidValidator = types.CodeInvalidValidator
CodeValidatorJailed = types.CodeValidatorJailed
CodeValidatorNotJailed = types.CodeValidatorNotJailed
CodeMissingSelfDelegation = types.CodeMissingSelfDelegation
CodeSelfDelegationTooLow = types.CodeSelfDelegationTooLow
CodeMissingSigningInfo = types.CodeMissingSigningInfo
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute
DefaultParamspace = types.DefaultParamspace
DefaultMaxEvidenceAge = types.DefaultMaxEvidenceAge
DefaultSignedBlocksWindow = types.DefaultSignedBlocksWindow
DefaultDowntimeJailDuration = types.DefaultDowntimeJailDuration
QueryParameters = types.QueryParameters
QuerySigningInfo = types.QuerySigningInfo
QuerySigningInfos = types.QuerySigningInfos
)

var (
Expand Down Expand Up @@ -55,6 +56,9 @@ var (
NewQuerySigningInfoParams = types.NewQuerySigningInfoParams
NewQuerySigningInfosParams = types.NewQuerySigningInfosParams
NewValidatorSigningInfo = types.NewValidatorSigningInfo
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
NewTestMsgCreateValidator = keeper.NewTestMsgCreateValidator

// variable aliases
ModuleCdc = types.ModuleCdc
Expand Down Expand Up @@ -82,4 +86,6 @@ type (
QuerySigningInfoParams = types.QuerySigningInfoParams
QuerySigningInfosParams = types.QuerySigningInfosParams
ValidatorSigningInfo = types.ValidatorSigningInfo
Hooks = keeper.Hooks
Keeper = keeper.Keeper
)
16 changes: 5 additions & 11 deletions x/slashing/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func InitGenesis(ctx sdk.Context, keeper Keeper, stakingKeeper types.StakingKeeper, data types.GenesisState) {
stakingKeeper.IterateValidators(ctx,
func(index int64, validator exported.ValidatorI) bool {
keeper.addPubkey(ctx, validator.GetConsPubKey())
keeper.AddPubkey(ctx, validator.GetConsPubKey())
return false
},
)
Expand All @@ -30,20 +30,18 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, stakingKeeper types.StakingKeep
panic(err)
}
for _, missed := range array {
keeper.setValidatorMissedBlockBitArray(ctx, address, missed.Index, missed.Missed)
keeper.SetValidatorMissedBlockBitArray(ctx, address, missed.Index, missed.Missed)
}
}

keeper.paramspace.SetParamSet(ctx, &data.Params)
keeper.SetParams(ctx, data.Params)
}

// ExportGenesis writes the current store values
// to a genesis file, which can be imported again
// with InitGenesis
func ExportGenesis(ctx sdk.Context, keeper Keeper) (data types.GenesisState) {
var params types.Params
keeper.paramspace.GetParamSet(ctx, &params)

params := keeper.GetParams(ctx)
signingInfos := make(map[string]types.ValidatorSigningInfo)
missedBlocks := make(map[string][]types.MissedBlock)
keeper.IterateValidatorSigningInfos(ctx, func(address sdk.ConsAddress, info types.ValidatorSigningInfo) (stop bool) {
Expand All @@ -60,9 +58,5 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) (data types.GenesisState) {
return false
})

return types.GenesisState{
Params: params,
SigningInfos: signingInfos,
MissedBlocks: missedBlocks,
}
return types.NewGenesisState(params, signingInfos, missedBlocks)
}
41 changes: 4 additions & 37 deletions x/slashing/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,45 +26,12 @@ func NewHandler(k Keeper) sdk.Handler {
// Validators must submit a transaction to unjail itself after
// having been jailed (and thus unbonded) for downtime
func handleMsgUnjail(ctx sdk.Context, msg MsgUnjail, k Keeper) sdk.Result {
validator := k.sk.Validator(ctx, msg.ValidatorAddr)
if validator == nil {
return ErrNoValidatorForAddress(k.codespace).Result()

err := k.Unjail(ctx, msg.ValidatorAddr)
if err != nil {
return err.Result()
}

// cannot be unjailed if no self-delegation exists
selfDel := k.sk.Delegation(ctx, sdk.AccAddress(msg.ValidatorAddr), msg.ValidatorAddr)
if selfDel == nil {
return ErrMissingSelfDelegation(k.codespace).Result()
}

if validator.TokensFromShares(selfDel.GetShares()).TruncateInt().LT(validator.GetMinSelfDelegation()) {
return ErrSelfDelegationTooLowToUnjail(k.codespace).Result()
}

// cannot be unjailed if not jailed
if !validator.IsJailed() {
return ErrValidatorNotJailed(k.codespace).Result()
}

consAddr := sdk.ConsAddress(validator.GetConsPubKey().Address())

info, found := k.GetValidatorSigningInfo(ctx, consAddr)
if !found {
return ErrNoValidatorForAddress(k.codespace).Result()
}

// cannot be unjailed if tombstoned
if info.Tombstoned {
return ErrValidatorJailed(k.codespace).Result()
}

// cannot be unjailed until out of jail
if ctx.BlockHeader().Time.Before(info.JailedUntil) {
return ErrValidatorJailed(k.codespace).Result()
}

k.sk.Unjail(ctx, consAddr)

ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
Expand Down
2 changes: 1 addition & 1 deletion x/slashing/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (k Keeper) AfterValidatorBonded(ctx sdk.Context, address sdk.ConsAddress, _
// When a validator is created, add the address-pubkey relation.
func (k Keeper) AfterValidatorCreated(ctx sdk.Context, valAddr sdk.ValAddress) {
validator := k.sk.Validator(ctx, valAddr)
k.addPubkey(ctx, validator.GetConsPubKey())
k.AddPubkey(ctx, validator.GetConsPubKey())
}

// When a validator is removed, delete the address-pubkey relation.
Expand Down
214 changes: 214 additions & 0 deletions x/slashing/keeper/infractions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package keeper

import (
"fmt"
"time"

"github.com/tendermint/tendermint/crypto"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/slashing/types"
)

// HandleDoubleSign handles a validator signing two blocks at the same height.
// power: power of the double-signing validator at the height of infraction
func (k Keeper) HandleDoubleSign(ctx sdk.Context, addr crypto.Address, infractionHeight int64, timestamp time.Time, power int64) {
logger := k.Logger(ctx)

// calculate the age of the evidence
time := ctx.BlockHeader().Time
age := time.Sub(timestamp)

// fetch the validator public key
consAddr := sdk.ConsAddress(addr)
pubkey, err := k.getPubkey(ctx, addr)
if err != nil {
// Ignore evidence that cannot be handled.
// NOTE:
// We used to panic with:
// `panic(fmt.Sprintf("Validator consensus-address %v not found", consAddr))`,
// but this couples the expectations of the app to both Tendermint and
// the simulator. Both are expected to provide the full range of
// allowable but none of the disallowed evidence types. Instead of
// getting this coordination right, it is easier to relax the
// constraints and ignore evidence that cannot be handled.
return
}

// Reject evidence if the double-sign is too old
if age > k.MaxEvidenceAge(ctx) {
logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, age of %d past max age of %d",
sdk.ConsAddress(pubkey.Address()), infractionHeight, age, k.MaxEvidenceAge(ctx)))
return
}

// Get validator and signing info
validator := k.sk.ValidatorByConsAddr(ctx, consAddr)
if validator == nil || validator.IsUnbonded() {
// Defensive.
// Simulation doesn't take unbonding periods into account, and
// Tendermint might break this assumption at some point.
return
}

// fetch the validator signing info
signInfo, found := k.GetValidatorSigningInfo(ctx, consAddr)
if !found {
panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr))
}

// validator is already tombstoned
if signInfo.Tombstoned {
logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, validator already tombstoned", sdk.ConsAddress(pubkey.Address()), infractionHeight))
return
}

// double sign confirmed
logger.Info(fmt.Sprintf("Confirmed double sign from %s at height %d, age of %d", sdk.ConsAddress(pubkey.Address()), infractionHeight, age))

// We need to retrieve the stake distribution which signed the block, so we subtract ValidatorUpdateDelay from the evidence height.
// Note that this *can* result in a negative "distributionHeight", up to -ValidatorUpdateDelay,
// i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block.
// That's fine since this is just used to filter unbonding delegations & redelegations.
distributionHeight := infractionHeight - sdk.ValidatorUpdateDelay

// get the percentage slash penalty fraction
fraction := k.SlashFractionDoubleSign(ctx)

// Slash validator
// `power` is the int64 power of the validator as provided to/by
// Tendermint. This value is validator.Tokens as sent to Tendermint via
// ABCI, and now received as evidence.
// The fraction is passed in to separately to slash unbonding and rebonding delegations.
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSlash,
sdk.NewAttribute(types.AttributeKeyAddress, consAddr.String()),
sdk.NewAttribute(types.AttributeKeyPower, fmt.Sprintf("%d", power)),
sdk.NewAttribute(types.AttributeKeyReason, types.AttributeValueDoubleSign),
),
)
k.sk.Slash(ctx, consAddr, distributionHeight, power, fraction)

// Jail validator if not already jailed
// begin unbonding validator if not already unbonding (tombstone)
if !validator.IsJailed() {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSlash,
sdk.NewAttribute(types.AttributeKeyJailed, consAddr.String()),
),
)
k.sk.Jail(ctx, consAddr)
}

// Set tombstoned to be true
signInfo.Tombstoned = true

// Set jailed until to be forever (max time)
signInfo.JailedUntil = types.DoubleSignJailEndTime

// Set validator signing info
k.SetValidatorSigningInfo(ctx, consAddr, signInfo)
}

// HandleValidatorSignature handles a validator signature, must be called once per validator per block.
func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr crypto.Address, power int64, signed bool) {
// TODO: refactor to take in a consensus address, additionally should maybe just take in the pubkey too
logger := k.Logger(ctx)
height := ctx.BlockHeight()
consAddr := sdk.ConsAddress(addr)
pubkey, err := k.getPubkey(ctx, addr)
if err != nil {
panic(fmt.Sprintf("Validator consensus-address %v not found", consAddr.String()))
}

// fetch signing info
signInfo, found := k.GetValidatorSigningInfo(ctx, consAddr)
if !found {
panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr))
}

// this is a relative index, so it counts blocks the validator *should* have signed
// will use the 0-value default signing info if not present, except for start height
index := signInfo.IndexOffset % k.SignedBlocksWindow(ctx)
signInfo.IndexOffset++

// Update signed block bit array & counter
// This counter just tracks the sum of the bit array
// That way we avoid needing to read/write the whole array each time
previous := k.GetValidatorMissedBlockBitArray(ctx, consAddr, index)
missed := !signed
switch {
case !previous && missed:
// Array value has changed from not missed to missed, increment counter
k.SetValidatorMissedBlockBitArray(ctx, consAddr, index, true)
signInfo.MissedBlocksCounter++
case previous && !missed:
// Array value has changed from missed to not missed, decrement counter
k.SetValidatorMissedBlockBitArray(ctx, consAddr, index, false)
signInfo.MissedBlocksCounter--
default:
// Array value at this index has not changed, no need to update counter
}

if missed {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeLiveness,
sdk.NewAttribute(types.AttributeKeyAddress, consAddr.String()),
sdk.NewAttribute(types.AttributeKeyMissedBlocks, fmt.Sprintf("%d", signInfo.MissedBlocksCounter)),
sdk.NewAttribute(types.AttributeKeyHeight, fmt.Sprintf("%d", height)),
),
)

logger.Info(fmt.Sprintf("Absent validator %s (%s) at height %d, %d missed, threshold %d", addr, pubkey, height, signInfo.MissedBlocksCounter, k.MinSignedPerWindow(ctx)))
}

minHeight := signInfo.StartHeight + k.SignedBlocksWindow(ctx)
maxMissed := k.SignedBlocksWindow(ctx) - k.MinSignedPerWindow(ctx)

// if we are past the minimum height and the validator has missed too many blocks, punish them
if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
validator := k.sk.ValidatorByConsAddr(ctx, consAddr)
if validator != nil && !validator.IsJailed() {

// Downtime confirmed: slash and jail the validator
logger.Info(fmt.Sprintf("Validator %s past min height of %d and below signed blocks threshold of %d",
sdk.ConsAddress(pubkey.Address()), minHeight, k.MinSignedPerWindow(ctx)))

// We need to retrieve the stake distribution which signed the block, so we subtract ValidatorUpdateDelay from the evidence height,
// and subtract an additional 1 since this is the LastCommit.
// Note that this *can* result in a negative "distributionHeight" up to -ValidatorUpdateDelay-1,
// i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block.
// That's fine since this is just used to filter unbonding delegations & redelegations.
distributionHeight := height - sdk.ValidatorUpdateDelay - 1

ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSlash,
sdk.NewAttribute(types.AttributeKeyAddress, consAddr.String()),
sdk.NewAttribute(types.AttributeKeyPower, fmt.Sprintf("%d", power)),
sdk.NewAttribute(types.AttributeKeyReason, types.AttributeValueMissingSignature),
sdk.NewAttribute(types.AttributeKeyJailed, consAddr.String()),
),
)
k.sk.Slash(ctx, consAddr, distributionHeight, power, k.SlashFractionDowntime(ctx))
k.sk.Jail(ctx, consAddr)

signInfo.JailedUntil = ctx.BlockHeader().Time.Add(k.DowntimeJailDuration(ctx))

// We need to reset the counter & array so that the validator won't be immediately slashed for downtime upon rebonding.
signInfo.MissedBlocksCounter = 0
signInfo.IndexOffset = 0
k.clearValidatorMissedBlockBitArray(ctx, consAddr)
} else {
// Validator was (a) not found or (b) already jailed, don't slash
logger.Info(fmt.Sprintf("Validator %s would have been slashed for downtime, but was either not found in store or already jailed",
sdk.ConsAddress(pubkey.Address())))
}
}

// Set the updated signing info
k.SetValidatorSigningInfo(ctx, consAddr, signInfo)
}
Loading