Merge PR #4892: Update Slashing Spec (Liveness)

This commit is contained in:
Alexander Bezobchuk 2019-08-12 12:02:46 -04:00 committed by GitHub
parent 0f4b938324
commit 436f440e8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 167 additions and 109 deletions

View File

@ -2,22 +2,33 @@
## States ## States
At any given time, there are any number of validators registered in the state machine. At any given time, there are any number of validators registered in the state
Each block, the top `n = MaximumBondedValidators` validators who are not jailed become *bonded*, meaning that they may propose and vote on blocks. machine. Each block, the top `MaxValidators` (defined by `x/staking`) validators
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. 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 ## Tombstone Caps
In order to mitigate the impact of initially likely categories of non-malicious protocol faults, the Cosmos Hub implements for each validator In order to mitigate the impact of initially likely categories of non-malicious
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 protocol faults, the Cosmos Hub implements for each validator
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 a *tombstone* cap, which only allows a validator to be slashed once for a double
to avoid, but tombstone caps somewhat blunt the economic impact of unintentional misconfiguration. 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. 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.
## ASCII timelines ## Infraction Timelines
*Code* To illustrate how the `x/slashing` module handles submitted evidence through
Tendermint consensus, consider the following examples:
__Definitions__:
*[* : timeline start *[* : timeline start
*]* : timeline end *]* : timeline end
@ -26,17 +37,19 @@ Liveness faults do not have caps, as they can't stack upon each other. Liveness
*V<sub>b</sub>* : validator bonded *V<sub>b</sub>* : validator bonded
*V<sub>u</sub>* : validator unbonded *V<sub>u</sub>* : validator unbonded
*Single Double Sign Infraction* ### Single Double Sign Infraction
<-----------------> <----------------->
[----------C<sub>1</sub>----D<sub>1</sub>,V<sub>u</sub>-----] [----------C<sub>1</sub>----D<sub>1</sub>,V<sub>u</sub>-----]
A single infraction is committed then later discovered, at which point the validator is unbonded and slashed at the full amount for the infraction. 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* ### Multiple Double Sign Infractions
<---------------------------> <--------------------------->
[----------C<sub>1</sub>--C<sub>2</sub>---C<sub>3</sub>---D<sub>1</sub>,D<sub>2</sub>,D<sub>3</sub>V<sub>u</sub>-----] [----------C<sub>1</sub>--C<sub>2</sub>---C<sub>3</sub>---D<sub>1</sub>,D<sub>2</sub>,D<sub>3</sub>V<sub>u</sub>-----]
Multiple infractions are committed and then later discovered, at which point the validator is jailed and slashed for only one infraction. Multiple infractions are committed and then later discovered, at which point the
Because the validator is also tombstoned, they can not rejoin the validator set. validator is jailed and slashed for only one infraction. Because the validator
is also tombstoned, they can not rejoin the validator set.

View File

@ -1,54 +1,62 @@
# State # State
## Signing Info ## Signing Info (Liveness)
Every block includes a set of precommits by the validators for the previous block, Every block includes a set of precommits by the validators for the previous block,
known as the LastCommit. A LastCommit is valid so long as it contains precommits from +2/3 of voting power. known as the `LastCommitInfo` provided by Tendermint. 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 Proposers are incentivized to include precommits from all validators in the `LastCommitInfo`
validators in the LastCommit by receiving additional fees by receiving additional fees proportional to the difference between the voting
proportional to the difference between the voting power included in the power included in the `LastCommitInfo` and +2/3 (see [TODO](https://github.com/cosmos/cosmos-sdk/issues/967)).
LastCommit and +2/3 (see [TODO](https://github.com/cosmos/cosmos-sdk/issues/967)).
Validators are penalized for failing to be included in the LastCommit for some Validators are penalized for failing to be included in the `LastCommitInfo` for some
number of blocks by being automatically unbonded. number of blocks by being automatically jailed, potentially slashed, and unbonded.
Information about validator activity is tracked in a `ValidatorSigningInfo`. Information about validator's liveness activity is tracked through `ValidatorSigningInfo`.
It is indexed in the store as follows: It is indexed in the store as follows:
- SigningInfo: ` 0x01 | ValTendermintAddr -> amino(valSigningInfo)` - ValidatorSigningInfo: ` 0x01 | ConsAddress -> amino(valSigningInfo)`
- MissedBlocksBitArray: ` 0x02 | ValTendermintAddr | LittleEndianUint64(signArrayIndex) -> VarInt(didMiss)` - MissedBlocksBitArray: ` 0x02 | ConsAddress | LittleEndianUint64(signArrayIndex) -> VarInt(didMiss)`
The first map allows us to easily lookup the recent signing info for a The first mapping allows us to easily lookup the recent signing info for a
validator, according to the Tendermint validator address. The second map acts as validator based on the validator's consensus address. The second mapping acts
a bit-array of size `SIGNED_BLOCKS_WINDOW` that tells us if the validator missed the block for a given index in the bit-array. 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
The index in the bit-array is given as little endian uint64. as little endian uint64.
The result is a `varint` that takes on `0` or `1`, where `0` indicates the 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). 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 Note that the `MissedBlocksBitArray` is not explicitly initialized up-front. Keys
added as we progress through the first `SIGNED_BLOCKS_WINDOW` blocks for a newly are added as we progress through the first `SignedBlocksWindow` blocks for a newly
bonded validator. 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: The information stored for tracking validator liveness is as follows:
```go ```go
type ValidatorSigningInfo struct { type ValidatorSigningInfo struct {
StartHeight int64 // Height at which the validator became able to sign blocks Address sdk.ConsAddress
IndexOffset int64 // Offset into the signed block bit array StartHeight int64
JailedUntilHeight int64 // Block height until which the validator is jailed, IndexOffset int64
// or sentinel value of 0 for not jailed JailedUntil time.Time
Tombstoned bool // Whether a validator is tombstoned or not Tombstoned bool
MissedBlocksCounter int64 // Running counter of missed blocks MissedBlocksCounter int64
} }
``` ```
Where: Where:
* `StartHeight` is set to the height that the candidate became an active validator (with non-zero voting power).
* `IndexOffset` is incremented each time the candidate was a bonded validator in a block (and may have signed a precommit or not). - __Address__: The validator's consensus address.
* `JailedUntil` is set whenever the candidate is jailed due to downtime - __StartHeight__: The height that the candidate became an active validator
* `Tombstoned` is set once a validator's first double sign evidence comes in (with non-zero voting power).
* `MissedBlocksCounter` is a counter kept to avoid unnecessary array reads. `MissedBlocksBitArray.Sum() == MissedBlocksCounter` always. - __IndexOffset__: Index which is incremented each time the validator was a bonded
in a block and may have signed a precommit or not. This in conjunction with the
`SignedBlocksWindow` param determines the index in the `MissedBlocksBitArray`.
- __JailedUntil__: Time for which the validator is jailed until due to liveness downtime.
- __Tombstoned__: Desribes if the validator is tombstoned or not. It is set once the
validator commits an equivocation or for any other configured misbehiavor.
- __MissedBlocksCounter__: A counter kept to avoid unnecessary array reads. Note
that `Sum(MissedBlocksBitArray)` equals `MissedBlocksCounter` always.

View File

@ -1,30 +1,29 @@
# Begin-Block # BeginBlock
## Evidence handling ## Evidence Handling
Tendermint blocks can include Tendermint blocks can include
[Evidence](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#evidence), which indicates that a validator [Evidence](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#evidence), which indicates that a validator committed malicious
committed malicious behavior. The relevant information is forwarded to the behavior. The relevant information is forwarded to the application as ABCI Evidence
application as [ABCI in `abci.RequestBeginBlock` so that the validator an be accordingly punished.
Evidence](https://github.com/tendermint/tendermint/blob/master/abci/types/types.pb.go#L3277:6) in `abci.RequestBeginBlock`
so that the validator an be accordingly punished.
For some `evidence` to be valid, it must satisfy: For some `Evidence` submitted in `block` to be valid, it must satisfy:
`evidence.Timestamp >= block.Timestamp - MAX_EVIDENCE_AGE` `Evidence.Timestamp >= block.Timestamp - MaxEvidenceAge`
where `evidence.Timestamp` is the timestamp in the block at height Where `Evidence.Timestamp` is the timestamp in the block at height
`evidence.Height` and `block.Timestamp` is the current block timestamp. `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 `SLASH_PROPORTION` of If valid evidence is included in a block, the validator's stake is reduced by
what their stake was when the infraction occurred (rather than when the evidence was discovered). some penalty (`SlashFractionDoubleSign` for equivocation) of what their stake was
We want to "follow the stake": the stake which contributed to the infraction should be when the infraction occurred (rather than when the evidence was discovered). We
slashed, even if it has since been redelegated or started unbonding. 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 We first need to loop through the unbondings and redelegations from the slashed
and track how much stake has since moved: validator and track how much stake has since moved:
``` ```go
slashAmountUnbondings := 0 slashAmountUnbondings := 0
slashAmountRedelegations := 0 slashAmountRedelegations := 0
@ -79,60 +78,98 @@ SigningInfo.Set(val.Address, signInfo)
This ensures that offending validators are punished the same amount whether they 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 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 stake. The amount slashed for all double signature infractions committed within a
single slashing period is capped as described in [overview.md](overview.md) under Tombstone Caps. single slashing period is capped as described in [overview.md](overview.md) under Tombstone Caps.
## Uptime tracking ## Liveness Tracking
At the beginning of each block, we update the signing info for each validator and check if they've dipped below the liveness threshold over the tracked window. If so, they will be slashed by `LivenessSlashAmount` and will be Jailed for `LivenessJailPeriod`. Liveness slashes do NOT lead to a tombstombing. 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.
If a validator misses a block, a warning event will get emitted. 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.
```go
height := block.Height height := block.Height
for val in block.Validators: for vote in block.LastCommitInfo.Votes {
signInfo = SigningInfo.Get(val.Address) signInfo := GetValidatorSigningInfo(vote.Validator.Address)
if signInfo == nil{
signInfo.StartHeight = height // 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
} }
index := signInfo.IndexOffset % SIGNED_BLOCKS_WINDOW if missed {
signInfo.IndexOffset++ // emit events...
previous = MissedBlockBitArray.Get(val.Address, index) }
// update counter if array has changed minHeight := signInfo.StartHeight + SignedBlocksWindow()
if !previous and val in block.AbsentValidators: maxMissed := SignedBlocksWindow() - MinSignedPerWindow()
MissedBlockBitArray.Set(val.Address, index, true)
signInfo.MissedBlocksCounter++
else if previous and val not in block.AbsentValidators:
MissedBlockBitArray.Set(val.Address, index, false)
signInfo.MissedBlocksCounter--
// else previous == val not in block.AbsentValidators, no change
// Emit warning events if Validator misses block // If we are past the minimum height and the validator has missed too many
if val in block.AbsentValidators { // jail and slash them.
ctx.EventManager().EmitEvent( if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
NewEvent( validator := ValidatorByConsAddr(vote.Validator.Address)
EventTypeLiveness,
NewAttribute(AttributeKeyAddress, consAddr),
NewAttribute(AttributeKeyMissedBlocks, signInfo.MissedBlocksCounter),
NewAttribute(AttributeKeyHeight, ctx.BlockHeight())
),
)
}
// validator must be active for at least SIGNED_BLOCKS_WINDOW // emit events...
// before they can be automatically unbonded for failing to be
// included in 50% of the recent LastCommits // We need to retrieve the stake distribution which signed the block, so we
minHeight = signInfo.StartHeight + SIGNED_BLOCKS_WINDOW // subtract ValidatorUpdateDelay from the block height, and subtract an
maxMissed = SIGNED_BLOCKS_WINDOW / 2 // additional 1 since this is the LastCommit.
if height > minHeight AND signInfo.MissedBlocksCounter > maxMissed: //
signInfo.JailedUntil = block.Time + DOWNTIME_UNBOND_DURATION // Note, that this CAN result in a negative "distributionHeight" up to
signInfo.IndexOffset = 0 // -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.MissedBlocksCounter = 0
clearMissedBlockBitArray() signInfo.IndexOffset = 0
slash & jail the validator ClearValidatorMissedBlockBitArray(vote.Validator.Address)
}
SigningInfo.Set(val.Address, signInfo) SetValidatorSigningInfo(vote.Validator.Address, signInfo)
}
``` ```