Skip to main content

Slashing

ℹ️info

Terra's slashing module inherits from the Cosmos SDK's slashing module. This document is a stub and mainly covers important Terra-specific notes on how it is used.

The slashing module enables Terra to disincentivize any attributable action by a protocol-recognized actor with value at stake by penalizing them. The penalty is called slashing. Terra mainly uses the Staking module to slash a validator who violates their responsibilities. This module manages lower-level penalties at the Tendermint consensus level, such as double-signing.

Message Types

MsgUnjail


_3
type MsgUnjail struct {
_3
ValidatorAddr sdk.ValAddress `json:"address" yaml:"address"` // address of the validator operator
_3
}

Transitions

Begin-Block

This section was taken from the official Cosmos SDK docs, and placed here for your convenience to understand the slashing module's parameters.

At the beginning of each block, the slashing module checks for evidence of infractions or downtime of validators, double-signing, and other low-level consensus penalties.

Evidence handling

Tendermint blocks can include evidence, which indicates that a validator committed malicious behaviour. The relevant information is forwarded to the application as ABCI Evidence in abci.RequestBeginBlock so that the validator an be punished.

For some Evidence submitted in block to be valid, it must satisfy:

Evidence.Timestamp >= block.Timestamp - MaxEvidenceAge

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 some penalty (SlashFractionDoubleSign for equivocation) of what their stake was when the infraction occurred instead of when the evidence was discovered. We want to follow the stake, i.e. the stake which contributed to the infraction should be slashed, even if it has since been redelegated or has started unbonding.

The unbondings and redelegations from the slashed validator are looped through, and the amount of stake that has moved is tracked:


_32
slashAmountUnbondings := 0
_32
slashAmountRedelegations := 0
_32
_32
unbondings := getUnbondings(validator.Address)
_32
for unbond in unbondings {
_32
_32
if was not bonded before evidence.Height or started unbonding before unbonding period ago {
_32
continue
_32
}
_32
_32
burn := unbond.InitialTokens * SLASH_PROPORTION
_32
slashAmountUnbondings += burn
_32
_32
unbond.Tokens = max(0, unbond.Tokens - burn)
_32
}
_32
_32
// only care if source gets slashed because we're already bonded to destination
_32
// so if destination validator gets slashed the delegation just has same shares
_32
// of smaller pool.
_32
redels := getRedelegationsBySource(validator.Address)
_32
for redel in redels {
_32
_32
if was not bonded before evidence.Height or started redelegating before unbonding period ago {
_32
continue
_32
}
_32
_32
burn := redel.InitialTokens * SLASH_PROPORTION
_32
slashAmountRedelegations += burn
_32
_32
amount := unbondFromValidator(redel.Destination, burn)
_32
destroy(amount)
_32
}

The validator is slashed and tombstoned:


_13
curVal := validator
_13
oldVal := loadValidator(evidence.Height, evidence.Address)
_13
_13
slashAmount := SLASH_PROPORTION * oldVal.Shares
_13
slashAmount -= slashAmountUnbondings
_13
slashAmount -= slashAmountRedelegations
_13
_13
curVal.Shares = max(0, curVal.Shares - slashAmount)
_13
_13
signInfo = SigningInfo.Get(val.Address)
_13
signInfo.JailedUntil = MAX_TIME
_13
signInfo.Tombstoned = true
_13
SigningInfo.Set(val.Address, signInfo)

This process ensures that offending validators are punished with the same amount whether they act as a single validator with X stake or as N validators with a collective X stake. The amount slashed for all double-signature infractions committed within a single slashing period is capped. For more information, see tombstone caps.

Liveness tracking

At the beginning of each block, the ValidatorSigningInfo for each validator is updated and whether they've crossed below the liveness threshold over a sliding window is checked. This sliding window is defined by SignedBlocksWindow, and the index in this window is determined by IndexOffset found in the validator's ValidatorSigningInfo. For each block processed, the IndexOffset is incremented regardless of whether the validator signed. After the index is determined, the MissedBlocksBitArray and MissedBlocksCounter are updated accordingly.

Finally, to determine whether a validator crosses below the liveness threshold, the maximum number of blocks missed, maxMissed, which is SignedBlocksWindow - (MinSignedPerWindow * SignedBlocksWindow), and the minimum height at which liveness can be determined, minHeight, are fetched. If the current block is greater than minHeight and the validator's MissedBlocksCounter is greater than maxMissed, they are slashed by SlashFractionDowntime, jailed for DowntimeJailDuration, and have the following values reset: MissedBlocksBitArray, MissedBlocksCounter, and IndexOffset.

📝note

Liveness slashes do not lead to tombstombing.


_69
height := block.Height
_69
_69
for vote in block.LastCommitInfo.Votes {
_69
signInfo := GetValidatorSigningInfo(vote.Validator.Address)
_69
_69
// This is a relative index, so it counts blocks the validator SHOULD have
_69
// signed. The 0-value default signing info is used if no signed block is present, except for
_69
// start height.
_69
index := signInfo.IndexOffset % SignedBlocksWindow()
_69
signInfo.IndexOffset++
_69
_69
// Update MissedBlocksBitArray and MissedBlocksCounter. The MissedBlocksCounter
_69
// just tracks the sum of MissedBlocksBitArray to avoid needing to
_69
// read/write the whole array each time.
_69
missedPrevious := GetValidatorMissedBlockBitArray(vote.Validator.Address, index)
_69
missed := !signed
_69
_69
switch {
_69
case !missedPrevious && missed:
_69
// array index has changed from not missed to missed, increment counter
_69
SetValidatorMissedBlockBitArray(vote.Validator.Address, index, true)
_69
signInfo.MissedBlocksCounter++
_69
_69
case missedPrevious && !missed:
_69
// array index has changed from missed to not missed, decrement counter
_69
SetValidatorMissedBlockBitArray(vote.Validator.Address, index, false)
_69
signInfo.MissedBlocksCounter--
_69
_69
default:
_69
// array index at this index has not changed; no need to update counter
_69
}
_69
_69
if missed {
_69
// emit events...
_69
}
_69
_69
minHeight := signInfo.StartHeight + SignedBlocksWindow()
_69
maxMissed := SignedBlocksWindow() - MinSignedPerWindow()
_69
_69
// If the minimum height has been reached and the validator has missed too many
_69
// jail and slash them.
_69
if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
_69
validator := ValidatorByConsAddr(vote.Validator.Address)
_69
_69
// emit events...
_69
_69
// To retrieve the stake distribution which signed the block,
_69
// subtract ValidatorUpdateDelay from the block height, and subtract an
_69
// additional 1 since this is the LastCommit.
_69
//
_69
// Note, that this CAN result in a negative "distributionHeight" up to
_69
// -ValidatorUpdateDelay-1, i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block.
_69
// That's fine since this is just used to filter unbonding delegations & redelegations.
_69
distributionHeight := height - sdk.ValidatorUpdateDelay - 1
_69
_69
Slash(vote.Validator.Address, distributionHeight, vote.Validator.Power, SlashFractionDowntime())
_69
Jail(vote.Validator.Address)
_69
_69
signInfo.JailedUntil = block.Time.Add(DowntimeJailDuration())
_69
_69
// Reset the counter & array so that the validator won't be
_69
// immediately slashed for downtime upon rebonding.
_69
signInfo.MissedBlocksCounter = 0
_69
signInfo.IndexOffset = 0
_69
ClearValidatorMissedBlockBitArray(vote.Validator.Address)
_69
}
_69
_69
SetValidatorSigningInfo(vote.Validator.Address, signInfo)
_69
}

Parameters

The subspace for the slashing module is slashing.


_8
type Params struct {
_8
MaxEvidenceAge time.Duration `json:"max_evidence_age" yaml:"max_evidence_age"`
_8
SignedBlocksWindow int64 `json:"signed_blocks_window" yaml:"signed_blocks_window"`
_8
MinSignedPerWindow sdk.Dec `json:"min_signed_per_window" yaml:"min_signed_per_window"`
_8
DowntimeJailDuration time.Duration `json:"downtime_jail_duration" yaml:"downtime_jail_duration"`
_8
SlashFractionDoubleSign sdk.Dec `json:"slash_fraction_double_sign" yaml:"slash_fraction_double_sign"`
_8
SlashFractionDowntime sdk.Dec `json:"slash_fraction_downtime" yaml:"slash_fraction_downtime"`
_8
}

Genesis parameters

The genesis parameters for the crisis module outlined in the Genesis Builder Script are as follows:


_7
# Slashing: slash window setup
_7
genesis['app_state']['slashing']['params'] = {
_7
'downtime_jail_duration': '600s',
_7
'min_signed_per_window': '0.05',
_7
'signed_blocks_window': '10000',
_7
'slash_fraction_double_sign': '0.05', # 5%
_7
'slash_fraction_downtime': '0.0001' # 0.01%


_6
# Consensus Params: Evidence
_6
genesis['consensus_params']['evidence'] = {
_6
'max_age_num_blocks': '100000',
_6
'max_age_duration': '172800000000000',
_6
'max_bytes': '1000000'
_6
}

SignedBlocksWindow

  • type: int64
  • default: 100

MinSignedPerWindow

  • type: Dec
  • default: .05

DowntimeJailDuration

  • type: time.Duration (seconds)
  • default: 600s

SlashFractionDoubleSign

  • type: Dec
  • default: 5%

SlashFractionDowntime

  • type: Dec
  • default: .01%