Slashing
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
_3type 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:
_32slashAmountUnbondings := 0_32slashAmountRedelegations := 0_32_32unbondings := getUnbondings(validator.Address)_32for 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._32redels := getRedelegationsBySource(validator.Address)_32for 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:
_13curVal := validator_13oldVal := loadValidator(evidence.Height, evidence.Address)_13_13slashAmount := SLASH_PROPORTION * oldVal.Shares_13slashAmount -= slashAmountUnbondings_13slashAmount -= slashAmountRedelegations_13_13curVal.Shares = max(0, curVal.Shares - slashAmount)_13_13signInfo = SigningInfo.Get(val.Address)_13signInfo.JailedUntil = MAX_TIME_13signInfo.Tombstoned = true_13SigningInfo.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
.
Liveness slashes do not lead to tombstombing.
_69height := block.Height_69_69for 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
.
_8type 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:
_8 # Slashing: slash window setup_8 genesis['app_state']['slashing']['params'] = {_8 'downtime_jail_duration': '600s',_8 'min_signed_per_window': '0.05',_8 'signed_blocks_window': '10000',_8 'slash_fraction_double_sign': '0.05', # 5%_8 'slash_fraction_downtime': '0.0001' # 0.01%_8 }
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%