Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R4R: Tombstone Implementation #3225

Merged
merged 12 commits into from
Jan 11, 2019
Merged
13 changes: 0 additions & 13 deletions cmd/gaia/app/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,6 @@ func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) {

/* Handle slashing state. */

// remove all existing slashing periods and recreate one for each validator
app.slashingKeeper.DeleteValidatorSlashingPeriods(ctx)

for _, valConsAddr := range valConsAddrs {
sp := slashing.ValidatorSlashingPeriod{
ValidatorAddr: valConsAddr,
StartHeight: 0,
EndHeight: 0,
SlashedSoFar: sdk.ZeroDec(),
}
app.slashingKeeper.SetValidatorSlashingPeriod(ctx, sp)
}

// reset start height on signing infos
app.slashingKeeper.IterateValidatorSigningInfos(
ctx,
Expand Down
13 changes: 6 additions & 7 deletions cmd/gaia/app/sim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,12 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage {

slashingGenesis := slashing.GenesisState{
Params: slashing.Params{
MaxEvidenceAge: stakeGenesis.Params.UnbondingTime,
DoubleSignUnbondDuration: time.Duration(randIntBetween(r, 60, 60*60*24)) * time.Second,
SignedBlocksWindow: int64(randIntBetween(r, 10, 1000)),
DowntimeUnbondDuration: time.Duration(randIntBetween(r, 60, 60*60*24)) * time.Second,
MinSignedPerWindow: sdk.NewDecWithPrec(int64(r.Intn(10)), 1),
SlashFractionDoubleSign: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(50) + 1))),
SlashFractionDowntime: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(200) + 1))),
MaxEvidenceAge: stakeGenesis.Params.UnbondingTime,
SignedBlocksWindow: int64(randIntBetween(r, 10, 1000)),
DowntimeJailDuration: time.Duration(randIntBetween(r, 60, 60*60*24)) * time.Second,
MinSignedPerWindow: sdk.NewDecWithPrec(int64(r.Intn(10)), 1),
SlashFractionDoubleSign: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(50) + 1))),
SlashFractionDowntime: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(200) + 1))),
},
}
fmt.Printf("Selected randomly generated slashing parameters:\n\t%+v\n", slashingGenesis)
Expand Down
13 changes: 5 additions & 8 deletions docs/spec/slashing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,12 @@ This module will be used by the Cosmos Hub, the first hub in the Cosmos ecosyste
1. **[Overview](overview.md)**
1. **[State](state.md)**
1. [SigningInfo](state.md#signing-info)
1. [SlashingPeriod](state.md#slashing-period)
1. **[Transactions](transactions.md)**
2. **[Transactions](transactions.md)**
1. [Unjail](transactions.md#unjail)
1. **[Hooks](hooks.md)**
3. **[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)**
4. **[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)**
2. [Uptime tracking](begin-block.md#uptime-tracking)
5. **[Future Improvements](future-improvements.md)**
1. [State cleanup](future-improvements.md#state-cleanup)
18 changes: 10 additions & 8 deletions docs/spec/slashing/begin-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ for redel in redels {
}
```

We then slash the validator:
We then slash the validator and tombstone them:

```
curVal := validator
Expand All @@ -70,17 +70,21 @@ slashAmount -= slashAmountUnbondings
slashAmount -= slashAmountRedelegations

curVal.Shares = max(0, curVal.Shares - slashAmount)

signInfo = SigningInfo.Get(val.Address)
signInfo.JailedUntil = MAX_TIME
signInfo.Tombstoned = true
cwgoes marked this conversation as resolved.
Show resolved Hide resolved
SigningInfo.Set(val.Address, signInfo)
```

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.

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).
sunnya97 marked this conversation as resolved.
Show resolved Hide resolved
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.

## Uptime tracking

At the beginning of each block, we update the signing info for each validator and check if they should be automatically unbonded:
At the beginning of each block, we update the signing info for each validator and check if they've dipped below the liveness threshhold 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.

```
height := block.Height
Expand Down Expand Up @@ -114,9 +118,7 @@ for val in block.Validators:
signInfo.IndexOffset = 0
signInfo.MissedBlocksCounter = 0
clearMissedBlockBitArray()
slash & unbond the validator
slash & jail the validator

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).
sunnya97 marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 0 additions & 4 deletions docs/spec/slashing/future-improvements.md

This file was deleted.

49 changes: 3 additions & 46 deletions docs/spec/slashing/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ In this section we describe the "hooks" - slashing module code that runs when ot

### 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`:
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)
Expand All @@ -18,52 +16,11 @@ onValidatorBonded(address sdk.ValAddress)
StartHeight : CurrentHeight,
IndexOffset : 0,
JailedUntil : time.Unix(0, 0),
Tombstone : false,
MissedBloskCounter : 0
}
setValidatorSigningInfo(signingInfo)
}

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
```
44 changes: 9 additions & 35 deletions docs/spec/slashing/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,63 +6,37 @@ At any given time, there are any number of validators registered in the state ma
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
### 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 *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.
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.

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

sunnya97 marked this conversation as resolved.
Show resolved Hide resolved
#### ASCII timelines

*Code*

*[* : timeline start
*]* : timeline end
*<* : slashing period start
*>* : slashing period end
*C<sub>n</sub>* : infraction `n` committed
*D<sub>n</sub>* : infraction `n` discovered
*V<sub>b</sub>* : validator bonded
*V<sub>u</sub>* : validator unbonded

*Single infraction*
*Single Double Sign Infraction*

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

*Multiple 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>-----]

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*


<--------------------------->&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<------------->
[----------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>---V<sub>b</sub>---C<sub>4</sub>----D<sub>4</sub>,V<sub>u</sub>--]

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.
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.
30 changes: 2 additions & 28 deletions docs/spec/slashing/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type ValidatorSigningInfo struct {
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
Tombstoned bool // Whether a validator is tombstoned or not
MissedBlocksCounter int64 // Running counter of missed blocks
}

Expand All @@ -49,32 +50,5 @@ 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).
* `JailedUntil` is set whenever the candidate is jailed due to downtime
* `Tombstoned` is set once a validator's first double sign evidence comes in
* `MissedBlocksCounter` is a counter kept to avoid unnecessary array reads. `MissedBlocksBitArray.Sum() == MissedBlocksCounter` 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
}
```
2 changes: 2 additions & 0 deletions docs/spec/slashing/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ handleMsgUnjail(tx TxUnjail)
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"

Expand Down
42 changes: 12 additions & 30 deletions x/slashing/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import (

// GenesisState - all slashing state that must be provided at genesis
type GenesisState struct {
Params Params `json:"params"`
SigningInfos map[string]ValidatorSigningInfo `json:"signing_infos"`
MissedBlocks map[string][]MissedBlock `json:"missed_blocks"`
SlashingPeriods []ValidatorSlashingPeriod `json:"slashing_periods"`
Params Params `json:"params"`
SigningInfos map[string]ValidatorSigningInfo `json:"signing_infos"`
MissedBlocks map[string][]MissedBlock `json:"missed_blocks"`
}

// MissedBlock
Expand All @@ -25,10 +24,9 @@ type MissedBlock struct {
// HubDefaultGenesisState - default GenesisState used by Cosmos Hub
func DefaultGenesisState() GenesisState {
return GenesisState{
Params: DefaultParams(),
SigningInfos: make(map[string]ValidatorSigningInfo),
MissedBlocks: make(map[string][]MissedBlock),
SlashingPeriods: []ValidatorSlashingPeriod{},
Params: DefaultParams(),
SigningInfos: make(map[string]ValidatorSigningInfo),
MissedBlocks: make(map[string][]MissedBlock),
}
}

Expand All @@ -54,14 +52,9 @@ func ValidateGenesis(data GenesisState) error {
return fmt.Errorf("Max evidence age must be at least 1 minute, is %s", maxEvidence.String())
}

dblSignUnbond := data.Params.DoubleSignUnbondDuration
if dblSignUnbond < 1*time.Minute {
return fmt.Errorf("Double sign unblond duration must be at least 1 minute, is %s", dblSignUnbond.String())
}

downtimeUnbond := data.Params.DowntimeUnbondDuration
if downtimeUnbond < 1*time.Minute {
return fmt.Errorf("Downtime unblond duration must be at least 1 minute, is %s", downtimeUnbond.String())
downtimeJail := data.Params.DowntimeJailDuration
if downtimeJail < 1*time.Minute {
return fmt.Errorf("Downtime unblond duration must be at least 1 minute, is %s", downtimeJail.String())
}

signedWindow := data.Params.SignedBlocksWindow
Expand Down Expand Up @@ -97,10 +90,6 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState, sdata types.
}
}

for _, slashingPeriod := range data.SlashingPeriods {
keeper.SetValidatorSlashingPeriod(ctx, slashingPeriod)
}

keeper.paramspace.SetParamSet(ctx, &data.Params)
}

Expand All @@ -127,16 +116,9 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) (data GenesisState) {
return false
})

slashingPeriods := []ValidatorSlashingPeriod{}
keeper.IterateValidatorSlashingPeriods(ctx, func(slashingPeriod ValidatorSlashingPeriod) (stop bool) {
slashingPeriods = append(slashingPeriods, slashingPeriod)
return false
})

return GenesisState{
Params: params,
SigningInfos: signingInfos,
MissedBlocks: missedBlocks,
SlashingPeriods: slashingPeriods,
Params: params,
SigningInfos: signingInfos,
MissedBlocks: missedBlocks,
}
}
4 changes: 4 additions & 0 deletions x/slashing/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func handleMsgUnjail(ctx sdk.Context, msg MsgUnjail, k Keeper) sdk.Result {
return ErrNoValidatorForAddress(k.codespace).Result()
}

if info.Tombstoned {
cwgoes marked this conversation as resolved.
Show resolved Hide resolved
return ErrValidatorJailed(k.codespace).Result()
}

// cannot be unjailed until out of jail
if ctx.BlockHeader().Time.Before(info.JailedUntil) {
return ErrValidatorJailed(k.codespace).Result()
Expand Down
Loading