Terra Documentation
  • Docs
  • Whitepaper
  • Agora
  • GitHub

›MODULE SPECS

Overview

  • Introduction
  • Columbus-3 Mainnet

Terra Protocol

  • How Terra Works
  • Developer Guide
  • MODULE SPECS

    • Auth
    • Bank
    • Supply
    • Distribution
    • Staking
    • Slashing
    • Oracle
    • Market
    • Treasury
    • Governance

User Manual

    NODE SOFTWARE

    • Installation
    • User Guide
    • Light Client Daemon
    • terracli Reference
    • Ledger Nano Support
    • Production Settings
    • Join a Network
    • Deploy a Testnet

Validator Handbook

  • Introduction
  • Validator FAQ
  • GUIDES

    • Getting Started
    • Security Best Practices
    • Exchange Rate Oracle

Contribute

  • Contribution guidelines for Terra Core
  • Help to translate

Slashing

Terra's Slashing module inherits from Cosmos SDK's slashing module. This document is a stub, and covers mainly important Terra-specific notes about 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 ("slashing"). Terra mainly uses the Staking module to slash when violating validator responsibilities (such as missing too many VotePeriods in the exchange rate oracle). This module deals with lower-level penalties at the Tendermint consensus level, such as double-signing.

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, as well as double-signing and other low-level consensus penalties.

Evidence Handling

Tendermint blocks can include Evidence, which indicates that a validator committed malicious behavior. The relevant information is forwarded to the application as ABCI Evidence in abci.RequestBeginBlock so that the validator an be accordingly 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 (rather than 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 started unbonding.

We first need to loop through the unbondings and redelegations from the slashed validator and track how much stake has since moved:

slashAmountUnbondings := 0
slashAmountRedelegations := 0

unbondings := getUnbondings(validator.Address)
for unbond in unbondings {

    if was not bonded before evidence.Height or started unbonding before unbonding period ago {
        continue
    }

    burn := unbond.InitialTokens * SLASH_PROPORTION
    slashAmountUnbondings += burn

    unbond.Tokens = max(0, unbond.Tokens - burn)
}

// only care if source gets slashed because we're already bonded to destination
// so if destination validator gets slashed our delegation just has same shares
// of smaller pool.
redels := getRedelegationsBySource(validator.Address)
for redel in redels {

    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 and tombstone them:

curVal := validator
oldVal := loadValidator(evidence.Height, evidence.Address)

slashAmount := SLASH_PROPORTION * oldVal.Shares
slashAmount -= slashAmountUnbondings
slashAmount -= slashAmountRedelegations

curVal.Shares = max(0, curVal.Shares - slashAmount)

signInfo = SigningInfo.Get(val.Address)
signInfo.JailedUntil = MAX_TIME
signInfo.Tombstoned = true
SigningInfo.Set(val.Address, signInfo)

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. The amount slashed for all double signature infractions committed within a single slashing period is capped as described here.

Liveness Tracking

At the beginning of each block, we update the ValidatorSigningInfo for each validator and check if they've crossed below the liveness threshold over a sliding window. 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 incrimented regardless if the validator signed or not. Once the index is determined, the MissedBlocksBitArray and MissedBlocksCounter are updated accordingly.

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

Note: Liveness slashes do NOT lead to a tombstombing.

height := block.Height

for vote in block.LastCommitInfo.Votes {
  signInfo := GetValidatorSigningInfo(vote.Validator.Address)

  // This is a relative index, so we counts blocks the validator SHOULD have
  // signed. We use the 0-value default signing info if not present, except for
  // start height.
  index := signInfo.IndexOffset % SignedBlocksWindow()
  signInfo.IndexOffset++

  // Update MissedBlocksBitArray and MissedBlocksCounter. The MissedBlocksCounter
  // just tracks the sum of MissedBlocksBitArray. That way we avoid needing to
  // read/write the whole array each time.
  missedPrevious := GetValidatorMissedBlockBitArray(vote.Validator.Address, index)
  missed := !signed

  switch {
  case !missedPrevious && missed:
    // array index has changed from not missed to missed, increment counter
    SetValidatorMissedBlockBitArray(vote.Validator.Address, index, true)
    signInfo.MissedBlocksCounter++

  case missedPrevious && !missed:
    // array index has changed from missed to not missed, decrement counter
    SetValidatorMissedBlockBitArray(vote.Validator.Address, index, false)
    signInfo.MissedBlocksCounter--

  default:
    // array index at this index has not changed; no need to update counter
  }

  if missed {
    // emit events...
  }

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

  // If we are past the minimum height and the validator has missed too many
  // jail and slash them.
  if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
    validator := ValidatorByConsAddr(vote.Validator.Address)

    // emit events...

    // We need to retrieve the stake distribution which signed the block, so we
    // subtract ValidatorUpdateDelay from the block 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

    Slash(vote.Validator.Address, distributionHeight, vote.Validator.Power, SlashFractionDowntime())
    Jail(vote.Validator.Address)

    signInfo.JailedUntil = block.Time.Add(DowntimeJailDuration())

    // 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
    ClearValidatorMissedBlockBitArray(vote.Validator.Address)
  }

  SetValidatorSigningInfo(vote.Validator.Address, signInfo)
}

Parameters

The subspace for the Slashing module is slashing.

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

MaxEvidenceAge

  • type: time.Duration
  • default value: 60 * 2 * time.Second (2 minutes)

SignedBlocksWindow

  • type: int64
  • default value: 100

MinSignedPerWindow

  • type: sdk.Dec
  • default value: sdk.NewDecWithPrec(5, 1) (5)

DowntimeJailDuration

  • type: time.Duration
  • default value: 60 * 10 * time.Second (10 minutes)

SlashFractionDoubleSign

  • type: sdk.Dec
  • default value: sdk.NewDec(1).Quo(sdk.NewDec(20)) (1/20)

SlashFractionDowntime

  • type: sdk.Dec
  • default value: sdk.NewDec(1).Quo(sdk.NewDec(100)) (1/100)
Last updated on 11/30/2019
← StakingOracle →
  • Transitions
    • Begin Block
  • Parameters
    • MaxEvidenceAge
    • SignedBlocksWindow
    • MinSignedPerWindow
    • DowntimeJailDuration
    • SlashFractionDoubleSign
    • SlashFractionDowntime
Terra Documentation
Docs
Getting Started (or other categories)Guides (or other categories)API Reference (or other categories)
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHubStar
Follow @terra_money
© 2019 Terra