2018-05-23 13:25:56 -07:00
|
|
|
package slashing
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2018-08-12 00:33:48 -07:00
|
|
|
"time"
|
|
|
|
|
2018-12-10 06:27:25 -08:00
|
|
|
"github.com/tendermint/tendermint/crypto"
|
2019-04-15 20:58:06 -07:00
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
2018-12-10 06:27:25 -08:00
|
|
|
|
2018-09-13 11:17:32 -07:00
|
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
2018-05-23 13:25:56 -07:00
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
2018-07-13 17:12:23 -07:00
|
|
|
"github.com/cosmos/cosmos-sdk/x/params"
|
2018-05-23 13:25:56 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
// Keeper of the slashing store
|
|
|
|
type Keeper struct {
|
2019-06-04 15:06:58 -07:00
|
|
|
storeKey sdk.StoreKey
|
|
|
|
cdc *codec.Codec
|
|
|
|
sk StakingKeeper
|
|
|
|
paramspace params.Subspace
|
2018-08-31 02:03:43 -07:00
|
|
|
|
2018-05-23 13:25:56 -07:00
|
|
|
// codespace
|
|
|
|
codespace sdk.CodespaceType
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewKeeper creates a slashing keeper
|
2019-06-04 15:06:58 -07:00
|
|
|
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, sk StakingKeeper, paramspace params.Subspace, codespace sdk.CodespaceType) Keeper {
|
2018-05-23 13:25:56 -07:00
|
|
|
keeper := Keeper{
|
2019-06-04 15:06:58 -07:00
|
|
|
storeKey: key,
|
|
|
|
cdc: cdc,
|
|
|
|
sk: sk,
|
|
|
|
paramspace: paramspace.WithKeyTable(ParamKeyTable()),
|
|
|
|
codespace: codespace,
|
2018-05-23 13:25:56 -07:00
|
|
|
}
|
|
|
|
return keeper
|
|
|
|
}
|
|
|
|
|
2019-04-15 20:58:06 -07:00
|
|
|
// Logger returns a module-specific logger.
|
|
|
|
func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/slashing") }
|
|
|
|
|
2018-10-23 11:33:39 -07:00
|
|
|
// handle a validator signing two blocks at the same height
|
|
|
|
// power: power of the double-signing validator at the height of infraction
|
2018-08-21 06:40:43 -07:00
|
|
|
func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractionHeight int64, timestamp time.Time, power int64) {
|
2019-04-15 20:58:06 -07:00
|
|
|
logger := k.Logger(ctx)
|
2019-02-07 17:41:23 -08:00
|
|
|
|
|
|
|
// calculate the age of the evidence
|
2018-06-29 20:34:55 -07:00
|
|
|
time := ctx.BlockHeader().Time
|
2018-08-12 00:33:48 -07:00
|
|
|
age := time.Sub(timestamp)
|
2019-02-07 17:41:23 -08:00
|
|
|
|
|
|
|
// fetch the validator public key
|
2018-09-24 21:09:31 -07:00
|
|
|
consAddr := sdk.ConsAddress(addr)
|
2018-08-21 06:40:43 -07:00
|
|
|
pubkey, err := k.getPubkey(ctx, addr)
|
|
|
|
if err != nil {
|
2019-02-27 13:09:26 -08:00
|
|
|
// 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
|
2018-08-21 06:40:43 -07:00
|
|
|
}
|
2018-05-28 15:10:52 -07:00
|
|
|
|
2019-02-07 17:41:23 -08:00
|
|
|
// Reject evidence if the double-sign is too old
|
2019-01-10 17:22:49 -08:00
|
|
|
if age > k.MaxEvidenceAge(ctx) {
|
2019-02-05 21:30:48 -08:00
|
|
|
logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, age of %d past max age of %d",
|
|
|
|
pubkey.Address(), infractionHeight, age, k.MaxEvidenceAge(ctx)))
|
2019-01-10 17:22:49 -08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get validator and signing info
|
2019-06-04 15:06:58 -07:00
|
|
|
validator := k.sk.ValidatorByConsAddr(ctx, consAddr)
|
|
|
|
if validator == nil || validator.IsUnbonded() {
|
2018-11-04 22:11:03 -08:00
|
|
|
// Defensive.
|
|
|
|
// Simulation doesn't take unbonding periods into account, and
|
|
|
|
// Tendermint might break this assumption at some point.
|
|
|
|
return
|
|
|
|
}
|
2019-02-07 17:41:23 -08:00
|
|
|
|
|
|
|
// fetch the validator signing info
|
2019-01-10 17:22:49 -08:00
|
|
|
signInfo, found := k.getValidatorSigningInfo(ctx, consAddr)
|
|
|
|
if !found {
|
|
|
|
panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr))
|
|
|
|
}
|
2018-11-04 22:11:03 -08:00
|
|
|
|
2019-02-07 17:41:23 -08:00
|
|
|
// validator is already tombstoned
|
2019-01-10 17:22:49 -08:00
|
|
|
if signInfo.Tombstoned {
|
|
|
|
logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, validator already tombstoned", pubkey.Address(), infractionHeight))
|
2018-05-23 13:25:56 -07:00
|
|
|
return
|
|
|
|
}
|
2018-05-28 15:10:52 -07:00
|
|
|
|
2019-02-07 17:41:23 -08:00
|
|
|
// double sign confirmed
|
2019-01-10 17:22:49 -08:00
|
|
|
logger.Info(fmt.Sprintf("Confirmed double sign from %s at height %d, age of %d", pubkey.Address(), infractionHeight, age))
|
2018-06-29 20:34:55 -07:00
|
|
|
|
2018-09-06 21:56:05 -07:00
|
|
|
// We need to retrieve the stake distribution which signed the block, so we subtract ValidatorUpdateDelay from the evidence height.
|
2018-10-05 11:00:01 -07:00
|
|
|
// Note that this *can* result in a negative "distributionHeight", up to -ValidatorUpdateDelay,
|
2018-10-04 09:22:51 -07:00
|
|
|
// 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.
|
2019-02-13 15:01:50 -08:00
|
|
|
distributionHeight := infractionHeight - sdk.ValidatorUpdateDelay
|
2018-09-06 21:56:05 -07:00
|
|
|
|
2019-01-10 17:22:49 -08:00
|
|
|
// get the percentage slash penalty fraction
|
2018-10-05 05:11:36 -07:00
|
|
|
fraction := k.SlashFractionDoubleSign(ctx)
|
|
|
|
|
2018-10-23 11:33:39 -07:00
|
|
|
// Slash validator
|
2018-10-22 01:18:10 -07:00
|
|
|
// `power` is the int64 power of the validator as provided to/by
|
2018-10-23 11:33:39 -07:00
|
|
|
// Tendermint. This value is validator.Tokens as sent to Tendermint via
|
2018-10-22 01:18:10 -07:00
|
|
|
// ABCI, and now received as evidence.
|
2019-01-10 17:22:49 -08:00
|
|
|
// The fraction is passed in to separately to slash unbonding and rebonding delegations.
|
2019-06-04 15:06:58 -07:00
|
|
|
k.sk.Slash(ctx, consAddr, distributionHeight, power, fraction)
|
2018-06-29 20:34:55 -07:00
|
|
|
|
2018-10-05 05:11:36 -07:00
|
|
|
// Jail validator if not already jailed
|
2019-01-10 17:22:49 -08:00
|
|
|
// begin unbonding validator if not already unbonding (tombstone)
|
2019-04-02 06:43:22 -07:00
|
|
|
if !validator.IsJailed() {
|
2019-06-04 15:06:58 -07:00
|
|
|
k.sk.Jail(ctx, consAddr)
|
2018-10-05 05:11:36 -07:00
|
|
|
}
|
2018-08-22 08:56:13 -07:00
|
|
|
|
2019-02-07 17:41:23 -08:00
|
|
|
// Set tombstoned to be true
|
2019-01-10 17:22:49 -08:00
|
|
|
signInfo.Tombstoned = true
|
|
|
|
|
|
|
|
// Set jailed until to be forever (max time)
|
|
|
|
signInfo.JailedUntil = DoubleSignJailEndTime
|
|
|
|
|
|
|
|
// Set validator signing info
|
2018-11-26 04:21:23 -08:00
|
|
|
k.SetValidatorSigningInfo(ctx, consAddr, signInfo)
|
2018-05-23 13:25:56 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// handle a validator signature, must be called once per validator per block
|
2018-09-24 21:09:31 -07:00
|
|
|
// TODO refactor to take in a consensus address, additionally should maybe just take in the pubkey too
|
2018-08-12 00:33:48 -07:00
|
|
|
func (k Keeper) handleValidatorSignature(ctx sdk.Context, addr crypto.Address, power int64, signed bool) {
|
2019-04-15 20:58:06 -07:00
|
|
|
logger := k.Logger(ctx)
|
2018-05-23 13:25:56 -07:00
|
|
|
height := ctx.BlockHeight()
|
2018-09-24 21:09:31 -07:00
|
|
|
consAddr := sdk.ConsAddress(addr)
|
2018-08-12 00:33:48 -07:00
|
|
|
pubkey, err := k.getPubkey(ctx, addr)
|
|
|
|
if err != nil {
|
2018-09-24 21:09:31 -07:00
|
|
|
panic(fmt.Sprintf("Validator consensus-address %v not found", consAddr))
|
2018-08-12 00:33:48 -07:00
|
|
|
}
|
2019-02-07 17:41:23 -08:00
|
|
|
|
|
|
|
// fetch signing info
|
2018-09-24 21:09:31 -07:00
|
|
|
signInfo, found := k.getValidatorSigningInfo(ctx, consAddr)
|
2018-06-10 17:55:54 -07:00
|
|
|
if !found {
|
2018-10-11 13:48:00 -07:00
|
|
|
panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr))
|
2018-06-10 17:55:54 -07:00
|
|
|
}
|
2019-02-07 17:41:23 -08:00
|
|
|
|
|
|
|
// 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
|
2018-07-13 17:12:23 -07:00
|
|
|
index := signInfo.IndexOffset % k.SignedBlocksWindow(ctx)
|
2018-05-28 14:46:08 -07:00
|
|
|
signInfo.IndexOffset++
|
2018-05-28 15:10:52 -07:00
|
|
|
|
|
|
|
// Update signed block bit array & counter
|
2018-05-30 15:32:08 -07:00
|
|
|
// This counter just tracks the sum of the bit array
|
|
|
|
// That way we avoid needing to read/write the whole array each time
|
2018-10-15 12:11:32 -07:00
|
|
|
previous := k.getValidatorMissedBlockBitArray(ctx, consAddr, index)
|
2018-10-11 15:04:44 -07:00
|
|
|
missed := !signed
|
2018-10-15 14:58:39 -07:00
|
|
|
switch {
|
|
|
|
case !previous && missed:
|
2018-10-11 15:04:44 -07:00
|
|
|
// Array value has changed from not missed to missed, increment counter
|
2018-10-15 12:11:32 -07:00
|
|
|
k.setValidatorMissedBlockBitArray(ctx, consAddr, index, true)
|
2018-10-11 15:04:44 -07:00
|
|
|
signInfo.MissedBlocksCounter++
|
2018-10-15 14:58:39 -07:00
|
|
|
case previous && !missed:
|
2018-10-11 15:04:44 -07:00
|
|
|
// Array value has changed from missed to not missed, decrement counter
|
2018-10-15 12:11:32 -07:00
|
|
|
k.setValidatorMissedBlockBitArray(ctx, consAddr, index, false)
|
2018-10-11 15:04:44 -07:00
|
|
|
signInfo.MissedBlocksCounter--
|
2018-10-15 14:58:39 -07:00
|
|
|
default:
|
|
|
|
// Array value at this index has not changed, no need to update counter
|
2018-05-23 13:25:56 -07:00
|
|
|
}
|
2018-05-28 15:10:52 -07:00
|
|
|
|
2018-10-11 15:04:44 -07:00
|
|
|
if missed {
|
2019-02-27 13:09:26 -08:00
|
|
|
logger.Info(fmt.Sprintf("Absent validator %s (%v) at height %d, %d missed, threshold %d", addr, pubkey, height, signInfo.MissedBlocksCounter, k.MinSignedPerWindow(ctx)))
|
2018-06-29 20:34:55 -07:00
|
|
|
}
|
2019-02-07 17:41:23 -08:00
|
|
|
|
2018-07-13 17:12:23 -07:00
|
|
|
minHeight := signInfo.StartHeight + k.SignedBlocksWindow(ctx)
|
2018-10-11 15:04:44 -07:00
|
|
|
maxMissed := k.SignedBlocksWindow(ctx) - k.MinSignedPerWindow(ctx)
|
2019-02-07 17:41:23 -08:00
|
|
|
|
|
|
|
// if we are past the minimum height and the validator has missed too many blocks, punish them
|
2018-10-12 12:15:39 -07:00
|
|
|
if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
|
2019-06-04 15:06:58 -07:00
|
|
|
validator := k.sk.ValidatorByConsAddr(ctx, consAddr)
|
2019-04-02 06:43:22 -07:00
|
|
|
if validator != nil && !validator.IsJailed() {
|
2019-02-07 17:41:23 -08:00
|
|
|
|
2018-08-22 08:56:13 -07:00
|
|
|
// Downtime confirmed: slash and jail the validator
|
2018-07-24 19:12:48 -07:00
|
|
|
logger.Info(fmt.Sprintf("Validator %s past min height of %d and below signed blocks threshold of %d",
|
|
|
|
pubkey.Address(), minHeight, k.MinSignedPerWindow(ctx)))
|
2019-02-07 17:41:23 -08:00
|
|
|
|
2018-09-06 21:56:05 -07:00
|
|
|
// 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.
|
2018-10-05 11:00:01 -07:00
|
|
|
// Note that this *can* result in a negative "distributionHeight" up to -ValidatorUpdateDelay-1,
|
2018-10-04 09:22:51 -07:00
|
|
|
// 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.
|
2019-02-13 15:01:50 -08:00
|
|
|
distributionHeight := height - sdk.ValidatorUpdateDelay - 1
|
2019-06-04 15:06:58 -07:00
|
|
|
k.sk.Slash(ctx, consAddr, distributionHeight, power, k.SlashFractionDowntime(ctx))
|
|
|
|
k.sk.Jail(ctx, consAddr)
|
2019-01-10 17:22:49 -08:00
|
|
|
signInfo.JailedUntil = ctx.BlockHeader().Time.Add(k.DowntimeJailDuration(ctx))
|
2019-02-07 17:41:23 -08:00
|
|
|
|
2018-10-15 12:44:23 -07:00
|
|
|
// 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)
|
2018-07-24 19:12:48 -07:00
|
|
|
} else {
|
2018-08-22 08:56:13 -07:00
|
|
|
// 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",
|
2018-07-24 19:12:48 -07:00
|
|
|
pubkey.Address()))
|
|
|
|
}
|
2018-05-23 13:25:56 -07:00
|
|
|
}
|
2018-05-28 17:35:42 -07:00
|
|
|
|
|
|
|
// Set the updated signing info
|
2018-11-26 04:21:23 -08:00
|
|
|
k.SetValidatorSigningInfo(ctx, consAddr, signInfo)
|
2018-05-23 13:25:56 -07:00
|
|
|
}
|
2018-08-12 00:33:48 -07:00
|
|
|
|
|
|
|
func (k Keeper) addPubkey(ctx sdk.Context, pubkey crypto.PubKey) {
|
|
|
|
addr := pubkey.Address()
|
|
|
|
k.setAddrPubkeyRelation(ctx, addr, pubkey)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (k Keeper) getPubkey(ctx sdk.Context, address crypto.Address) (crypto.PubKey, error) {
|
|
|
|
store := ctx.KVStore(k.storeKey)
|
|
|
|
var pubkey crypto.PubKey
|
2018-11-04 18:28:38 -08:00
|
|
|
err := k.cdc.UnmarshalBinaryLengthPrefixed(store.Get(getAddrPubkeyRelationKey(address)), &pubkey)
|
2018-08-12 00:33:48 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("address %v not found", address)
|
|
|
|
}
|
|
|
|
return pubkey, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (k Keeper) setAddrPubkeyRelation(ctx sdk.Context, addr crypto.Address, pubkey crypto.PubKey) {
|
|
|
|
store := ctx.KVStore(k.storeKey)
|
2018-11-04 18:28:38 -08:00
|
|
|
bz := k.cdc.MustMarshalBinaryLengthPrefixed(pubkey)
|
2018-08-12 00:33:48 -07:00
|
|
|
store.Set(getAddrPubkeyRelationKey(addr), bz)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (k Keeper) deleteAddrPubkeyRelation(ctx sdk.Context, addr crypto.Address) {
|
|
|
|
store := ctx.KVStore(k.storeKey)
|
|
|
|
store.Delete(getAddrPubkeyRelationKey(addr))
|
|
|
|
}
|