摘要

本节说明 Cosmos SDK 的 slashing 模块。该模块实现了 2016 年 6 月在 Cosmos 白皮书 中首次概述的功能。 slashing 模块使基于 Cosmos SDK 的区块链能够通过惩罚具有可归责行为且存在质押风险的协议认可参与者(即“罚没”)来抑制此类行为。 处罚可能包括但不限于:
  • 销毁其部分质押
  • 在一段时间内取消其对未来区块的投票能力。
该模块将由 Cosmos 生态中的第一个 Hub,即 Cosmos Hub 使用。

目录

概念

状态

在任意时刻,状态机中都会注册若干验证者。在每个区块中,排名前 MaxValidators(由 x/staking 定义)且未被监禁的验证者会成为已绑定状态,这意味着他们可以提议区块并对区块投票。处于已绑定状态的验证者也意味着其处于质押风险中,即如果其发生协议故障,其自身质押以及其委托人的质押都可能面临部分或全部损失。 对于每个这样的验证者,我们都会维护一条 ValidatorSigningInfo 记录,其中包含与验证者活性及其他违规相关属性有关的信息。

墓碑上限

为了减轻那些一开始较可能出现、但并非恶意的协议故障类别所带来的影响,Cosmos Hub 为每个验证者实现了一个墓碑上限,这意味着验证者因双签故障最多只会被罚没一次。举例来说,如果你错误配置了 HSM,导致对一批旧区块进行了双签,那么你只会因第一次双签受到处罚(随后会立即进入墓碑状态)。这仍然会带来相当高的成本,因此应尽量避免,但墓碑上限在一定程度上削弱了无意配置错误造成的经济影响。 活性故障没有上限,因为它们不会彼此叠加。活性问题会在违规发生时立即被“检测”到,验证者也会立刻被监禁,因此他们不可能在未先解除监禁的情况下连续发生多次活性故障。

违规时间线

为了说明 x/slashing 模块如何通过 CometBFT 共识处理提交的证据,请参考以下示例: 定义: [ : 时间线开始
] : 时间线结束
Cn : 第 n 次违规发生
Dn : 第 n 次违规被发现
Vb : 验证者已绑定
Vu : 验证者已解绑

单次双签违规

[----------C1----D1,Vu-----] 一次违规先发生,随后在稍后被发现;此时验证者已解绑,并会按照该次违规的全额受到罚没。

多次双签违规

[----------C1—C2---C3---D1,D2,D3Vu-----] 多次违规先发生,随后在稍后被发现;此时验证者会被监禁,并且只会因其中一次违规而被罚没。由于验证者还会进入墓碑状态,因此不能重新加入验证者集合。

状态

签名信息(活性)

每个区块都包含验证者针对前一个区块的一组预提交,这组信息称为由 CometBFT 提供的 LastCommitInfo。只要 LastCommitInfo 中包含了总投票权 +2/3 的预提交,它就是有效的。 提议者会被激励在 CometBFT 的 LastCommitInfo 中纳入所有验证者的预提交,因为他们可以根据 LastCommitInfo 中所包含投票权与 +2/3 之间的差值获得额外费用(参见费用分配)。
type LastCommitInfo struct {
    Round int32
	Votes []VoteInfo
}
如果验证者在若干个区块中未被包含在 LastCommitInfo 内,就会受到处罚:自动被监禁、可能被罚没并被解绑。 验证者活性活动的信息通过 ValidatorSigningInfo 进行跟踪。 它在存储中的索引如下:
  • ValidatorSigningInfo: 0x01 | ConsAddrLen (1 byte) | ConsAddress -> ProtocolBuffer(ValSigningInfo)
  • MissedBlocksBitArray: 0x02 | ConsAddrLen (1 byte) | ConsAddress | LittleEndianUint64(signArrayIndex) -> VarInt(didMiss)(varint 是一种数字编码格式)
第一条映射使我们能够根据验证者的共识地址,方便地查找该验证者最近的签名信息。 第二条映射(MissedBlocksBitArray)充当一个大小为 SignedBlocksWindow 的位数组,用来告诉我们验证者在位数组给定索引处是否错过了对应区块。位数组中的索引以小端序 uint64 给出。 结果是一个取值为 0 或 1 的 varint,其中 0 表示验证者没有错过对应区块(即已签名),1 表示其错过了该区块(即未签名)。 请注意,MissedBlocksBitArray 不会在一开始显式初始化。对于新进入已绑定状态的验证者,随着前 SignedBlocksWindow 个区块逐步推进,相应的键才会被加入。SignedBlocksWindow 参数定义了用于跟踪验证者活性的滑动窗口大小(即区块数)。 用于跟踪验证者活性而存储的信息如下:
// Reference: https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/slashing/v1beta1/slashing.proto#L13-L35

参数

slashing 模块将其参数以 0x00 为前缀存储在状态中,可以通过治理或具有权限的地址进行更新。
  • Params: 0x00 | ProtocolBuffer(Params)
// Reference: https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/slashing/v1beta1/slashing.proto#L37-L59

消息

本节描述 slashing 模块中消息的处理流程。

解除监禁

如果验证者由于宕机而被自动解绑,并希望重新上线且可能重新加入已绑定集合,则必须发送 MsgUnjail:
// MsgUnjail is an sdk.Msg used for unjailing a jailed validator, thus returning
// them into the bonded validator set, so they can begin receiving provisions
// and rewards again.
message MsgUnjail {
  string validator_addr = 1;
}
下面是 MsgSrv/Unjail RPC 的伪代码:
unjail(tx MsgUnjail)

validator = getValidator(tx.ValidatorAddr)
    if validator == nil
      fail with "No validator found"
    if getSelfDelegation(validator) == 0
      fail with "validator must self delegate before unjailing"
    if !validator.Jailed
      fail with "Validator not jailed, cannot unjail"

    info = GetValidatorSigningInfo(operator)
    if info.Tombstoned
      fail with "Tombstoned validator cannot be unjailed"
    if block time < info.JailedUntil
      fail with "Validator still jailed, cannot unjail until period has expired"

    validator.Jailed = false
    setValidator(validator)

return
如果验证者拥有足够的质押,能够进入前 n = MaximumBondedValidators,则会自动重新绑定;所有仍委托给该验证者的委托人也会重新绑定,并再次开始获得增发和奖励。

BeginBlock

活性跟踪

在每个区块开始时,我们会更新每个验证者的 ValidatorSigningInfo,并检查他们在一个滑动窗口内是否跌破活性阈值。这个滑动窗口由 SignedBlocksWindow 定义,而窗口中的索引由验证者 ValidatorSigningInfo 中的 IndexOffset 决定。每处理一个区块,无论验证者是否签名,IndexOffset 都会递增。索引确定后,会相应更新 MissedBlocksBitArray 和 MissedBlocksCounter。 最后,为了判断验证者是否跌破活性阈值,我们会获取允许错过的最大区块数 maxMissed,其值为 SignedBlocksWindow - (MinSignedPerWindow * SignedBlocksWindow),以及可以判定活性的最小高度 minHeight。如果当前区块高度大于 minHeight,并且验证者的 MissedBlocksCounter 大于 maxMissed,那么该验证者将按照 SlashFractionDowntime 被罚没,被监禁 DowntimeJailDuration,并重置以下值:MissedBlocksBitArray、MissedBlocksCounter 和 IndexOffset。 注意:活性罚没不会导致墓碑状态。
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

    SlashWithInfractionReason(vote.Validator.Address, distributionHeight, vote.Validator.Power, SlashFractionDowntime(), stakingtypes.Downtime)

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。hooks 是在事件触发时自动执行的操作。

质押钩子

slashing 模块实现了 x/staking 中定义的 StakingHooks,用于记录验证者信息。在应用初始化期间,应当将这些钩子注册到质押模块的结构体中。 以下钩子会影响 slashing 状态:
  • AfterValidatorBonded 会创建一个 ValidatorSigningInfo 实例,具体说明见下一节。
  • AfterValidatorCreated 会存储验证者的共识密钥。
  • AfterValidatorRemoved 会移除验证者的共识密钥。

验证者已绑定

当一个新验证者首次成功绑定时,我们会为这个现已绑定的验证者创建一个新的 ValidatorSigningInfo 结构,其中 StartHeight 为当前区块高度。 如果该验证者曾退出验证者集合并再次完成绑定,则会设置其新的绑定高度。
onValidatorBonded(address sdk.ValAddress)

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

else {
    signingInfo.StartHeight = CurrentHeight
}

setValidatorSigningInfo(signingInfo)
}

return

事件

slashing 模块会发出以下事件:

MsgServer

MsgUnjail

类型属性键属性值
messagemoduleslashing
messagesender{validatorAddress}

Keeper

BeginBlocker:HandleValidatorSignature

类型属性键属性值
slashaddress{validatorConsensusAddress}
slashpower{validatorPower}
slashreason{slashReason}
slashjailed [0]{validatorConsensusAddress}
slashburned coins{math.Int}
  • [0] 仅在验证者被监禁时包含。
类型属性键属性值
livenessaddress{validatorConsensusAddress}
livenessmissed_blocks{missedBlocksCounter}
livenessheight{blockHeight}

惩罚

  • 与 HandleValidatorSignature 的 "slash" 事件相同,但不包含 jailed 属性。

监禁

类型属性键属性值
slashjailed{validatorAddress}

质押墓碑

摘要

在当前 slashing 模块的实现中,当共识引擎将验证者的共识故障通知给状态机时,验证者会被部分惩罚,并进入一个“监禁期”,即在这段时间内不允许重新加入验证者集合。然而,由于共识故障和 ABCI 的特性,从违规发生到相关证据送达状态机之间可能存在延迟(这也是解绑期存在的主要原因之一)。
注意:墓碑概念只适用于那些从违规发生到证据送达状态机之间存在延迟的故障。例如,验证者双签的证据可能需要一段时间才能送达状态机,因为证据 gossip 层的延迟不可预测,而且验证者还可以选择性地披露双签(例如仅向不常在线的轻客户端披露)。另一方面,活性惩罚会在违规发生时立即被检测到,因此不需要惩罚期。验证者会立即进入监禁期,在解除监禁之前不能再次提交活性故障。未来也可能出现其他存在延迟的拜占庭故障类型(例如将无效提案的证据作为交易提交)。当这些类型被实现时,需要决定它们是否会导致墓碑状态(如果不会,则惩罚额度不会受到惩罚期的上限约束)。
在当前系统设计中,一旦验证者因共识故障被监禁,在 JailPeriod 结束后,他们可以发送一笔 unjail 交易来解除监禁,从而重新加入验证者集合。 slashing 模块的一个“设计目标”是:如果在证据被执行之前发生了多次违规(并最终导致验证者被监禁),则只应对其中最严重的一次违规进行处罚,而不是累积处罚。例如,若事件顺序如下:
  1. 验证者 A 提交违规 1(应惩罚 30%)
  2. 验证者 A 提交违规 2(应惩罚 40%)
  3. 验证者 A 提交违规 3(应惩罚 35%)
  4. 违规 1 的证据到达状态机(并且验证者被监禁)
  5. 违规 2 的证据到达状态机
  6. 违规 3 的证据到达状态机
只有违规 2 的惩罚应当生效,因为它最严重。这样做是为了保证:在验证者共识密钥被攻破的情况下,即使黑客对许多区块进行了双签,他们也只会被处罚一次。由于解除监禁必须使用验证者的操作员密钥完成,他们有机会重新保护自己的共识密钥,然后再通过操作员密钥表明自己已准备就绪。我们将这段只跟踪最大违规的时期称为“惩罚期”。 一旦验证者通过自行解除监禁重新加入,我们就开始一个新的惩罚期;如果他们在解除监禁后又提交了新的违规,该违规会在上一个惩罚期中最严重违规的基础上继续累积惩罚。 不过,虽然违规是按惩罚期分组的,但由于在违规发生后的 unbondingPeriod 内都仍然可以提交证据,我们仍然必须允许为先前的惩罚期提交证据。例如,若事件顺序如下:
  1. 验证者 A 提交违规 1(应惩罚 30%)
  2. 验证者 A 提交违规 2(应惩罚 40%)
  3. 违规 1 的证据到达状态机(并且验证者 A 被监禁)
  4. 验证者 A 解除监禁
此时我们已经进入一个新的惩罚期,但仍然必须为之前的违规保留处理空间,因为违规 2 的证据仍可能到来。随着惩罚期数量增加,复杂度也会提高,因为我们必须跟踪每一个惩罚期中的最高违规额度。
注意:当前根据 slashing 模块规范,每当验证者先解绑再重新绑定时,都会创建一个新的惩罚期。这可能应当改为在 jailed/unjailed 时创建。详见 issue #3205。在下文中,我将假设只有当验证者解除监禁时才会开始新的惩罚期。
惩罚期的最大数量为 len(UnbondingPeriod) / len(JailPeriod)。Gaia 当前对 UnbondingPeriod 和 JailPeriod 的默认值分别是 3 周和 2 天。这意味着每个验证者理论上最多可能同时跟踪 11 个惩罚期。如果我们设置 JailPeriod >= UnbondingPeriod,则只需跟踪 1 个惩罚期(也就是无需跟踪惩罚期)。 当前在监禁期实现中,一旦验证者解除监禁,所有仍委托给他们的委托人(尚未解绑或未重新委托出去)都会继续留在该验证者名下。考虑到共识安全故障极其严重(远比活性故障严重),让委托人自动重新绑定到该验证者可能并不稳妥。

提案:无限期监禁

我们提议将提交共识安全故障的验证者的“监禁时间”设置为 infinite(即墓碑状态)。这实质上会将该验证者踢出验证者集合,并且不允许其重新进入。其所有委托人(包括操作员本人)都必须选择解绑或重新委托出去。验证者操作员如果愿意,可以使用新的操作员密钥和共识密钥创建一个新的验证者,但他们必须重新“赢回”这些委托。 实现墓碑系统并去除惩罚期跟踪后,slashing 模块会变得简单得多,尤其是因为我们可以移除 slashing 模块中由 staking 模块消费的所有钩子(slashing 模块仍会消费 staking 中定义的钩子)。

单一惩罚额度

另一个可做的优化是:如果我们假设 CometBFT 共识中的所有 ABCI 故障都按同一等级进行惩罚,那么就不必再跟踪“最大惩罚”。一旦发生某个 ABCI 故障,我们就无需再担心将来可能出现的其他故障并拿它们做比较以找出最大值。 当前唯一的 CometBFT ABCI 故障是:
  • 无正当理由的预提交(双签)
目前计划在不久的将来加入以下故障:
  • 在处于解绑阶段时仍对预提交进行签名(这是为了保证轻客户端二分安全所必需的)
考虑到这两类故障都属于可归责的拜占庭故障,我们很可能会希望对它们施加相同级别的惩罚,因此可以采用上述变更。
注意:这一变更对于当前的 CometBFT 共识可能有意义,但对于其他共识算法或未来版本的 CometBFT 可能未必适用,因为它们可能希望采用不同等级的惩罚(例如部分惩罚)。

参数

slashing 模块包含以下参数:
键类型示例
SignedBlocksWindowstring (int64)“100”
MinSignedPerWindowstring (dec)“0.500000000000000000”
DowntimeJailDurationstring (ns)“600000000000”
SlashFractionDoubleSignstring (dec)“0.050000000000000000”
SlashFractionDowntimestring (dec)“0.010000000000000000”

CLI

用户可以使用 CLI 查询并与 slashing 模块交互。

查询

query 命令允许用户查询 slashing 状态。
simd query slashing --help

params

params 命令允许用户查询 slashing 模块的创世参数。
simd query slashing params [flags]
示例:
simd query slashing params
示例输出:
downtime_jail_duration: 600s
min_signed_per_window: "0.500000000000000000"
signed_blocks_window: "100"
slash_fraction_double_sign: "0.050000000000000000"
slash_fraction_downtime: "0.010000000000000000"

signing-info

signing-info 命令允许用户使用共识公钥查询验证者的 signing-info。
simd query slashing signing-infos [flags]
示例:
simd query slashing signing-info '{"@type":"/cosmos.crypto.ed25519.PubKey","key":"Auxs3865HpB/EfssYOzfqNhEJjzys6jD5B6tPgC8="}'

示例输出:
address: cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c
index_offset: "2068"
jailed_until: "1970-01-01T00:00:00Z"
missed_blocks_counter: "0"
start_height: "0"
tombstoned: false

signing-infos

signing-infos 命令允许用户查询所有验证者的 signing infos。
simd query slashing signing-infos [flags]
示例:
simd query slashing signing-infos
示例输出:
info:
- address: cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c
  index_offset: "2075"
  jailed_until: "1970-01-01T00:00:00Z"
  missed_blocks_counter: "0"
  start_height: "0"
  tombstoned: false
pagination:
  next_key: null
  total: "0"

交易

tx 命令允许用户与 slashing 模块交互。
simd tx slashing --help

unjail

unjail 命令允许用户将此前因宕机而被监禁的验证者解除监禁。
simd tx slashing unjail --from mykey [flags]
示例:
simd tx slashing unjail --from mykey

gRPC

用户可以使用 gRPC 端点查询 slashing 模块。

Params

Params 端点允许用户查询 slashing 模块的参数。
cosmos.slashing.v1beta1.Query/Params
示例:
grpcurl -plaintext localhost:9090 cosmos.slashing.v1beta1.Query/Params
示例输出:
{
  "params": {
    "signedBlocksWindow": "100",
    "minSignedPerWindow": "NTAwMDAwMDAwMDAwMDAwMDAw",
    "downtimeJailDuration": "600s",
    "slashFractionDoubleSign": "NTAwMDAwMDAwMDAwMDAwMDA=",
    "slashFractionDowntime": "MTAwMDAwMDAwMDAwMDAwMDA="
  }
}

SigningInfo

SigningInfo 用于查询给定共识地址的签名信息。
cosmos.slashing.v1beta1.Query/SigningInfo
示例:
grpcurl -plaintext -d '{"cons_address":"cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c"}' localhost:9090 cosmos.slashing.v1beta1.Query/SigningInfo
示例输出:
{
  "valSigningInfo": {
    "address": "cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c",
    "indexOffset": "3493",
    "jailedUntil": "1970-01-01T00:00:00Z"
  }
}

SigningInfos

SigningInfos 用于查询所有验证者的签名信息。
cosmos.slashing.v1beta1.Query/SigningInfos
示例:
grpcurl -plaintext localhost:9090 cosmos.slashing.v1beta1.Query/SigningInfos
示例输出:
{
  "info": [
    {
      "address": "cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c",
      "indexOffset": "2467",
      "jailedUntil": "1970-01-01T00:00:00Z"
    }
  ],
  "pagination": {
    "total": "1"
  }
}

REST

用户可以使用 REST 端点查询 slashing 模块。

Params

/cosmos/slashing/v1beta1/params
示例:
curl "localhost:1317/cosmos/slashing/v1beta1/params"
示例输出:
{
  "params": {
  "signed_blocks_window": "100",
  "min_signed_per_window": "0.500000000000000000",
  "downtime_jail_duration": "600s",
  "slash_fraction_double_sign": "0.050000000000000000",
  "slash_fraction_downtime": "0.010000000000000000"
}

signing_info

/cosmos/slashing/v1beta1/signing_infos/%s
示例:
curl "localhost:1317/cosmos/slashing/v1beta1/signing_infos/cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c"
示例输出:
{
  "val_signing_info": {
    "address": "cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c",
    "start_height": "0",
    "index_offset": "4184",
    "jailed_until": "1970-01-01T00:00:00Z",
    "tombstoned": false,
    "missed_blocks_counter": "0"
  }
}

signing_infos

/cosmos/slashing/v1beta1/signing_infos
示例:
curl "localhost:1317/cosmos/slashing/v1beta1/signing_infos
示例输出:
{
  "info": [
    {
      "address": "cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c",
      "start_height": "0",
      "index_offset": "4169",
      "jailed_until": "1970-01-01T00:00:00Z",
      "tombstoned": false,
      "missed_blocks_counter": "0"
    }
  ],
  "pagination": {
    "next_key": null,
    "total": "1"
  }
}

Abstract

This section specifies the slashing module of the Cosmos SDK, which implements functionality first outlined in the Cosmos Whitepaper in June 2016. The slashing module enables Cosmos SDK-based blockchains to disincentivize any attributable action by a protocol-recognized actor with value at stake by penalizing them (“slashing”). Penalties may include, but are not limited to:
  • Burning some amount of their stake
  • Removing their ability to vote on future blocks for a period of time.
This module will be used by the Cosmos Hub, the first hub in the Cosmos ecosystem.

Contents

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 pertaining 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 Cosmos Hub 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 tombstoned). 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 CometBFT 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.

State

Signing Info (Liveness)

Every block includes a set of precommits by the validators for the previous block, known as the LastCommitInfo provided by CometBFT. A LastCommitInfo is valid so long as it contains precommits from +2/3 of total voting power. Proposers are incentivized to include precommits from all validators in the CometBFT LastCommitInfo by receiving additional fees proportional to the difference between the voting power included in the LastCommitInfo and +2/3 (see fee distribution).
type LastCommitInfo struct {
    Round int32
	Votes []VoteInfo
}
Validators are penalized for failing to be included in the LastCommitInfo for some number of blocks by being automatically jailed, potentially slashed, and unbonded. Information about validator’s liveness activity is tracked through ValidatorSigningInfo. It is indexed in the store as follows:
  • ValidatorSigningInfo: 0x01 | ConsAddrLen (1 byte) | ConsAddress -> ProtocolBuffer(ValSigningInfo)
  • MissedBlocksBitArray: 0x02 | ConsAddrLen (1 byte) | ConsAddress | LittleEndianUint64(signArrayIndex) -> VarInt(didMiss) (varint is a number encoding format)
The first mapping allows us to easily lookup the recent signing info for a validator based on the validator’s consensus address. The second mapping (MissedBlocksBitArray) acts as a bit-array of size SignedBlocksWindow that tells us if the validator missed the block for a given index in the bit-array. The index in the bit-array is given as little endian uint64. The result is a varint that takes on 0 or 1, where 0 indicates the validator did not miss (did sign) the corresponding block, and 1 indicates they missed the block (did not sign). Note that the MissedBlocksBitArray is not explicitly initialized up-front. Keys are added as we progress through the first SignedBlocksWindow blocks for a newly bonded validator. The SignedBlocksWindow parameter defines the size (number of blocks) of the sliding window used to track validator liveness. The information stored for tracking validator liveness is as follows:
// Reference: https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/slashing/v1beta1/slashing.proto#L13-L35

Params

The slashing module stores it’s params in state with the prefix of 0x00, it can be updated with governance or the address with authority.
  • Params: 0x00 | ProtocolBuffer(Params)
// Reference: https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/slashing/v1beta1/slashing.proto#L37-L59

Messages

In this section we describe the processing of messages for the slashing module.

Unjail

If a validator was automatically unbonded due to downtime and wishes to come back online & possibly rejoin the bonded set, it must send MsgUnjail:
// MsgUnjail is an sdk.Msg used for unjailing a jailed validator, thus returning
// them into the bonded validator set, so they can begin receiving provisions
// and rewards again.
message MsgUnjail {
  string validator_addr = 1;
}
Below is a pseudocode of the MsgSrv/Unjail RPC:
unjail(tx MsgUnjail)

validator = getValidator(tx.ValidatorAddr)
    if validator == nil
      fail with "No validator found"
    if getSelfDelegation(validator) == 0
      fail with "validator must self delegate before unjailing"
    if !validator.Jailed
      fail with "Validator not jailed, cannot unjail"

    info = GetValidatorSigningInfo(operator)
    if info.Tombstoned
      fail with "Tombstoned validator cannot be unjailed"
    if block time < info.JailedUntil
      fail with "Validator still jailed, cannot unjail until period has expired"

    validator.Jailed = false
    setValidator(validator)

return
If the validator has enough stake to be in the top n = MaximumBondedValidators, it will be automatically rebonded, and all delegators still delegated to the validator will be rebonded and begin to again collect provisions and rewards.

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 tombstoning.
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

    SlashWithInfractionReason(vote.Validator.Address, distributionHeight, vote.Validator.Power, SlashFractionDowntime(), stakingtypes.Downtime)

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. If the validator was out of the validator set and gets bonded again, its new bonded height is set.
onValidatorBonded(address sdk.ValAddress)

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

else {
    signingInfo.StartHeight = CurrentHeight
}

setValidatorSigningInfo(signingInfo)
}

return

Events

The slashing module emits the following events:

MsgServer

MsgUnjail

TypeAttribute KeyAttribute Value
messagemoduleslashing
messagesender{validatorAddress}

Keeper

BeginBlocker: HandleValidatorSignature

TypeAttribute KeyAttribute Value
slashaddress{validatorConsensusAddress}
slashpower{validatorPower}
slashreason{slashReason}
slashjailed [0]{validatorConsensusAddress}
slashburned coins{math.Int}
  • [0] Only included if the validator is jailed.
TypeAttribute KeyAttribute Value
livenessaddress{validatorConsensusAddress}
livenessmissed_blocks{missedBlocksCounter}
livenessheight{blockHeight}

Slash

  • same as "slash" event from HandleValidatorSignature, but without the jailed attribute.

Jail

TypeAttribute KeyAttribute Value
slashjailed{validatorAddress}

Staking Tombstone

Abstract

In the current implementation of the slashing module, when the consensus engine informs the state machine of a validator’s consensus fault, the validator is partially slashed, and put into a “jail period”, a period of time in which they are not allowed to rejoin the validator set. However, because of the nature of consensus faults and ABCI, there can be a delay between an infraction occurring, and evidence of the infraction reaching the state machine (this is one of the primary reasons for the existence of the unbonding period).
Note: The tombstone concept, only applies to faults that have a delay between the infraction occurring and evidence reaching the state machine. For example, evidence of a validator double signing may take a while to reach the state machine due to unpredictable evidence gossip layer delays and the ability of validators to selectively reveal double-signatures (e.g. to infrequently-online light clients). Liveness slashing, on the other hand, is detected immediately as soon as the infraction occurs, and therefore no slashing period is needed. A validator is immediately put into jail period, and they cannot commit another liveness fault until they unjail. In the future, there may be other types of byzantine faults that have delays (for example, submitting evidence of an invalid proposal as a transaction). When implemented, it will have to be decided whether these future types of byzantine faults will result in a tombstoning (and if not, the slash amounts will not be capped by a slashing period).
In the current system design, once a validator is put in the jail for a consensus fault, after the JailPeriod they are allowed to send a transaction to unjail themselves, and thus rejoin the validator set. One of the “design desires” of the slashing module is that if multiple infractions occur before evidence is executed (and a validator is put in jail), they should only be punished for single worst infraction, but not cumulatively. For example, if the sequence of events is:
  1. Validator A commits Infraction 1 (worth 30% slash)
  2. Validator A commits Infraction 2 (worth 40% slash)
  3. Validator A commits Infraction 3 (worth 35% slash)
  4. Evidence for Infraction 1 reaches state machine (and validator is put in jail)
  5. Evidence for Infraction 2 reaches state machine
  6. Evidence for Infraction 3 reaches state machine
Only Infraction 2 should have its slash take effect, as it is the highest. This is done, so that in the case of the compromise of a validator’s consensus key, they will only be punished once, even if the hacker double-signs many blocks. Because, the unjailing has to be done with the validator’s operator key, they have a chance to re-secure their consensus key, and then signal that they are ready using their operator key. We call this period during which we track only the max infraction, the “slashing period”. Once, a validator rejoins by unjailing themselves, we begin a new slashing period; if they commit a new infraction after unjailing, it gets slashed cumulatively on top of the worst infraction from the previous slashing period. However, while infractions are grouped based off of the slashing periods, because evidence can be submitted up to an unbondingPeriod after the infraction, we still have to allow for evidence to be submitted for previous slashing periods. For example, if the sequence of events is:
  1. Validator A commits Infraction 1 (worth 30% slash)
  2. Validator A commits Infraction 2 (worth 40% slash)
  3. Evidence for Infraction 1 reaches state machine (and Validator A is put in jail)
  4. Validator A unjails
We are now in a new slashing period, however we still have to keep the door open for the previous infraction, as the evidence for Infraction 2 may still come in. As the number of slashing periods increase, it creates more complexity as we have to keep track of the highest infraction amount for every single slashing period.
Note: Currently, according to the slashing module spec, a new slashing period is created every time a validator is unbonded then rebonded. This should probably be changed to jailed/unjailed. See issue #3205 for further details. For the remainder of this, I will assume that we only start a new slashing period when a validator gets unjailed.
The maximum number of slashing periods is the len(UnbondingPeriod) / len(JailPeriod). The current defaults in Gaia for the UnbondingPeriod and JailPeriod are 3 weeks and 2 days, respectively. This means there could potentially be up to 11 slashing periods concurrently being tracked per validator. If we set the JailPeriod >= UnbondingPeriod, we only have to track 1 slashing period (i.e not have to track slashing periods). Currently, in the jail period implementation, once a validator unjails, all of their delegators who are delegated to them (haven’t unbonded / redelegated away), stay with them. Given that consensus safety faults are so egregious (way more so than liveness faults), it is probably prudent to have delegators not “auto-rebond” to the validator.

Proposal: infinite jail

We propose setting the “jail time” for a validator who commits a consensus safety fault, to infinite (i.e. a tombstone state). This essentially kicks the validator out of the validator set and does not allow them to re-enter the validator set. All of their delegators (including the operator themselves) have to either unbond or redelegate away. The validator operator can create a new validator if they would like, with a new operator key and consensus key, but they have to “re-earn” their delegations back. Implementing the tombstone system and getting rid of the slashing period tracking will make the slashing module way simpler, especially because we can remove all of the hooks defined in the slashing module consumed by the staking module (the slashing module still consumes hooks defined in staking).

Single slashing amount

Another optimization that can be made is that if we assume that all ABCI faults for CometBFT consensus are slashed at the same level, we don’t have to keep track of “max slash”. Once an ABCI fault happens, we don’t have to worry about comparing potential future ones to find the max. Currently the only CometBFT ABCI fault is:
  • Unjustified precommits (double signs)
It is currently planned to include the following fault in the near future:
  • Signing a precommit when you’re in unbonding phase (needed to make light client bisection safe)
Given that these faults are both attributable byzantine faults, we will likely want to slash them equally, and thus we can enact the above change.
Note: This change may make sense for current CometBFT consensus, but maybe not for a different consensus algorithm or future versions of CometBFT that may want to punish at different levels (for example, partial slashing).

Parameters

The slashing module contains the following parameters:
KeyTypeExample
SignedBlocksWindowstring (int64)“100”
MinSignedPerWindowstring (dec)“0.500000000000000000”
DowntimeJailDurationstring (ns)“600000000000”
SlashFractionDoubleSignstring (dec)“0.050000000000000000”
SlashFractionDowntimestring (dec)“0.010000000000000000”

CLI

A user can query and interact with the slashing module using the CLI.

Query

The query commands allow users to query slashing state.
simd query slashing --help

params

The params command allows users to query genesis parameters for the slashing module.
simd query slashing params [flags]
Example:
simd query slashing params
Example Output:
downtime_jail_duration: 600s
min_signed_per_window: "0.500000000000000000"
signed_blocks_window: "100"
slash_fraction_double_sign: "0.050000000000000000"
slash_fraction_downtime: "0.010000000000000000"

signing-info

The signing-info command allows users to query signing-info of the validator using consensus public key.
simd query slashing signing-infos [flags]
Example:
simd query slashing signing-info '{"@type":"/cosmos.crypto.ed25519.PubKey","key":"Auxs3865HpB/EfssYOzfqNhEJjzys6jD5B6tPgC8="}'

Example Output:
address: cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c
index_offset: "2068"
jailed_until: "1970-01-01T00:00:00Z"
missed_blocks_counter: "0"
start_height: "0"
tombstoned: false

signing-infos

The signing-infos command allows users to query signing infos of all validators.
simd query slashing signing-infos [flags]
Example:
simd query slashing signing-infos
Example Output:
info:
- address: cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c
  index_offset: "2075"
  jailed_until: "1970-01-01T00:00:00Z"
  missed_blocks_counter: "0"
  start_height: "0"
  tombstoned: false
pagination:
  next_key: null
  total: "0"

Transactions

The tx commands allow users to interact with the slashing module.
simd tx slashing --help

unjail

The unjail command allows users to unjail a validator previously jailed for downtime.
simd tx slashing unjail --from mykey [flags]
Example:
simd tx slashing unjail --from mykey

gRPC

A user can query the slashing module using gRPC endpoints.

Params

The Params endpoint allows users to query the parameters of slashing module.
cosmos.slashing.v1beta1.Query/Params
Example:
grpcurl -plaintext localhost:9090 cosmos.slashing.v1beta1.Query/Params
Example Output:
{
  "params": {
    "signedBlocksWindow": "100",
    "minSignedPerWindow": "NTAwMDAwMDAwMDAwMDAwMDAw",
    "downtimeJailDuration": "600s",
    "slashFractionDoubleSign": "NTAwMDAwMDAwMDAwMDAwMDA=",
    "slashFractionDowntime": "MTAwMDAwMDAwMDAwMDAwMDA="
  }
}

SigningInfo

The SigningInfo queries the signing info of given cons address.
cosmos.slashing.v1beta1.Query/SigningInfo
Example:
grpcurl -plaintext -d '{"cons_address":"cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c"}' localhost:9090 cosmos.slashing.v1beta1.Query/SigningInfo
Example Output:
{
  "valSigningInfo": {
    "address": "cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c",
    "indexOffset": "3493",
    "jailedUntil": "1970-01-01T00:00:00Z"
  }
}

SigningInfos

The SigningInfos queries signing info of all validators.
cosmos.slashing.v1beta1.Query/SigningInfos
Example:
grpcurl -plaintext localhost:9090 cosmos.slashing.v1beta1.Query/SigningInfos
Example Output:
{
  "info": [
    {
      "address": "cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c",
      "indexOffset": "2467",
      "jailedUntil": "1970-01-01T00:00:00Z"
    }
  ],
  "pagination": {
    "total": "1"
  }
}

REST

A user can query the slashing module using REST endpoints.

Params

/cosmos/slashing/v1beta1/params
Example:
curl "localhost:1317/cosmos/slashing/v1beta1/params"
Example Output:
{
  "params": {
  "signed_blocks_window": "100",
  "min_signed_per_window": "0.500000000000000000",
  "downtime_jail_duration": "600s",
  "slash_fraction_double_sign": "0.050000000000000000",
  "slash_fraction_downtime": "0.010000000000000000"
}

signing_info

/cosmos/slashing/v1beta1/signing_infos/%s
Example:
curl "localhost:1317/cosmos/slashing/v1beta1/signing_infos/cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c"
Example Output:
{
  "val_signing_info": {
    "address": "cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c",
    "start_height": "0",
    "index_offset": "4184",
    "jailed_until": "1970-01-01T00:00:00Z",
    "tombstoned": false,
    "missed_blocks_counter": "0"
  }
}

signing_infos

/cosmos/slashing/v1beta1/signing_infos
Example:
curl "localhost:1317/cosmos/slashing/v1beta1/signing_infos
Example Output:
{
  "info": [
    {
      "address": "cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c",
      "start_height": "0",
      "index_offset": "4169",
      "jailed_until": "1970-01-01T00:00:00Z",
      "tombstoned": false,
      "missed_blocks_counter": "0"
    }
  ],
  "pagination": {
    "next_key": null,
    "total": "1"
  }
}