diff --git a/PENDING.md b/PENDING.md
index 0d2ebc83c..01bdc2158 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 000000000..dee91eb33
--- /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 3eec27372..375e19185 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 000000000..84be139e8
--- /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 000000000..36dde61f9
--- /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 000000000..aa42f6193
--- /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 8bbb22c76..ae426db7b 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 cdf495e4d..be33ee096 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
-```golang
-type TxProveLive struct {
- PubKey crypto.PubKey
+If a validator was automatically unbonded due to downtime and wishes to come back online &
+possibly rejoin the bonded set, it must send `TxUnjail`:
+
+```
+type TxUnjail struct {
+ ValidatorAddr sdk.AccAddress
}
+
+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
```
-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.
-
-```
-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.