Slashing

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

Concepts

States

At any given time, there are any number of validators registered in the state machine. Each block, the top MaxValidators (defined by x/staking) validators who are not jailed become bonded, meaning that they may propose and vote on blocks. Validators who are bonded are at stake, meaning that part or all of their stake and their delegators’ stake is at risk if they commit a protocol fault.

For each of these validators we keep a ValidatorSigningInfo record that contains information partaining to validator’s liveness and other infraction related attributes.

Tombstone Caps

In order to mitigate the impact of initially likely categories of non-malicious protocol faults, the XPLA Chain implements for each validator a tombstone cap, which only allows a validator to be slashed once for a double sign fault. For example, if you misconfigure your HSM and double-sign a bunch of old blocks, you’ll only be punished for the first double-sign (and then immediately tombstombed). This will still be quite expensive and desirable to avoid, but tombstone caps somewhat blunt the economic impact of unintentional misconfiguration.

Liveness faults do not have caps, as they can’t stack upon each other. Liveness bugs are “detected” as soon as the infraction occurs, and the validators are immediately put in jail, so it is not possible for them to commit multiple liveness faults without unjailing in between.

Infraction Timelines

To illustrate how the x/slashing module handles submitted evidence through Tendermint consensus, consider the following examples:

Definitions:

[ : timeline start
] : timeline end
Cn : infraction n committed
Dn : infraction n discovered
Vb : validator bonded
Vu : validator unbonded

Single Double Sign Infraction

<—————–> [———-C1—-D1,Vu—–]

A single infraction is committed then later discovered, at which point the validator is unbonded and slashed at the full amount for the infraction.

Multiple Double Sign Infractions

<—————————> [———-C1–C2—C3—D1,D2,D3Vu—–]

Multiple infractions are committed and then later discovered, at which point the validator is jailed and slashed for only one infraction. Because the validator is also tombstoned, they can not rejoin the validator set.

Message Types

MsgUnjail

// MsgUnjail defines the Msg/Unjail request type
type MsgUnjail struct {
	ValidatorAddr string `protobuf:"bytes,1,opt,name=validator_addr,json=validatorAddr,proto3" json:"address" yaml:"address"`
}

Transitions

BeginBlock

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 incremented 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)
}

Hooks

This section contains a description of the module’s hooks. Hooks are operations that are executed automatically when events are raised.

Staking hooks

The slashing module implements the StakingHooks defined in x/staking and are used as record-keeping of validators information. During the app initialization, these hooks should be registered in the staking module struct.

The following hooks impact the slashing state:

  • AfterValidatorBonded creates a ValidatorSigningInfo instance as described in the following section.
  • AfterValidatorCreated stores a validator’s consensus key.
  • AfterValidatorRemoved removes a validator’s consensus key.

Validator Bonded

Upon successful first-time bonding of a new validator, we create a new ValidatorSigningInfo structure for the now-bonded validator, which StartHeight of the current block.

onValidatorBonded(address sdk.ValAddress)

  signingInfo, found = GetValidatorSigningInfo(address)
  if !found {
    signingInfo = ValidatorSigningInfo {
      StartHeight         : CurrentHeight,
      IndexOffset         : 0,
      JailedUntil         : time.Unix(0, 0),
      Tombstone           : false,
      MissedBloskCounter  : 0
    }
    setValidatorSigningInfo(signingInfo)
  }

  return

Parameters

The subspace for the slashing module is slashing.

// Params represents the parameters used for by the slashing module.
type Params struct {
	SignedBlocksWindow      int64                                  `protobuf:"varint,1,opt,name=signed_blocks_window,json=signedBlocksWindow,proto3" json:"signed_blocks_window,omitempty" yaml:"signed_blocks_window"`
	MinSignedPerWindow      github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,2,opt,name=min_signed_per_window,json=minSignedPerWindow,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"min_signed_per_window" yaml:"min_signed_per_window"`
	DowntimeJailDuration    time.Duration                          `protobuf:"bytes,3,opt,name=downtime_jail_duration,json=downtimeJailDuration,proto3,stdduration" json:"downtime_jail_duration" yaml:"downtime_jail_duration"`
	SlashFractionDoubleSign github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,4,opt,name=slash_fraction_double_sign,json=slashFractionDoubleSign,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"slash_fraction_double_sign" yaml:"slash_fraction_double_sign"`
	SlashFractionDowntime   github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,5,opt,name=slash_fraction_downtime,json=slashFractionDowntime,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"slash_fraction_downtime" yaml:"slash_fraction_downtime"`
}

SignedBlocksWindow

  • type: int64
  • "signed_blocks_window": "100"

MinSignedPerWindow

  • type: Dec
  • "min_signed_per_window": "0.500000000000000000"

DowntimeJailDuration

  • type: time.Duration (seconds)
  • "downtime_jail_duration": "600s"

SlashFractionDoubleSign

  • type: Dec
  • "slash_fraction_double_sign": "0.050000000000000000"

SlashFractionDowntime

  • type: Dec
  • "slash_fraction_downtime": "0.010000000000000000"