diff --git a/cmd/gaia/app/export.go b/cmd/gaia/app/export.go index ec2e83b2e720..f8968ceec643 100644 --- a/cmd/gaia/app/export.go +++ b/cmd/gaia/app/export.go @@ -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, diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index 483ce621e13d..14469a6ce774 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -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) diff --git a/docs/spec/slashing/README.md b/docs/spec/slashing/README.md index dee91eb33078..2240930f8e80 100644 --- a/docs/spec/slashing/README.md +++ b/docs/spec/slashing/README.md @@ -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) diff --git a/docs/spec/slashing/begin-block.md b/docs/spec/slashing/begin-block.md index 43691b38d211..4bc677ded534 100644 --- a/docs/spec/slashing/begin-block.md +++ b/docs/spec/slashing/begin-block.md @@ -59,7 +59,7 @@ for redel in redels { } ``` -We then slash the validator: +We then slash the validator and tombstone them: ``` curVal := validator @@ -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 +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). +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 @@ -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). diff --git a/docs/spec/slashing/future-improvements.md b/docs/spec/slashing/future-improvements.md deleted file mode 100644 index 84be139e8cf9..000000000000 --- a/docs/spec/slashing/future-improvements.md +++ /dev/null @@ -1,4 +0,0 @@ -## 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 index 1888c1d2f847..74207891fb84 100644 --- a/docs/spec/slashing/hooks.md +++ b/docs/spec/slashing/hooks.md @@ -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) @@ -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 -``` diff --git a/docs/spec/slashing/overview.md b/docs/spec/slashing/overview.md index aa42f6193985..63f0e494f856 100644 --- a/docs/spec/slashing/overview.md +++ b/docs/spec/slashing/overview.md @@ -6,18 +6,14 @@ 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. #### ASCII timelines @@ -25,44 +21,22 @@ committed within the slashing period, whenever they are discovered, is capped at *[* : 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* +*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 infractions* +*Multiple Double Sign 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. +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. \ No newline at end of file diff --git a/docs/spec/slashing/state.md b/docs/spec/slashing/state.md index 9a9a188ff2d2..556b9c3cc914 100644 --- a/docs/spec/slashing/state.md +++ b/docs/spec/slashing/state.md @@ -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 } @@ -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 -} -``` diff --git a/docs/spec/slashing/transactions.md b/docs/spec/slashing/transactions.md index c33e96047d61..6642acbb8e40 100644 --- a/docs/spec/slashing/transactions.md +++ b/docs/spec/slashing/transactions.md @@ -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" diff --git a/x/slashing/genesis.go b/x/slashing/genesis.go index aa126f56a5dd..eb943799b920 100644 --- a/x/slashing/genesis.go +++ b/x/slashing/genesis.go @@ -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 @@ -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), } } @@ -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 @@ -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) } @@ -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, } } diff --git a/x/slashing/handler.go b/x/slashing/handler.go index e55dfa0a82c8..2bed8db9423d 100644 --- a/x/slashing/handler.go +++ b/x/slashing/handler.go @@ -42,6 +42,10 @@ func handleMsgUnjail(ctx sdk.Context, msg MsgUnjail, k Keeper) sdk.Result { return ErrNoValidatorForAddress(k.codespace).Result() } + if info.Tombstoned { + return ErrValidatorJailed(k.codespace).Result() + } + // cannot be unjailed until out of jail if ctx.BlockHeader().Time.Before(info.JailedUntil) { return ErrValidatorJailed(k.codespace).Result() diff --git a/x/slashing/hooks.go b/x/slashing/hooks.go index 598de87ee381..8f90a5addd83 100644 --- a/x/slashing/hooks.go +++ b/x/slashing/hooks.go @@ -17,26 +17,11 @@ func (k Keeper) AfterValidatorBonded(ctx sdk.Context, address sdk.ConsAddress, _ StartHeight: ctx.BlockHeight(), IndexOffset: 0, JailedUntil: time.Unix(0, 0), + Tombstoned: false, MissedBlocksCounter: 0, } k.SetValidatorSigningInfo(ctx, address, signingInfo) } - - // Create a new slashing period when a validator is bonded - slashingPeriod := ValidatorSlashingPeriod{ - ValidatorAddr: address, - StartHeight: ctx.BlockHeight(), - EndHeight: 0, - SlashedSoFar: sdk.ZeroDec(), - } - k.SetValidatorSlashingPeriod(ctx, slashingPeriod) -} - -// Mark the slashing period as having ended when a validator begins unbonding -func (k Keeper) AfterValidatorBeginUnbonding(ctx sdk.Context, address sdk.ConsAddress, _ sdk.ValAddress) { - slashingPeriod := k.getValidatorSlashingPeriodForHeight(ctx, address, ctx.BlockHeight()) - slashingPeriod.EndHeight = ctx.BlockHeight() - k.SetValidatorSlashingPeriod(ctx, slashingPeriod) } // When a validator is created, add the address-pubkey relation. @@ -69,11 +54,6 @@ func (h Hooks) AfterValidatorBonded(ctx sdk.Context, consAddr sdk.ConsAddress, v h.k.AfterValidatorBonded(ctx, consAddr, valAddr) } -// Implements sdk.ValidatorHooks -func (h Hooks) AfterValidatorBeginUnbonding(ctx sdk.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) { - h.k.AfterValidatorBeginUnbonding(ctx, consAddr, valAddr) -} - // Implements sdk.ValidatorHooks func (h Hooks) AfterValidatorRemoved(ctx sdk.Context, consAddr sdk.ConsAddress, _ sdk.ValAddress) { h.k.AfterValidatorRemoved(ctx, consAddr) @@ -85,8 +65,8 @@ func (h Hooks) AfterValidatorCreated(ctx sdk.Context, valAddr sdk.ValAddress) { } // nolint - unused hooks -func (h Hooks) AfterValidatorPowerDidChange(_ sdk.Context, _ sdk.ConsAddress, _ sdk.ValAddress) { -} +func (h Hooks) AfterValidatorBeginUnbonding(_ sdk.Context, _ sdk.ConsAddress, _ sdk.ValAddress) {} +func (h Hooks) AfterValidatorPowerDidChange(_ sdk.Context, _ sdk.ConsAddress, _ sdk.ValAddress) {} func (h Hooks) BeforeValidatorModified(_ sdk.Context, _ sdk.ValAddress) {} func (h Hooks) BeforeDelegationCreated(_ sdk.Context, _ sdk.AccAddress, _ sdk.ValAddress) {} func (h Hooks) BeforeDelegationSharesModified(_ sdk.Context, _ sdk.AccAddress, _ sdk.ValAddress) {} diff --git a/x/slashing/hooks_test.go b/x/slashing/hooks_test.go deleted file mode 100644 index 563ede4f0d70..000000000000 --- a/x/slashing/hooks_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package slashing - -import ( - "testing" - - "github.com/stretchr/testify/require" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -func TestHookOnValidatorBonded(t *testing.T) { - ctx, _, _, _, keeper := createTestInput(t, DefaultParams()) - addr := sdk.ConsAddress(addrs[0]) - keeper.AfterValidatorBonded(ctx, addr, nil) - period := keeper.getValidatorSlashingPeriodForHeight(ctx, addr, ctx.BlockHeight()) - require.Equal(t, ValidatorSlashingPeriod{addr, ctx.BlockHeight(), 0, sdk.ZeroDec()}, period) -} - -func TestHookOnValidatorBeginUnbonding(t *testing.T) { - ctx, _, _, _, keeper := createTestInput(t, DefaultParams()) - addr := sdk.ConsAddress(addrs[0]) - keeper.AfterValidatorBonded(ctx, addr, nil) - keeper.AfterValidatorBeginUnbonding(ctx, addr, addrs[0]) - period := keeper.getValidatorSlashingPeriodForHeight(ctx, addr, ctx.BlockHeight()) - require.Equal(t, ValidatorSlashingPeriod{addr, ctx.BlockHeight(), ctx.BlockHeight(), sdk.ZeroDec()}, period) -} diff --git a/x/slashing/keeper.go b/x/slashing/keeper.go index 5137b11981c5..c0ea431f7ba0 100644 --- a/x/slashing/keeper.go +++ b/x/slashing/keeper.go @@ -47,7 +47,13 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractio panic(fmt.Sprintf("Validator consensus-address %v not found", consAddr)) } - // Get validator. + // Reject evidence if the double is too old + if age > k.MaxEvidenceAge(ctx) { + logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, age of %d past max age of %d", pubkey.Address(), infractionHeight, age, k.MaxEvidenceAge(ctx))) + return + } + + // Get validator and signing info validator := k.validatorSet.ValidatorByConsAddr(ctx, consAddr) if validator == nil || validator.GetStatus() == sdk.Unbonded { // Defensive. @@ -55,16 +61,19 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractio // Tendermint might break this assumption at some point. return } + signInfo, found := k.getValidatorSigningInfo(ctx, consAddr) + if !found { + panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr)) + } - // Double sign too old - maxEvidenceAge := k.MaxEvidenceAge(ctx) - if age > maxEvidenceAge { - logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, age of %d past max age of %d", pubkey.Address(), infractionHeight, age, maxEvidenceAge)) + // Validator is already tombstoned + if signInfo.Tombstoned { + logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, validator already tombstoned", pubkey.Address(), infractionHeight)) return } // Double sign confirmed - logger.Info(fmt.Sprintf("Confirmed double sign from %s at height %d, age of %d less than max age of %d", pubkey.Address(), infractionHeight, age, maxEvidenceAge)) + logger.Info(fmt.Sprintf("Confirmed double sign from %s at height %d, age of %d", pubkey.Address(), infractionHeight, age)) // We need to retrieve the stake distribution which signed the block, so we subtract ValidatorUpdateDelay from the evidence height. // Note that this *can* result in a negative "distributionHeight", up to -ValidatorUpdateDelay, @@ -72,31 +81,29 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractio // That's fine since this is just used to filter unbonding delegations & redelegations. distributionHeight := infractionHeight - stake.ValidatorUpdateDelay - // Cap the amount slashed to the penalty for the worst infraction - // within the slashing period when this infraction was committed + // get the percentage slash penalty fraction fraction := k.SlashFractionDoubleSign(ctx) - revisedFraction := k.capBySlashingPeriod(ctx, consAddr, fraction, distributionHeight) - logger.Info(fmt.Sprintf("Fraction slashed capped by slashing period from %v to %v", fraction, revisedFraction)) // Slash validator // `power` is the int64 power of the validator as provided to/by // Tendermint. This value is validator.Tokens as sent to Tendermint via // ABCI, and now received as evidence. - // The revisedFraction (which is the new fraction to be slashed) is passed - // in separately to separately slash unbonding and rebonding delegations. - k.validatorSet.Slash(ctx, consAddr, distributionHeight, power, revisedFraction) + // The fraction is passed in to separately to slash unbonding and rebonding delegations. + k.validatorSet.Slash(ctx, consAddr, distributionHeight, power, fraction) // Jail validator if not already jailed + // begin unbonding validator if not already unbonding (tombstone) if !validator.GetJailed() { k.validatorSet.Jail(ctx, consAddr) } - // Set or updated validator jail duration - signInfo, found := k.getValidatorSigningInfo(ctx, consAddr) - if !found { - panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr)) - } - signInfo.JailedUntil = time.Add(k.DoubleSignUnbondDuration(ctx)) + // Set slashed so far to total slash + signInfo.Tombstoned = true + + // Set jailed until to be forever (max time) + signInfo.JailedUntil = DoubleSignJailEndTime + + // Set validator signing info k.SetValidatorSigningInfo(ctx, consAddr, signInfo) } @@ -156,7 +163,7 @@ func (k Keeper) handleValidatorSignature(ctx sdk.Context, addr crypto.Address, p distributionHeight := height - stake.ValidatorUpdateDelay - 1 k.validatorSet.Slash(ctx, consAddr, distributionHeight, power, k.SlashFractionDowntime(ctx)) k.validatorSet.Jail(ctx, consAddr) - signInfo.JailedUntil = ctx.BlockHeader().Time.Add(k.DowntimeUnbondDuration(ctx)) + signInfo.JailedUntil = ctx.BlockHeader().Time.Add(k.DowntimeJailDuration(ctx)) // 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 diff --git a/x/slashing/keeper_test.go b/x/slashing/keeper_test.go index e15215bf837b..f9fe6cfa8726 100644 --- a/x/slashing/keeper_test.go +++ b/x/slashing/keeper_test.go @@ -16,8 +16,7 @@ import ( func keeperTestParams() Params { params := DefaultParams() params.SignedBlocksWindow = 1000 - params.DowntimeUnbondDuration = 60 * 60 - params.DoubleSignUnbondDuration = 60 * 60 + params.DowntimeJailDuration = 60 * 60 return params } @@ -45,41 +44,54 @@ func TestHandleDoubleSign(t *testing.T) { // handle a signature to set signing info keeper.handleValidatorSignature(ctx, val.Address(), amtInt, true) + oldTokens := sk.Validator(ctx, operatorAddr).GetTokens() + // double sign less than max age keeper.handleDoubleSign(ctx, val.Address(), 0, time.Unix(0, 0), amtInt) // should be jailed require.True(t, sk.Validator(ctx, operatorAddr).GetJailed()) - // unjail to measure power - sk.Unjail(ctx, sdk.ConsAddress(val.Address())) - // power should be reduced - require.True(sdk.IntEq( - t, amt.Mul(sdk.NewInt(19)).Div(sdk.NewInt(20)), - sk.Validator(ctx, operatorAddr).GetPower(), - )) - ctx = ctx.WithBlockHeader(abci.Header{Time: time.Unix(1, 0).Add(keeper.MaxEvidenceAge(ctx))}) - // double sign past max age + // tokens should be decreased + newTokens := sk.Validator(ctx, operatorAddr).GetTokens() + require.True(t, newTokens.LT(oldTokens)) + + // New evidence keeper.handleDoubleSign(ctx, val.Address(), 0, time.Unix(0, 0), amtInt) - require.True(sdk.IntEq( - t, amt.Mul(sdk.NewInt(19)).Div(sdk.NewInt(20)), - sk.Validator(ctx, operatorAddr).GetPower(), - )) + + // tokens should be the same (capped slash) + require.True(t, sk.Validator(ctx, operatorAddr).GetTokens().Equal(newTokens)) + + // Jump to past the unbonding period + ctx = ctx.WithBlockHeader(abci.Header{Time: time.Unix(1, 0).Add(sk.GetParams(ctx).UnbondingTime)}) + + // Still shouldn't be able to unjail + msgUnjail := NewMsgUnjail(operatorAddr) + res := handleMsgUnjail(ctx, msgUnjail, keeper) + require.False(t, res.IsOK()) + + // Should be able to unbond now + del, _ := sk.GetDelegation(ctx, sdk.AccAddress(operatorAddr), operatorAddr) + msgUnbond := stake.NewMsgBeginUnbonding(sdk.AccAddress(operatorAddr), operatorAddr, del.GetShares()) + res = stake.NewHandler(sk)(ctx, msgUnbond) + require.True(t, res.IsOK()) } -// Test that the amount a validator is slashed for multiple double signs -// is correctly capped by the slashing period in which they were committed -func TestSlashingPeriodCap(t *testing.T) { +// ______________________________________________________________ + +// Test that a validator is slashed correctly +// when we discover evidence of infraction +func TestPastMaxEvidenceAge(t *testing.T) { // initial setup - ctx, ck, sk, _, keeper := createTestInput(t, DefaultParams()) + ctx, ck, sk, _, keeper := createTestInput(t, keeperTestParams()) + // validator added pre-genesis + ctx = ctx.WithBlockHeight(-1) amtInt := int64(100) - operatorAddr, amt := addrs[0], sdk.NewInt(amtInt) - valConsPubKey, valConsAddr := pks[0], pks[0].Address() - got := stake.NewHandler(sk)(ctx, NewTestMsgCreateValidator(operatorAddr, valConsPubKey, amt)) + operatorAddr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) + got := stake.NewHandler(sk)(ctx, NewTestMsgCreateValidator(operatorAddr, val, amt)) require.True(t, got.IsOK()) stake.EndBlocker(ctx, sk) - ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) require.Equal( t, ck.GetCoins(ctx, sdk.AccAddress(operatorAddr)), sdk.Coins{sdk.NewCoin(sk.GetParams(ctx).BondDenom, initCoins.Sub(amt))}, @@ -87,51 +99,20 @@ func TestSlashingPeriodCap(t *testing.T) { require.True(sdk.IntEq(t, amt, sk.Validator(ctx, operatorAddr).GetPower())) // handle a signature to set signing info - keeper.handleValidatorSignature(ctx, valConsAddr, amtInt, true) + keeper.handleValidatorSignature(ctx, val.Address(), amtInt, true) - // double sign less than max age - keeper.handleDoubleSign(ctx, valConsAddr, 1, time.Unix(0, 0), amtInt) - // should be jailed - require.True(t, sk.Validator(ctx, operatorAddr).GetJailed()) - // end block - stake.EndBlocker(ctx, sk) - // update block height - ctx = ctx.WithBlockHeight(int64(2)) - // unjail to measure power - sk.Unjail(ctx, sdk.ConsAddress(valConsAddr)) - // end block - stake.EndBlocker(ctx, sk) - // power should be reduced - expectedPower := amt.Mul(sdk.NewInt(19)).Div(sdk.NewInt(20)) - require.True(sdk.IntEq(t, expectedPower, sk.Validator(ctx, operatorAddr).GetPower())) + ctx = ctx.WithBlockHeader(abci.Header{Time: time.Unix(1, 0).Add(keeper.MaxEvidenceAge(ctx))}) - // double sign again, same slashing period - keeper.handleDoubleSign(ctx, valConsAddr, 1, time.Unix(0, 0), amtInt) - // should be jailed - require.True(t, sk.Validator(ctx, operatorAddr).GetJailed()) - // end block - stake.EndBlocker(ctx, sk) - // update block height - ctx = ctx.WithBlockHeight(int64(3)) - // unjail to measure power - sk.Unjail(ctx, sdk.ConsAddress(valConsAddr)) - // end block - stake.EndBlocker(ctx, sk) - // power should be equal, no more should have been slashed - expectedPower = amt.Mul(sdk.NewInt(19)).Div(sdk.NewInt(20)) - require.True(sdk.IntEq(t, expectedPower, sk.Validator(ctx, operatorAddr).GetPower())) + oldPower := sk.Validator(ctx, operatorAddr).GetPower() - // double sign again, new slashing period - keeper.handleDoubleSign(ctx, valConsAddr, 3, time.Unix(0, 0), amtInt) - // should be jailed - require.True(t, sk.Validator(ctx, operatorAddr).GetJailed()) - // unjail to measure power - sk.Unjail(ctx, sdk.ConsAddress(valConsAddr)) - // end block - stake.EndBlocker(ctx, sk) - // power should be reduced - expectedPower = amt.Mul(sdk.NewInt(18)).Div(sdk.NewInt(20)) - require.True(sdk.IntEq(t, expectedPower, sk.Validator(ctx, operatorAddr).GetPower())) + // double sign past max age + keeper.handleDoubleSign(ctx, val.Address(), 0, time.Unix(0, 0), amtInt) + + // should still be bonded + require.True(t, sk.Validator(ctx, operatorAddr).GetStatus() == sdk.Bonded) + + // should still have same power + require.True(t, sk.Validator(ctx, operatorAddr).GetPower().Equal(oldPower)) } // Test a validator through uptime, downtime, revocation, @@ -226,20 +207,12 @@ func TestHandleAbsentValidator(t *testing.T) { validator, _ = sk.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(val)) require.Equal(t, amtInt64-slashAmt, validator.GetTokens().Int64()) - // 502nd block *double signed* (oh no!) - keeper.handleDoubleSign(ctx, val.Address(), height, ctx.BlockHeader().Time, amtInt64) - - // validator should have been slashed - validator, _ = sk.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(val)) - secondSlashAmt := sdk.NewDec(amtInt64).Mul(keeper.SlashFractionDoubleSign(ctx)).RoundInt64() - require.Equal(t, amtInt64-slashAmt-secondSlashAmt, validator.GetTokens().Int64()) - // unrevocation should fail prior to jail expiration got = slh(ctx, NewMsgUnjail(addr)) require.False(t, got.IsOK()) // unrevocation should succeed after jail expiration - ctx = ctx.WithBlockHeader(abci.Header{Time: time.Unix(1, 0).Add(keeper.DowntimeUnbondDuration(ctx))}) + ctx = ctx.WithBlockHeader(abci.Header{Time: time.Unix(1, 0).Add(keeper.DowntimeJailDuration(ctx))}) got = slh(ctx, NewMsgUnjail(addr)) require.True(t, got.IsOK()) @@ -252,9 +225,9 @@ func TestHandleAbsentValidator(t *testing.T) { // validator should have been slashed pool = sk.GetPool(ctx) - require.Equal(t, amtInt64-slashAmt-secondSlashAmt, pool.BondedTokens.Int64()) + require.Equal(t, amtInt64-slashAmt, pool.BondedTokens.Int64()) - // validator start height should not have been changed + // Validator start height should not have been changed info, found = keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) require.True(t, found) require.Equal(t, int64(0), info.StartHeight) diff --git a/x/slashing/params.go b/x/slashing/params.go index 6f95ca73e9fa..4e43c43de7fd 100644 --- a/x/slashing/params.go +++ b/x/slashing/params.go @@ -12,15 +12,19 @@ const ( DefaultParamspace = "slashing" ) +// The Double Sign Jail period ends at Max Time supported by Amino (Dec 31, 9999 - 23:59:59 GMT) +var ( + DoubleSignJailEndTime = time.Unix(253402300799, 0) +) + // Parameter store key var ( - KeyMaxEvidenceAge = []byte("MaxEvidenceAge") - KeySignedBlocksWindow = []byte("SignedBlocksWindow") - KeyMinSignedPerWindow = []byte("MinSignedPerWindow") - KeyDoubleSignUnbondDuration = []byte("DoubleSignUnbondDuration") - KeyDowntimeUnbondDuration = []byte("DowntimeUnbondDuration") - KeySlashFractionDoubleSign = []byte("SlashFractionDoubleSign") - KeySlashFractionDowntime = []byte("SlashFractionDowntime") + KeyMaxEvidenceAge = []byte("MaxEvidenceAge") + KeySignedBlocksWindow = []byte("SignedBlocksWindow") + KeyMinSignedPerWindow = []byte("MinSignedPerWindow") + KeyDowntimeJailDuration = []byte("DowntimeJailDuration") + KeySlashFractionDoubleSign = []byte("SlashFractionDoubleSign") + KeySlashFractionDowntime = []byte("SlashFractionDowntime") ) // ParamTypeTable for slashing module @@ -30,13 +34,12 @@ func ParamTypeTable() params.TypeTable { // Params - used for initializing default parameter for slashing at genesis type Params struct { - MaxEvidenceAge time.Duration `json:"max-evidence-age"` - SignedBlocksWindow int64 `json:"signed-blocks-window"` - MinSignedPerWindow sdk.Dec `json:"min-signed-per-window"` - DoubleSignUnbondDuration time.Duration `json:"double-sign-unbond-duration"` - DowntimeUnbondDuration time.Duration `json:"downtime-unbond-duration"` - SlashFractionDoubleSign sdk.Dec `json:"slash-fraction-double-sign"` - SlashFractionDowntime sdk.Dec `json:"slash-fraction-downtime"` + MaxEvidenceAge time.Duration `json:"max-evidence-age"` + SignedBlocksWindow int64 `json:"signed-blocks-window"` + MinSignedPerWindow sdk.Dec `json:"min-signed-per-window"` + DowntimeJailDuration time.Duration `json:"downtime-jail-duration"` + SlashFractionDoubleSign sdk.Dec `json:"slash-fraction-double-sign"` + SlashFractionDowntime sdk.Dec `json:"slash-fraction-downtime"` } // Implements params.ParamStruct @@ -45,8 +48,7 @@ func (p *Params) KeyValuePairs() params.KeyValuePairs { {KeyMaxEvidenceAge, &p.MaxEvidenceAge}, {KeySignedBlocksWindow, &p.SignedBlocksWindow}, {KeyMinSignedPerWindow, &p.MinSignedPerWindow}, - {KeyDoubleSignUnbondDuration, &p.DoubleSignUnbondDuration}, - {KeyDowntimeUnbondDuration, &p.DowntimeUnbondDuration}, + {KeyDowntimeJailDuration, &p.DowntimeJailDuration}, {KeySlashFractionDoubleSign, &p.SlashFractionDoubleSign}, {KeySlashFractionDowntime, &p.SlashFractionDowntime}, } @@ -59,14 +61,11 @@ func DefaultParams() Params { // TODO Temporarily set to 2 minutes for testnets. MaxEvidenceAge: 60 * 2 * time.Second, - // TODO Temporarily set to five minutes for testnets - DoubleSignUnbondDuration: 60 * 5 * time.Second, - // TODO Temporarily set to 100 blocks for testnets SignedBlocksWindow: 100, // TODO Temporarily set to 10 minutes for testnets - DowntimeUnbondDuration: 60 * 10 * time.Second, + DowntimeJailDuration: 60 * 10 * time.Second, MinSignedPerWindow: sdk.NewDecWithPrec(5, 1), @@ -97,15 +96,9 @@ func (k Keeper) MinSignedPerWindow(ctx sdk.Context) int64 { return sdk.NewDec(signedBlocksWindow).Mul(minSignedPerWindow).RoundInt64() } -// Double-sign unbond duration -func (k Keeper) DoubleSignUnbondDuration(ctx sdk.Context) (res time.Duration) { - k.paramspace.Get(ctx, KeyDoubleSignUnbondDuration, &res) - return -} - // Downtime unbond duration -func (k Keeper) DowntimeUnbondDuration(ctx sdk.Context) (res time.Duration) { - k.paramspace.Get(ctx, KeyDowntimeUnbondDuration, &res) +func (k Keeper) DowntimeJailDuration(ctx sdk.Context) (res time.Duration) { + k.paramspace.Get(ctx, KeyDowntimeJailDuration, &res) return } diff --git a/x/slashing/signing_info.go b/x/slashing/signing_info.go index f4f2d2fde8cd..78cd30bfd833 100644 --- a/x/slashing/signing_info.go +++ b/x/slashing/signing_info.go @@ -91,11 +91,12 @@ func (k Keeper) clearValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.C } // Construct a new `ValidatorSigningInfo` struct -func NewValidatorSigningInfo(startHeight int64, indexOffset int64, jailedUntil time.Time, missedBlocksCounter int64) ValidatorSigningInfo { +func NewValidatorSigningInfo(startHeight int64, indexOffset int64, jailedUntil time.Time, tombstoned bool, missedBlocksCounter int64) ValidatorSigningInfo { return ValidatorSigningInfo{ StartHeight: startHeight, IndexOffset: indexOffset, JailedUntil: jailedUntil, + Tombstoned: tombstoned, MissedBlocksCounter: missedBlocksCounter, } } @@ -105,6 +106,7 @@ type ValidatorSigningInfo struct { StartHeight int64 `json:"start_height"` // height at which validator was first a candidate OR was unjailed IndexOffset int64 `json:"index_offset"` // index offset into signed block bit array JailedUntil time.Time `json:"jailed_until"` // timestamp validator cannot be unjailed until + Tombstoned bool `json:"tombstoned"` // whether or not a validator has been tombstoned (killed out of validator set) MissedBlocksCounter int64 `json:"missed_blocks_counter"` // missed blocks counter (to avoid scanning the array every time) } diff --git a/x/slashing/slashing_period.go b/x/slashing/slashing_period.go deleted file mode 100644 index ee2c135875ff..000000000000 --- a/x/slashing/slashing_period.go +++ /dev/null @@ -1,134 +0,0 @@ -package slashing - -import ( - "encoding/binary" - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - stake "github.com/cosmos/cosmos-sdk/x/stake/types" -) - -// Cap an infraction's slash amount by the slashing period in which it was committed -func (k Keeper) capBySlashingPeriod(ctx sdk.Context, address sdk.ConsAddress, fraction sdk.Dec, infractionHeight int64) (revisedFraction sdk.Dec) { - - // Fetch the newest slashing period starting before this infraction was committed - slashingPeriod := k.getValidatorSlashingPeriodForHeight(ctx, address, infractionHeight) - - // Sanity check - if slashingPeriod.EndHeight > 0 && slashingPeriod.EndHeight < infractionHeight { - panic(fmt.Sprintf("slashing period ended before infraction: validator %s, infraction height %d, slashing period ended at %d", address, infractionHeight, slashingPeriod.EndHeight)) - } - - // Calculate the updated total slash amount - // This is capped at the slashing fraction for the worst infraction within this slashing period - totalToSlash := sdk.MaxDec(slashingPeriod.SlashedSoFar, fraction) - - // Calculate the remainder which we now must slash - revisedFraction = totalToSlash.Sub(slashingPeriod.SlashedSoFar) - - // Update the slashing period struct - slashingPeriod.SlashedSoFar = totalToSlash - k.SetValidatorSlashingPeriod(ctx, slashingPeriod) - - return -} - -// Stored by validator Tendermint address (not operator address) -// This function retrieves the most recent slashing period starting -// before a particular height - so the slashing period that was "in effect" -// at the time of an infraction committed at that height. -// Slashing periods are created upon validator bonding. -func (k Keeper) getValidatorSlashingPeriodForHeight(ctx sdk.Context, address sdk.ConsAddress, height int64) (slashingPeriod ValidatorSlashingPeriod) { - store := ctx.KVStore(k.storeKey) - // Get the most recent slashing period at or before the infraction height - start := GetValidatorSlashingPeriodPrefix(address) - end := sdk.PrefixEndBytes(GetValidatorSlashingPeriodKey(address, height)) - iterator := store.ReverseIterator(start, end) - if !iterator.Valid() { - panic(fmt.Sprintf("expected to find slashing period for validator %s before height %d, but none was found", address, height)) - } - slashingPeriod = k.unmarshalSlashingPeriodKeyValue(iterator.Key(), iterator.Value()) - return -} - -// Iterate over all slashing periods in the store, calling on each -// decode slashing period a provided handler function -// Stop if the provided handler function returns true -func (k Keeper) IterateValidatorSlashingPeriods(ctx sdk.Context, handler func(slashingPeriod ValidatorSlashingPeriod) (stop bool)) { - store := ctx.KVStore(k.storeKey) - iter := sdk.KVStorePrefixIterator(store, ValidatorSlashingPeriodKey) - defer iter.Close() - for ; iter.Valid(); iter.Next() { - slashingPeriod := k.unmarshalSlashingPeriodKeyValue(iter.Key(), iter.Value()) - if handler(slashingPeriod) { - break - } - } -} - -// Delete all slashing periods in the store. -func (k Keeper) DeleteValidatorSlashingPeriods(ctx sdk.Context) { - store := ctx.KVStore(k.storeKey) - iter := sdk.KVStorePrefixIterator(store, ValidatorSlashingPeriodKey) - for ; iter.Valid(); iter.Next() { - store.Delete(iter.Key()) - } - iter.Close() -} - -// Stored by validator Tendermint address (not operator address) -// This function sets a validator slashing period for a particular validator, -// start height, end height, and current slashed-so-far total, or updates -// an existing slashing period for the same validator and start height. -func (k Keeper) SetValidatorSlashingPeriod(ctx sdk.Context, slashingPeriod ValidatorSlashingPeriod) { - slashingPeriodValue := ValidatorSlashingPeriodValue{ - EndHeight: slashingPeriod.EndHeight, - SlashedSoFar: slashingPeriod.SlashedSoFar, - } - store := ctx.KVStore(k.storeKey) - bz := k.cdc.MustMarshalBinaryLengthPrefixed(slashingPeriodValue) - store.Set(GetValidatorSlashingPeriodKey(slashingPeriod.ValidatorAddr, slashingPeriod.StartHeight), bz) -} - -// Unmarshal key/value into a ValidatorSlashingPeriod -func (k Keeper) unmarshalSlashingPeriodKeyValue(key []byte, value []byte) ValidatorSlashingPeriod { - var slashingPeriodValue ValidatorSlashingPeriodValue - k.cdc.MustUnmarshalBinaryLengthPrefixed(value, &slashingPeriodValue) - address := sdk.ConsAddress(key[1 : 1+sdk.AddrLen]) - startHeight := int64(binary.BigEndian.Uint64(key[1+sdk.AddrLen:1+sdk.AddrLen+8]) - uint64(stake.ValidatorUpdateDelay)) - return ValidatorSlashingPeriod{ - ValidatorAddr: address, - StartHeight: startHeight, - EndHeight: slashingPeriodValue.EndHeight, - SlashedSoFar: slashingPeriodValue.SlashedSoFar, - } -} - -// Construct a new `ValidatorSlashingPeriod` struct -func NewValidatorSlashingPeriod(startHeight int64, endHeight int64, slashedSoFar sdk.Dec) ValidatorSlashingPeriod { - return ValidatorSlashingPeriod{ - StartHeight: startHeight, - EndHeight: endHeight, - SlashedSoFar: slashedSoFar, - } -} - -// Slashing period for a validator -type ValidatorSlashingPeriod struct { - ValidatorAddr sdk.ConsAddress `json:"validator_addr"` // validator which this slashing period is for - StartHeight int64 `json:"start_height"` // starting height of the slashing period - EndHeight int64 `json:"end_height"` // ending height of the slashing period, or sentinel value of 0 for in-progress - SlashedSoFar sdk.Dec `json:"slashed_so_far"` // fraction of validator stake slashed so far in this slashing period -} - -// Value part of slashing period (validator address & start height are stored in the key) -type ValidatorSlashingPeriodValue struct { - EndHeight int64 `json:"end_height"` - SlashedSoFar sdk.Dec `json:"slashed_so_far"` -} - -// Return human readable slashing period -func (p ValidatorSlashingPeriod) HumanReadableString() string { - return fmt.Sprintf("Start height: %d, end height: %d, slashed so far: %v", - p.StartHeight, p.EndHeight, p.SlashedSoFar) -} diff --git a/x/slashing/slashing_period_test.go b/x/slashing/slashing_period_test.go deleted file mode 100644 index 522dcdf787e3..000000000000 --- a/x/slashing/slashing_period_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package slashing - -import ( - "testing" - - "github.com/stretchr/testify/require" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -func TestGetSetValidatorSlashingPeriod(t *testing.T) { - ctx, _, _, _, keeper := createTestInput(t, DefaultParams()) - addr := sdk.ConsAddress(addrs[0]) - height := int64(5) - require.Panics(t, func() { keeper.getValidatorSlashingPeriodForHeight(ctx, addr, height) }) - newPeriod := ValidatorSlashingPeriod{ - ValidatorAddr: addr, - StartHeight: height, - EndHeight: height + 10, - SlashedSoFar: sdk.ZeroDec(), - } - keeper.SetValidatorSlashingPeriod(ctx, newPeriod) - - // Get at start height - retrieved := keeper.getValidatorSlashingPeriodForHeight(ctx, addr, height) - require.Equal(t, newPeriod, retrieved) - - // Get after start height (works) - retrieved = keeper.getValidatorSlashingPeriodForHeight(ctx, addr, int64(6)) - require.Equal(t, newPeriod, retrieved) - - // Get before start height (panic) - require.Panics(t, func() { keeper.getValidatorSlashingPeriodForHeight(ctx, addr, int64(0)) }) - - // Get after end height (panic) - newPeriod.EndHeight = int64(4) - keeper.SetValidatorSlashingPeriod(ctx, newPeriod) - require.Panics(t, func() { keeper.capBySlashingPeriod(ctx, addr, sdk.ZeroDec(), height) }) - - // Back to old end height - newPeriod.EndHeight = height + 10 - keeper.SetValidatorSlashingPeriod(ctx, newPeriod) - - // Set a new, later period - anotherPeriod := ValidatorSlashingPeriod{ - ValidatorAddr: addr, - StartHeight: height + 1, - EndHeight: height + 11, - SlashedSoFar: sdk.ZeroDec(), - } - keeper.SetValidatorSlashingPeriod(ctx, anotherPeriod) - - // Old period retrieved for prior height - retrieved = keeper.getValidatorSlashingPeriodForHeight(ctx, addr, height) - require.Equal(t, newPeriod, retrieved) - - // New period retrieved at new height - retrieved = keeper.getValidatorSlashingPeriodForHeight(ctx, addr, height+1) - require.Equal(t, anotherPeriod, retrieved) -} - -func TestValidatorSlashingPeriodCap(t *testing.T) { - ctx, _, _, _, keeper := createTestInput(t, DefaultParams()) - addr := sdk.ConsAddress(addrs[0]) - height := int64(5) - newPeriod := ValidatorSlashingPeriod{ - ValidatorAddr: addr, - StartHeight: height, - EndHeight: height + 10, - SlashedSoFar: sdk.ZeroDec(), - } - keeper.SetValidatorSlashingPeriod(ctx, newPeriod) - half := sdk.NewDec(1).Quo(sdk.NewDec(2)) - - // First slash should be full - fractionA := keeper.capBySlashingPeriod(ctx, addr, half, height) - require.True(t, fractionA.Equal(half)) - - // Second slash should be capped - fractionB := keeper.capBySlashingPeriod(ctx, addr, half, height) - require.True(t, fractionB.Equal(sdk.ZeroDec())) - - // Third slash should be capped to difference - fractionC := keeper.capBySlashingPeriod(ctx, addr, sdk.OneDec(), height) - require.True(t, fractionC.Equal(half)) -} diff --git a/x/slashing/test_common.go b/x/slashing/test_common.go index a0203bf5ab1e..8a5f44773e58 100644 --- a/x/slashing/test_common.go +++ b/x/slashing/test_common.go @@ -92,7 +92,7 @@ func createTestInput(t *testing.T, defaults Params) (sdk.Context, bank.Keeper, s sk.SetHooks(keeper.Hooks()) require.NotPanics(t, func() { - InitGenesis(ctx, keeper, GenesisState{defaults, nil, nil, nil}, genesis) + InitGenesis(ctx, keeper, GenesisState{defaults, nil, nil}, genesis) }) return ctx, ck, sk, paramstore, keeper