diff --git a/PENDING.md b/PENDING.md index 0d2ebc83cf22..01bdc2158bed 100644 --- a/PENDING.md +++ b/PENDING.md @@ -16,8 +16,9 @@ BREAKING CHANGES * Gaia * Make the transient store key use a distinct store key. [#2013](https://github.com/cosmos/cosmos-sdk/pull/2013) * [x/stake] \#1901 Validator type's Owner field renamed to Operator; Validator's GetOwner() renamed accordingly to comply with the SDK's Validator interface. + * [docs] [#2001](https://github.com/cosmos/cosmos-sdk/pull/2001) Update slashing spec for slashing period * [x/stake, x/slashing] [#1305](https://github.com/cosmos/cosmos-sdk/issues/1305) - Rename "revoked" to "jailed" - + * SDK * [core] \#1807 Switch from use of rational to decimal * [types] \#1901 Validator interface's GetOwner() renamed to GetOperator() diff --git a/docs/spec/slashing/README.md b/docs/spec/slashing/README.md new file mode 100644 index 000000000000..dee91eb33078 --- /dev/null +++ b/docs/spec/slashing/README.md @@ -0,0 +1,33 @@ +# Slashing module specification + +## Abstract + +This section specifies the slashing module of the Cosmos SDK, which implements functionality +first outlined in the [Cosmos Whitepaper](https://cosmos.network/about/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 + +1. **[Overview](overview.md)** +1. **[State](state.md)** + 1. [SigningInfo](state.md#signing-info) + 1. [SlashingPeriod](state.md#slashing-period) +1. **[Transactions](transactions.md)** + 1. [Unjail](transactions.md#unjail) +1. **[Hooks](hooks.md)** + 1. [Validator Bonded](hooks.md#validator-bonded) + 1. [Validator Unbonded](hooks.md#validator-unbonded) + 1. [Validator Slashed](hooks.md#validator-slashed) +1. **[Begin Block](begin-block.md)** + 1. [Evidence handling](begin-block.md#evidence-handling) + 1. [Uptime tracking](begin-block.md#uptime-tracking) +1. **[Future Improvements](future-improvements.md)** + 1. [State cleanup](future-improvements.md#state-cleanup) diff --git a/docs/spec/slashing/end_block.md b/docs/spec/slashing/begin-block.md similarity index 86% rename from docs/spec/slashing/end_block.md rename to docs/spec/slashing/begin-block.md index 3eec27372c2a..375e191859a4 100644 --- a/docs/spec/slashing/end_block.md +++ b/docs/spec/slashing/begin-block.md @@ -1,12 +1,13 @@ -# End-Block +# Begin-Block -## Slashing +## Evidence handling Tendermint blocks can include [Evidence](https://github.com/tendermint/tendermint/blob/develop/docs/spec/blockchain/blockchain.md#evidence), which indicates that a validator -committed malicious behaviour. The relevant information is forwarded to the +committed malicious behavior. The relevant information is forwarded to the application as [ABCI -Evidence](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto#L259), so the validator an be accordingly punished. +Evidence](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto#L259) in `abci.RequestBeginBlock` +so that the validator an be accordingly punished. For some `evidence` to be valid, it must satisfy: @@ -75,7 +76,9 @@ 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. -## Automatic Unbonding +The amount slashed for all double signature infractions committed within a single slashing period is capped as described in [state-machine.md](state-machine.md). + +## Uptime tracking At the beginning of each block, we update the signing info for each validator and check if they should be automatically unbonded: @@ -113,3 +116,5 @@ for val in block.Validators: SigningInfo.Set(val.Address, signInfo) ``` + +The amount slashed for downtime slashes is *not* capped by the slashing period in which they are committed, although they do reset it (since the validator is unbonded). diff --git a/docs/spec/slashing/future-improvements.md b/docs/spec/slashing/future-improvements.md new file mode 100644 index 000000000000..84be139e8cf9 --- /dev/null +++ b/docs/spec/slashing/future-improvements.md @@ -0,0 +1,4 @@ +## State Cleanup + +Once no evidence for a given slashing period can possibly be valid (the end time plus the unbonding period is less than the current time), +old slashing periods should be cleaned up. This will be implemented post-launch. diff --git a/docs/spec/slashing/hooks.md b/docs/spec/slashing/hooks.md new file mode 100644 index 000000000000..36dde61f940c --- /dev/null +++ b/docs/spec/slashing/hooks.md @@ -0,0 +1,58 @@ +## Hooks + +In this section we describe the "hooks" - slashing module code that runs when other events happen. + +### Validator Bonded + +Upon successful bonding of a validator (a given validator entering the "bonded" state, +which may happen on delegation, on unjailing, etc), we create a new `SlashingPeriod` structure for the +now-bonded validator, which `StartHeight` of the current block, `EndHeight` of `0` (sentinel value for not-yet-ended), +and `SlashedSoFar` of `0`: + +``` +onValidatorBonded(address sdk.ValAddress) + + slashingPeriod = SlashingPeriod{ + ValidatorAddr : address, + StartHeight : CurrentHeight, + EndHeight : 0, + SlashedSoFar : 0, + } + setSlashingPeriod(slashingPeriod) + + return +``` + +### Validator Unbonded + +When a validator is unbonded, we update the in-progress `SlashingPeriod` with the current block as the `EndHeight`: + +``` +onValidatorUnbonded(address sdk.ValAddress) + + slashingPeriod = getSlashingPeriod(address, CurrentHeight) + slashingPeriod.EndHeight = CurrentHeight + setSlashingPeriod(slashingPeriod) + + return +``` + +### Validator Slashed + +When a validator is slashed, we look up the appropriate `SlashingPeriod` based on the validator +address and the time of infraction, cap the fraction slashed as `max(SlashFraction, SlashedSoFar)` +(which may be `0`), and update the `SlashingPeriod` with the increased `SlashedSoFar`: + +``` +beforeValidatorSlashed(address sdk.ValAddress, fraction sdk.Rat, infractionHeight int64) + + slashingPeriod = getSlashingPeriod(address, infractionHeight) + totalToSlash = max(slashingPeriod.SlashedSoFar, fraction) + slashingPeriod.SlashedSoFar = totalToSlash + setSlashingPeriod(slashingPeriod) + + remainderToSlash = slashingPeriod.SlashedSoFar - totalToSlash + fraction = remainderToSlash + + continue with slashing +``` diff --git a/docs/spec/slashing/overview.md b/docs/spec/slashing/overview.md new file mode 100644 index 000000000000..aa42f6193985 --- /dev/null +++ b/docs/spec/slashing/overview.md @@ -0,0 +1,68 @@ +## Conceptual overview + +### States + +At any given time, there are any number of validators registered in the state machine. +Each block, the top `n = MaximumBondedValidators` 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. + +### Slashing period + +In order to mitigate the impact of initially likely categories of non-malicious protocol faults, the Cosmos Hub implements for each validator +a *slashing period*, in which the amount by which a validator can be slashed is capped at the punishment for the worst violation. 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 jailed, +so that you have a chance to reconfigure your setup). This will still be quite expensive and desirable to avoid, but slashing periods somewhat blunt +the economic impact of unintentional misconfiguration. + +Unlike the unbonding period, the slashing period doesn't have a fixed length. A new slashing period starts whenever a validator is bonded and ends +whenever the validator is unbonded (which will happen if the validator is jailed). The amount of tokens slashed relative to validator power for infractions +committed within the slashing period, whenever they are discovered, is capped at the punishment for the worst infraction +(which for the Cosmos Hub at launch will be double-signing a block). + +#### ASCII timelines + +*Code* + +*[* : timeline start +*]* : timeline end +*<* : slashing period start +*>* : slashing period end +*Cn* : infraction `n` committed +*Dn* : infraction `n` discovered +*Vb* : validator bonded +*Vu* : validator unbonded + +*Single 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 infractions* + +<---------------------------> +[----------C1--C2---C3---D1,D2,D3Vu-----] + +Multiple infractions are committed within a single slashing period then later discovered, at which point the validator is unbonded and slashed for only the worst infraction. + +*Multiple infractions after rebonding* + + +<--------------------------->                        <-------------> +[----------C1--C2---C3---D1,D2,D3Vu---Vb---C4----D4,Vu--] + +Multiple infractions are committed within a single slashing period then later discovered, at which point the validator is unbonded and slashed for only the worst infraction. +The validator then unjails themself and rebonds, then commits a fourth infraction - which is discovered and punished at the full amount, since a new slashing period started +when they unjailed and rebonded. + +### Safety note + +Slashing is capped fractionally per period, but the amount of total bonded stake associated with any given validator can change (by an unbounded amount) over that period. + +For example, with MaxFractionSlashedPerPeriod = `0.5`, if a validator is initially slashed at `0.4` near the start of a period when they have 100 stake bonded, +then later slashed at `0.4` when they have `1000` stake bonded, the total amount slashed is just `40 + 100 = 140` (since the latter slash is capped at `0.1`) - +whereas if they had `1000` stake bonded initially, the first offense would have been slashed for `400` stake and the total amount slashed would have been `400 + 100 = 500`. + +This means that any slashing events which utilize the slashing period (are capped-per-period) **must also** jail the validator when the infraction is discovered. +Otherwise it would be possible for a validator to slash themselves intentionally at a low bond, then increase their bond but no longer be at stake since they would have already hit the `SlashedSoFar` cap. diff --git a/docs/spec/slashing/state.md b/docs/spec/slashing/state.md index 8bbb22c76a1b..ae426db7b392 100644 --- a/docs/spec/slashing/state.md +++ b/docs/spec/slashing/state.md @@ -1,6 +1,6 @@ -## State +# State -### Signing Info +## Signing Info 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. @@ -36,10 +36,11 @@ The information stored for tracking validator liveness is as follows: ```go type ValidatorSigningInfo struct { - StartHeight int64 - IndexOffset int64 - JailedUntil int64 - SignedBlocksCounter int64 + StartHeight int64 // Height at which the validator became able to sign blocks + IndexOffset int64 // Offset into the signed block bit array + JailedUntilHeight int64 // Block height until which the validator is jailed, + // or sentinel value of 0 for not jailed + SignedBlocksCounter int64 // Running counter of signed blocks } ``` @@ -49,3 +50,31 @@ Where: * `IndexOffset` is incremented each time the candidate was a bonded validator in a block (and may have signed a precommit or not). * `JailedUntil` is set whenever the candidate is jailed due to downtime * `SignedBlocksCounter` is a counter kept to avoid unnecessary array reads. `SignedBlocksBitArray.Sum() == SignedBlocksCounter` always. + +## Slashing Period + +A slashing period is a start and end block height associated with a particular validator, +within which only the "worst infraction counts" (see the [Overview](overview.md)): the total +amount of slashing for infractions committed within the period (and discovered whenever) is +capped at the penalty for the worst offense. + +This period starts when a validator is first bonded and ends when a validator is slashed & jailed +for any reason. When the validator rejoins the validator set (perhaps through unjailing themselves, +and perhaps also changing signing keys), they enter into a new period. + +Slashing periods are indexed in the store as follows: + +- SlashingPeriod: ` 0x03 | ValTendermintAddr | StartHeight -> amino(slashingPeriod) ` + +This allows us to look up slashing period by a validator's address, the only lookup necessary, +and iterate over start height to efficiently retrieve the most recent slashing period(s) +or those beginning after a given height. + +```go +type SlashingPeriod struct { + ValidatorAddr sdk.ValAddress // Tendermint address of the validator + StartHeight int64 // Block height at which slashing period begin + EndHeight int64 // Block height at which slashing period ended + SlashedSoFar sdk.Rat // Fraction slashed so far, cumulative +} +``` diff --git a/docs/spec/slashing/transactions.md b/docs/spec/slashing/transactions.md index cdf495e4d220..be33ee096539 100644 --- a/docs/spec/slashing/transactions.md +++ b/docs/spec/slashing/transactions.md @@ -1,19 +1,40 @@ +## Transactions -### TxProveLive +In this section we describe the processing of transactions for the `slashing` module. -If a validator was automatically unbonded due to liveness issues and wishes to -assert it is still online, it can send `TxProveLive`: +### Unjail + +If a validator was automatically unbonded due to downtime and wishes to come back online & +possibly rejoin the bonded set, it must send `TxUnjail`: -```golang -type TxProveLive struct { - PubKey crypto.PubKey -} ``` +type TxUnjail struct { + ValidatorAddr sdk.AccAddress +} -All delegators in the temporary unbonding pool which have not -transacted to move will be bonded back to the now-live validator and begin to -once again collect provisions and rewards. +handleMsgUnjail(tx TxUnjail) + validator = getValidator(tx.ValidatorAddr) + if validator == nil + fail with "No validator found" + + if !validator.Jailed + fail with "Validator not jailed, cannot unjail" + + info = getValidatorSigningInfo(operator) + if block time < info.JailedUntil + fail with "Validator still jailed, cannot unjail until period has expired" + + // Update the start height so the validator won't be immediately unbonded again + info.StartHeight = BlockHeight + setValidatorSigningInfo(info) + + validator.Jailed = false + setValidator(validator) + + return ``` -TODO: pseudo-code -``` + +If the validator has enough stake to be in the top `n = MaximumBondedValidators`, they will be automatically rebonded, +and all delegators still delegated to the validator will be rebonded and begin to again collect +provisions and rewards.