diff --git a/PENDING.md b/PENDING.md index eb61a5468..376cd2daf 100644 --- a/PENDING.md +++ b/PENDING.md @@ -69,6 +69,7 @@ BREAKING CHANGES * [x/staking] \#2244 staking now holds a consensus-address-index instead of a consensus-pubkey-index * [x/staking] \#2236 more distribution hooks for distribution * [x/stake] \#2394 Split up UpdateValidator into distinct state transitions applied only in EndBlock + * [x/slashing] \#2480 Fix signing info handling bugs & faulty slashing * [x/stake] \#2412 Added an unbonding validator queue to EndBlock to automatically update validator.Status when finished Unbonding * [x/stake] \#2500 Block conflicting redelegations until we add an index * [x/params] Global Paramstore refactored diff --git a/client/lcd/certificates.go b/client/lcd/certificates.go index f47f2397c..1516ed35a 100644 --- a/client/lcd/certificates.go +++ b/client/lcd/certificates.go @@ -43,7 +43,7 @@ func generateSelfSignedCert(host string) (certBytes []byte, priv *ecdsa.PrivateK KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, - IsCA: true, + IsCA: true, } hosts := strings.Split(host, ",") for _, h := range hosts { diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index fce58c253..f675afe14 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -713,7 +713,7 @@ func TestUnjail(t *testing.T) { tests.WaitForHeight(4, port) require.Equal(t, true, signingInfo.IndexOffset > 0) require.Equal(t, time.Unix(0, 0).UTC(), signingInfo.JailedUntil) - require.Equal(t, true, signingInfo.SignedBlocksCounter > 0) + require.Equal(t, true, signingInfo.MissedBlocksCounter == 0) } func TestProposalsQuery(t *testing.T) { diff --git a/docs/spec/slashing/begin-block.md b/docs/spec/slashing/begin-block.md index 375e19185..43691b38d 100644 --- a/docs/spec/slashing/begin-block.md +++ b/docs/spec/slashing/begin-block.md @@ -93,25 +93,27 @@ for val in block.Validators: index := signInfo.IndexOffset % SIGNED_BLOCKS_WINDOW signInfo.IndexOffset++ - previous = SigningBitArray.Get(val.Address, index) + previous = MissedBlockBitArray.Get(val.Address, index) // update counter if array has changed - if previous and val in block.AbsentValidators: - SigningBitArray.Set(val.Address, index, false) - signInfo.SignedBlocksCounter-- - else if !previous and val not in block.AbsentValidators: - SigningBitArray.Set(val.Address, index, true) - signInfo.SignedBlocksCounter++ + if !previous and val in block.AbsentValidators: + MissedBlockBitArray.Set(val.Address, index, true) + signInfo.MissedBlocksCounter++ + else if previous and val not in block.AbsentValidators: + MissedBlockBitArray.Set(val.Address, index, false) + signInfo.MissedBlocksCounter-- // else previous == val not in block.AbsentValidators, no change // validator must be active for at least SIGNED_BLOCKS_WINDOW // before they can be automatically unbonded for failing to be // included in 50% of the recent LastCommits minHeight = signInfo.StartHeight + SIGNED_BLOCKS_WINDOW - minSigned = SIGNED_BLOCKS_WINDOW / 2 - if height > minHeight AND signInfo.SignedBlocksCounter < minSigned: + maxMissed = SIGNED_BLOCKS_WINDOW / 2 + if height > minHeight AND signInfo.MissedBlocksCounter > maxMissed: signInfo.JailedUntil = block.Time + DOWNTIME_UNBOND_DURATION - + signInfo.IndexOffset = 0 + signInfo.MissedBlocksCounter = 0 + clearMissedBlockBitArray() slash & unbond the validator SigningInfo.Set(val.Address, signInfo) diff --git a/docs/spec/slashing/hooks.md b/docs/spec/slashing/hooks.md index 36dde61f9..1888c1d2f 100644 --- a/docs/spec/slashing/hooks.md +++ b/docs/spec/slashing/hooks.md @@ -12,6 +12,17 @@ and `SlashedSoFar` of `0`: ``` onValidatorBonded(address sdk.ValAddress) + signingInfo, found = getValidatorSigningInfo(address) + if !found { + signingInfo = ValidatorSigningInfo { + StartHeight : CurrentHeight, + IndexOffset : 0, + JailedUntil : time.Unix(0, 0), + MissedBloskCounter : 0 + } + setValidatorSigningInfo(signingInfo) + } + slashingPeriod = SlashingPeriod{ ValidatorAddr : address, StartHeight : CurrentHeight, diff --git a/docs/spec/slashing/state.md b/docs/spec/slashing/state.md index ae426db7b..9a9a188ff 100644 --- a/docs/spec/slashing/state.md +++ b/docs/spec/slashing/state.md @@ -17,18 +17,18 @@ Information about validator activity is tracked in a `ValidatorSigningInfo`. It is indexed in the store as follows: - SigningInfo: ` 0x01 | ValTendermintAddr -> amino(valSigningInfo)` -- SigningBitArray: ` 0x02 | ValTendermintAddr | LittleEndianUint64(signArrayIndex) -> VarInt(didSign)` +- MissedBlocksBitArray: ` 0x02 | ValTendermintAddr | LittleEndianUint64(signArrayIndex) -> VarInt(didMiss)` The first map allows us to easily lookup the recent signing info for a validator, according to the Tendermint validator address. The second map acts as -a bit-array of size `SIGNED_BLOCKS_WINDOW` that tells us if the validator signed for a given index in the bit-array. +a bit-array of size `SIGNED_BLOCKS_WINDOW` that tells us if the validator missed the block for a given index in the bit-array. The index in the bit-array is given as little endian uint64. The result is a `varint` that takes on `0` or `1`, where `0` indicates the -validator did not sign the corresponding block, and `1` indicates they did. +validator did not miss (did sign) the corresponding block, and `1` indicates they missed the block (did not sign). -Note that the SigningBitArray is not explicitly initialized up-front. Keys are +Note that the MissedBlocksBitArray is not explicitly initialized up-front. Keys are added as we progress through the first `SIGNED_BLOCKS_WINDOW` blocks for a newly bonded validator. @@ -40,7 +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 - SignedBlocksCounter int64 // Running counter of signed blocks + MissedBlocksCounter int64 // Running counter of missed blocks } ``` @@ -49,7 +49,7 @@ 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 -* `SignedBlocksCounter` is a counter kept to avoid unnecessary array reads. `SignedBlocksBitArray.Sum() == SignedBlocksCounter` always. +* `MissedBlocksCounter` is a counter kept to avoid unnecessary array reads. `MissedBlocksBitArray.Sum() == MissedBlocksCounter` always. ## Slashing Period diff --git a/docs/spec/slashing/transactions.md b/docs/spec/slashing/transactions.md index be33ee096..c33e96047 100644 --- a/docs/spec/slashing/transactions.md +++ b/docs/spec/slashing/transactions.md @@ -25,10 +25,6 @@ handleMsgUnjail(tx TxUnjail) 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) diff --git a/x/distribution/keeper/key.go b/x/distribution/keeper/key.go index 2e5989081..466d83c24 100644 --- a/x/distribution/keeper/key.go +++ b/x/distribution/keeper/key.go @@ -13,9 +13,9 @@ var ( ProposerKey = []byte{0x04} // key for storing the proposer operator address // params store - ParamStoreKeyCommunityTax = []byte("community-tax") - ParamStoreKeyBaseProposerReward = []byte("base-proposer-reward") - ParamStoreKeyBonusProposerReward = []byte("bonus-proposer-reward") + ParamStoreKeyCommunityTax = []byte("communitytax") + ParamStoreKeyBaseProposerReward = []byte("baseproposerreward") + ParamStoreKeyBonusProposerReward = []byte("bonusproposerreward") ) const ( diff --git a/x/distribution/types/validator_info.go b/x/distribution/types/validator_info.go index 18aef8bde..c8a02569c 100644 --- a/x/distribution/types/validator_info.go +++ b/x/distribution/types/validator_info.go @@ -19,9 +19,9 @@ func NewValidatorDistInfo(operatorAddr sdk.ValAddress, currentHeight int64) Vali return ValidatorDistInfo{ OperatorAddr: operatorAddr, FeePoolWithdrawalHeight: currentHeight, - Pool: DecCoins{}, - PoolCommission: DecCoins{}, - DelAccum: NewTotalAccum(currentHeight), + Pool: DecCoins{}, + PoolCommission: DecCoins{}, + DelAccum: NewTotalAccum(currentHeight), } } diff --git a/x/slashing/handler.go b/x/slashing/handler.go index 740166d2a..701577080 100644 --- a/x/slashing/handler.go +++ b/x/slashing/handler.go @@ -46,11 +46,7 @@ func handleMsgUnjail(ctx sdk.Context, msg MsgUnjail, k Keeper) sdk.Result { return ErrValidatorJailed(k.codespace).Result() } - // update the starting height so the validator can't be immediately jailed - // again - info.StartHeight = ctx.BlockHeight() - k.setValidatorSigningInfo(ctx, consAddr, info) - + // unjail the validator k.validatorSet.Unjail(ctx, consAddr) tags := sdk.NewTags("action", []byte("unjail"), "validator", []byte(msg.ValidatorAddr.String())) diff --git a/x/slashing/handler_test.go b/x/slashing/handler_test.go index 394a8e0b3..ef307547c 100644 --- a/x/slashing/handler_test.go +++ b/x/slashing/handler_test.go @@ -53,7 +53,7 @@ func TestJailedValidatorDelegations(t *testing.T) { StartHeight: int64(0), IndexOffset: int64(0), JailedUntil: time.Unix(0, 0), - SignedBlocksCounter: int64(0), + MissedBlocksCounter: int64(0), } slashingKeeper.setValidatorSigningInfo(ctx, consAddr, newInfo) diff --git a/x/slashing/hooks.go b/x/slashing/hooks.go index 701a6b2cd..ed07bc0c7 100644 --- a/x/slashing/hooks.go +++ b/x/slashing/hooks.go @@ -1,11 +1,25 @@ package slashing import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" ) -// Create a new slashing period when a validator is bonded func (k Keeper) onValidatorBonded(ctx sdk.Context, address sdk.ConsAddress) { + // Update the signing info start height or create a new signing info + _, found := k.getValidatorSigningInfo(ctx, address) + if !found { + signingInfo := ValidatorSigningInfo{ + StartHeight: ctx.BlockHeight(), + IndexOffset: 0, + JailedUntil: time.Unix(0, 0), + MissedBlocksCounter: 0, + } + k.setValidatorSigningInfo(ctx, address, signingInfo) + } + + // Create a new slashing period when a validator is bonded slashingPeriod := ValidatorSlashingPeriod{ ValidatorAddr: address, StartHeight: ctx.BlockHeight(), diff --git a/x/slashing/keeper.go b/x/slashing/keeper.go index f5fe3bc36..861391ac2 100644 --- a/x/slashing/keeper.go +++ b/x/slashing/keeper.go @@ -102,8 +102,7 @@ func (k Keeper) handleValidatorSignature(ctx sdk.Context, addr crypto.Address, p // Will use the 0-value default signing info if not present, except for start height signInfo, found := k.getValidatorSigningInfo(ctx, consAddr) if !found { - // If this validator has never been seen before, construct a new SigningInfo with the correct start height - signInfo = NewValidatorSigningInfo(height, 0, time.Unix(0, 0), 0) + panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr)) } index := signInfo.IndexOffset % k.SignedBlocksWindow(ctx) signInfo.IndexOffset++ @@ -111,24 +110,27 @@ func (k Keeper) handleValidatorSignature(ctx sdk.Context, addr crypto.Address, p // Update signed block bit array & counter // This counter just tracks the sum of the bit array // That way we avoid needing to read/write the whole array each time - previous := k.getValidatorSigningBitArray(ctx, consAddr, index) - if previous == signed { + previous := k.getValidatorMissedBlockBitArray(ctx, consAddr, index) + missed := !signed + switch { + case !previous && missed: + // Array value has changed from not missed to missed, increment counter + k.setValidatorMissedBlockBitArray(ctx, consAddr, index, true) + signInfo.MissedBlocksCounter++ + case previous && !missed: + // Array value has changed from missed to not missed, decrement counter + k.setValidatorMissedBlockBitArray(ctx, consAddr, index, false) + signInfo.MissedBlocksCounter-- + default: // Array value at this index has not changed, no need to update counter - } else if previous && !signed { - // Array value has changed from signed to unsigned, decrement counter - k.setValidatorSigningBitArray(ctx, consAddr, index, false) - signInfo.SignedBlocksCounter-- - } else if !previous && signed { - // Array value has changed from unsigned to signed, increment counter - k.setValidatorSigningBitArray(ctx, consAddr, index, true) - signInfo.SignedBlocksCounter++ } - if !signed { - logger.Info(fmt.Sprintf("Absent validator %s at height %d, %d signed, threshold %d", addr, height, signInfo.SignedBlocksCounter, k.MinSignedPerWindow(ctx))) + if missed { + logger.Info(fmt.Sprintf("Absent validator %s at height %d, %d missed, threshold %d", addr, height, signInfo.MissedBlocksCounter, k.MinSignedPerWindow(ctx))) } minHeight := signInfo.StartHeight + k.SignedBlocksWindow(ctx) - if height > minHeight && signInfo.SignedBlocksCounter < k.MinSignedPerWindow(ctx) { + maxMissed := k.SignedBlocksWindow(ctx) - k.MinSignedPerWindow(ctx) + if height > minHeight && signInfo.MissedBlocksCounter > maxMissed { validator := k.validatorSet.ValidatorByConsAddr(ctx, consAddr) if validator != nil && !validator.GetJailed() { // Downtime confirmed: slash and jail the validator @@ -143,6 +145,10 @@ func (k Keeper) handleValidatorSignature(ctx sdk.Context, addr crypto.Address, p k.validatorSet.Slash(ctx, consAddr, distributionHeight, power, k.SlashFractionDowntime(ctx)) k.validatorSet.Jail(ctx, consAddr) signInfo.JailedUntil = ctx.BlockHeader().Time.Add(k.DowntimeUnbondDuration(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 + k.clearValidatorMissedBlockBitArray(ctx, consAddr) } else { // Validator was (a) not found or (b) already jailed, don't slash logger.Info(fmt.Sprintf("Validator %s would have been slashed for downtime, but was either not found in store or already jailed", diff --git a/x/slashing/keeper_test.go b/x/slashing/keeper_test.go index a0980d04d..caf1cf3da 100644 --- a/x/slashing/keeper_test.go +++ b/x/slashing/keeper_test.go @@ -30,7 +30,6 @@ func TestHandleDoubleSign(t *testing.T) { ctx, ck, sk, _, keeper := createTestInput(t, keeperTestParams()) // validator added pre-genesis ctx = ctx.WithBlockHeight(-1) - sk = sk.WithHooks(keeper.Hooks()) amtInt := int64(100) operatorAddr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) got := stake.NewHandler(sk)(ctx, NewTestMsgCreateValidator(operatorAddr, val, amt)) @@ -71,7 +70,6 @@ func TestSlashingPeriodCap(t *testing.T) { // initial setup ctx, ck, sk, _, keeper := createTestInput(t, DefaultParams()) - sk = sk.WithHooks(keeper.Hooks()) amtInt := int64(100) operatorAddr, amt := addrs[0], sdk.NewInt(amtInt) valConsPubKey, valConsAddr := pks[0], pks[0].Address() @@ -137,7 +135,6 @@ func TestHandleAbsentValidator(t *testing.T) { // initial setup ctx, ck, sk, _, keeper := createTestInput(t, keeperTestParams()) - sk = sk.WithHooks(keeper.Hooks()) amtInt := int64(100) addr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) sh := stake.NewHandler(sk) @@ -148,14 +145,13 @@ func TestHandleAbsentValidator(t *testing.T) { keeper.AddValidators(ctx, validatorUpdates) require.Equal(t, ck.GetCoins(ctx, sdk.AccAddress(addr)), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.Sub(amt)}}) require.True(t, sdk.NewDecFromInt(amt).Equal(sk.Validator(ctx, addr).GetPower())) + // will exist since the validator has been bonded info, found := keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) - require.False(t, found) + require.True(t, found) require.Equal(t, int64(0), info.StartHeight) require.Equal(t, int64(0), info.IndexOffset) - require.Equal(t, int64(0), info.SignedBlocksCounter) - // default time.Time value - var blankTime time.Time - require.Equal(t, blankTime, info.JailedUntil) + require.Equal(t, int64(0), info.MissedBlocksCounter) + require.Equal(t, time.Unix(0, 0).UTC(), info.JailedUntil) height := int64(0) // 1000 first blocks OK @@ -166,7 +162,7 @@ func TestHandleAbsentValidator(t *testing.T) { info, found = keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) require.True(t, found) require.Equal(t, int64(0), info.StartHeight) - require.Equal(t, keeper.SignedBlocksWindow(ctx), info.SignedBlocksCounter) + require.Equal(t, int64(0), info.MissedBlocksCounter) // 500 blocks missed for ; height < keeper.SignedBlocksWindow(ctx)+(keeper.SignedBlocksWindow(ctx)-keeper.MinSignedPerWindow(ctx)); height++ { @@ -176,7 +172,7 @@ func TestHandleAbsentValidator(t *testing.T) { info, found = keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) require.True(t, found) require.Equal(t, int64(0), info.StartHeight) - require.Equal(t, keeper.SignedBlocksWindow(ctx)-keeper.MinSignedPerWindow(ctx), info.SignedBlocksCounter) + require.Equal(t, keeper.SignedBlocksWindow(ctx)-keeper.MinSignedPerWindow(ctx), info.MissedBlocksCounter) // validator should be bonded still validator, _ := sk.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(val)) @@ -190,7 +186,8 @@ func TestHandleAbsentValidator(t *testing.T) { info, found = keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) require.True(t, found) require.Equal(t, int64(0), info.StartHeight) - require.Equal(t, keeper.SignedBlocksWindow(ctx)-keeper.MinSignedPerWindow(ctx)-1, info.SignedBlocksCounter) + // counter now reset to zero + require.Equal(t, int64(0), info.MissedBlocksCounter) // end block stake.EndBlocker(ctx, sk) @@ -211,7 +208,7 @@ func TestHandleAbsentValidator(t *testing.T) { info, found = keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address())) require.True(t, found) require.Equal(t, int64(0), info.StartHeight) - require.Equal(t, keeper.SignedBlocksWindow(ctx)-keeper.MinSignedPerWindow(ctx)-2, info.SignedBlocksCounter) + require.Equal(t, int64(1), info.MissedBlocksCounter) // end block stake.EndBlocker(ctx, sk) @@ -248,12 +245,12 @@ func TestHandleAbsentValidator(t *testing.T) { pool = sk.GetPool(ctx) require.Equal(t, amtInt-slashAmt-secondSlashAmt, pool.BondedTokens.RoundInt64()) - // validator start height should 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, height, info.StartHeight) - // we've missed 2 blocks more than the maximum - require.Equal(t, keeper.SignedBlocksWindow(ctx)-keeper.MinSignedPerWindow(ctx)-2, info.SignedBlocksCounter) + require.Equal(t, int64(0), info.StartHeight) + // we've missed 2 blocks more than the maximum, so the counter was reset to 0 at 1 block more and is now 1 + require.Equal(t, int64(1), info.MissedBlocksCounter) // validator should not be immediately jailed again height++ @@ -294,6 +291,11 @@ func TestHandleNewValidator(t *testing.T) { ctx, ck, sk, _, keeper := createTestInput(t, keeperTestParams()) addr, val, amt := addrs[0], pks[0], int64(100) sh := stake.NewHandler(sk) + + // 1000 first blocks not a validator + ctx = ctx.WithBlockHeight(keeper.SignedBlocksWindow(ctx) + 1) + + // Validator created got := sh(ctx, NewTestMsgCreateValidator(addr, val, sdk.NewInt(amt))) require.True(t, got.IsOK()) validatorUpdates := stake.EndBlocker(ctx, sk) @@ -301,9 +303,6 @@ func TestHandleNewValidator(t *testing.T) { require.Equal(t, ck.GetCoins(ctx, sdk.AccAddress(addr)), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins.SubRaw(amt)}}) require.Equal(t, sdk.NewDec(amt), sk.Validator(ctx, addr).GetPower()) - // 1000 first blocks not a validator - ctx = ctx.WithBlockHeight(keeper.SignedBlocksWindow(ctx) + 1) - // Now a validator, for two blocks keeper.handleValidatorSignature(ctx, val.Address(), 100, true) ctx = ctx.WithBlockHeight(keeper.SignedBlocksWindow(ctx) + 2) @@ -313,7 +312,7 @@ func TestHandleNewValidator(t *testing.T) { require.True(t, found) require.Equal(t, keeper.SignedBlocksWindow(ctx)+1, info.StartHeight) require.Equal(t, int64(2), info.IndexOffset) - require.Equal(t, int64(1), info.SignedBlocksCounter) + require.Equal(t, int64(1), info.MissedBlocksCounter) require.Equal(t, time.Unix(0, 0).UTC(), info.JailedUntil) // validator should be bonded still, should not have been jailed or slashed @@ -369,3 +368,112 @@ func TestHandleAlreadyJailed(t *testing.T) { require.Equal(t, amtInt-1, validator.GetTokens().RoundInt64()) } + +// Test a validator dipping in and out of the validator set +// Ensure that missed blocks are tracked correctly and that +// the start height of the signing info is reset correctly +func TestValidatorDippingInAndOut(t *testing.T) { + + // initial setup + // keeperTestParams set the SignedBlocksWindow to 1000 and MaxMissedBlocksPerWindow to 500 + ctx, _, sk, _, keeper := createTestInput(t, keeperTestParams()) + params := sk.GetParams(ctx) + params.MaxValidators = 1 + sk.SetParams(ctx, params) + amtInt := int64(100) + addr, val, amt := addrs[0], pks[0], sdk.NewInt(amtInt) + consAddr := sdk.ConsAddress(addr) + sh := stake.NewHandler(sk) + got := sh(ctx, NewTestMsgCreateValidator(addr, val, amt)) + require.True(t, got.IsOK()) + validatorUpdates := stake.EndBlocker(ctx, sk) + keeper.AddValidators(ctx, validatorUpdates) + + // 100 first blocks OK + height := int64(0) + for ; height < int64(100); height++ { + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val.Address(), amtInt, true) + } + + // validator kicked out of validator set + newAmt := int64(101) + got = sh(ctx, NewTestMsgCreateValidator(addrs[1], pks[1], sdk.NewInt(newAmt))) + require.True(t, got.IsOK()) + validatorUpdates = stake.EndBlocker(ctx, sk) + require.Equal(t, 2, len(validatorUpdates)) + keeper.AddValidators(ctx, validatorUpdates) + validator, _ := sk.GetValidator(ctx, addr) + require.Equal(t, sdk.Unbonding, validator.Status) + + // 600 more blocks happened + height = int64(700) + ctx = ctx.WithBlockHeight(height) + + // validator added back in + got = sh(ctx, newTestMsgDelegate(sdk.AccAddress(addrs[2]), addrs[0], sdk.NewInt(2))) + require.True(t, got.IsOK()) + validatorUpdates = stake.EndBlocker(ctx, sk) + require.Equal(t, 2, len(validatorUpdates)) + validator, _ = sk.GetValidator(ctx, addr) + require.Equal(t, sdk.Bonded, validator.Status) + newAmt = int64(102) + + // validator misses a block + keeper.handleValidatorSignature(ctx, val.Address(), newAmt, false) + height++ + + // shouldn't be jailed/kicked yet + validator, _ = sk.GetValidator(ctx, addr) + require.Equal(t, sdk.Bonded, validator.Status) + + // validator misses 500 more blocks, 501 total + latest := height + for ; height < latest+500; height++ { + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val.Address(), newAmt, false) + } + + // should now be jailed & kicked + stake.EndBlocker(ctx, sk) + validator, _ = sk.GetValidator(ctx, addr) + require.Equal(t, sdk.Unbonding, validator.Status) + + // check all the signing information + signInfo, found := keeper.getValidatorSigningInfo(ctx, consAddr) + require.True(t, found) + require.Equal(t, int64(0), signInfo.MissedBlocksCounter) + require.Equal(t, int64(0), signInfo.IndexOffset) + // array should be cleared + for offset := int64(0); offset < keeper.SignedBlocksWindow(ctx); offset++ { + missed := keeper.getValidatorMissedBlockBitArray(ctx, consAddr, offset) + require.False(t, missed) + } + + // some blocks pass + height = int64(5000) + ctx = ctx.WithBlockHeight(height) + + // validator rejoins and starts signing again + sk.Unjail(ctx, consAddr) + keeper.handleValidatorSignature(ctx, val.Address(), newAmt, true) + height++ + + // validator should not be kicked since we reset counter/array when it was jailed + stake.EndBlocker(ctx, sk) + validator, _ = sk.GetValidator(ctx, addr) + require.Equal(t, sdk.Bonded, validator.Status) + + // validator misses 501 blocks + latest = height + for ; height < latest+501; height++ { + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val.Address(), newAmt, false) + } + + // validator should now be jailed & kicked + stake.EndBlocker(ctx, sk) + validator, _ = sk.GetValidator(ctx, addr) + require.Equal(t, sdk.Unbonding, validator.Status) + +} diff --git a/x/slashing/keys.go b/x/slashing/keys.go index ec453cedc..8f4fecc6c 100644 --- a/x/slashing/keys.go +++ b/x/slashing/keys.go @@ -9,10 +9,10 @@ import ( // key prefix bytes var ( - ValidatorSigningInfoKey = []byte{0x01} // Prefix for signing info - ValidatorSigningBitArrayKey = []byte{0x02} // Prefix for signature bit array - ValidatorSlashingPeriodKey = []byte{0x03} // Prefix for slashing period - AddrPubkeyRelationKey = []byte{0x04} // Prefix for address-pubkey relation + ValidatorSigningInfoKey = []byte{0x01} // Prefix for signing info + ValidatorMissedBlockBitArrayKey = []byte{0x02} // Prefix for missed block bit array + ValidatorSlashingPeriodKey = []byte{0x03} // Prefix for slashing period + AddrPubkeyRelationKey = []byte{0x04} // Prefix for address-pubkey relation ) // stored by *Tendermint* address (not operator address) @@ -21,10 +21,15 @@ func GetValidatorSigningInfoKey(v sdk.ConsAddress) []byte { } // stored by *Tendermint* address (not operator address) -func GetValidatorSigningBitArrayKey(v sdk.ConsAddress, i int64) []byte { +func GetValidatorMissedBlockBitArrayPrefixKey(v sdk.ConsAddress) []byte { + return append(ValidatorMissedBlockBitArrayKey, v.Bytes()...) +} + +// stored by *Tendermint* address (not operator address) +func GetValidatorMissedBlockBitArrayKey(v sdk.ConsAddress, i int64) []byte { b := make([]byte, 8) binary.LittleEndian.PutUint64(b, uint64(i)) - return append(ValidatorSigningBitArrayKey, append(v.Bytes(), b...)...) + return append(GetValidatorMissedBlockBitArrayPrefixKey(v), b...) } // stored by *Tendermint* address (not operator address) diff --git a/x/slashing/signing_info.go b/x/slashing/signing_info.go index 1adf49abc..8c35693f1 100644 --- a/x/slashing/signing_info.go +++ b/x/slashing/signing_info.go @@ -28,32 +28,42 @@ func (k Keeper) setValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress } // Stored by *validator* address (not operator address) -func (k Keeper) getValidatorSigningBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64) (signed bool) { +func (k Keeper) getValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64) (missed bool) { store := ctx.KVStore(k.storeKey) - bz := store.Get(GetValidatorSigningBitArrayKey(address, index)) + bz := store.Get(GetValidatorMissedBlockBitArrayKey(address, index)) if bz == nil { - // lazy: treat empty key as unsigned - signed = false + // lazy: treat empty key as not missed + missed = false return } - k.cdc.MustUnmarshalBinary(bz, &signed) + k.cdc.MustUnmarshalBinary(bz, &missed) return } // Stored by *validator* address (not operator address) -func (k Keeper) setValidatorSigningBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64, signed bool) { +func (k Keeper) setValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64, missed bool) { store := ctx.KVStore(k.storeKey) - bz := k.cdc.MustMarshalBinary(signed) - store.Set(GetValidatorSigningBitArrayKey(address, index), bz) + bz := k.cdc.MustMarshalBinary(missed) + store.Set(GetValidatorMissedBlockBitArrayKey(address, index), bz) +} + +// Stored by *validator* address (not operator address) +func (k Keeper) clearValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, GetValidatorMissedBlockBitArrayPrefixKey(address)) + for ; iter.Valid(); iter.Next() { + store.Delete(iter.Key()) + } + iter.Close() } // Construct a new `ValidatorSigningInfo` struct -func NewValidatorSigningInfo(startHeight int64, indexOffset int64, jailedUntil time.Time, signedBlocksCounter int64) ValidatorSigningInfo { +func NewValidatorSigningInfo(startHeight int64, indexOffset int64, jailedUntil time.Time, missedBlocksCounter int64) ValidatorSigningInfo { return ValidatorSigningInfo{ StartHeight: startHeight, IndexOffset: indexOffset, JailedUntil: jailedUntil, - SignedBlocksCounter: signedBlocksCounter, + MissedBlocksCounter: missedBlocksCounter, } } @@ -62,11 +72,11 @@ 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 - SignedBlocksCounter int64 `json:"signed_blocks_counter"` // signed blocks counter (to avoid scanning the array every time) + MissedBlocksCounter int64 `json:"missed_blocks_counter"` // missed blocks counter (to avoid scanning the array every time) } // Return human readable signing info func (i ValidatorSigningInfo) HumanReadableString() string { - return fmt.Sprintf("Start height: %d, index offset: %d, jailed until: %v, signed blocks counter: %d", - i.StartHeight, i.IndexOffset, i.JailedUntil, i.SignedBlocksCounter) + return fmt.Sprintf("Start height: %d, index offset: %d, jailed until: %v, missed blocks counter: %d", + i.StartHeight, i.IndexOffset, i.JailedUntil, i.MissedBlocksCounter) } diff --git a/x/slashing/signing_info_test.go b/x/slashing/signing_info_test.go index 123457d5d..15863ebc7 100644 --- a/x/slashing/signing_info_test.go +++ b/x/slashing/signing_info_test.go @@ -17,7 +17,7 @@ func TestGetSetValidatorSigningInfo(t *testing.T) { StartHeight: int64(4), IndexOffset: int64(3), JailedUntil: time.Unix(2, 0), - SignedBlocksCounter: int64(10), + MissedBlocksCounter: int64(10), } keeper.setValidatorSigningInfo(ctx, sdk.ConsAddress(addrs[0]), newInfo) info, found = keeper.getValidatorSigningInfo(ctx, sdk.ConsAddress(addrs[0])) @@ -25,14 +25,14 @@ func TestGetSetValidatorSigningInfo(t *testing.T) { require.Equal(t, info.StartHeight, int64(4)) require.Equal(t, info.IndexOffset, int64(3)) require.Equal(t, info.JailedUntil, time.Unix(2, 0).UTC()) - require.Equal(t, info.SignedBlocksCounter, int64(10)) + require.Equal(t, info.MissedBlocksCounter, int64(10)) } -func TestGetSetValidatorSigningBitArray(t *testing.T) { +func TestGetSetValidatorMissedBlockBitArray(t *testing.T) { ctx, _, _, _, keeper := createTestInput(t, DefaultParams()) - signed := keeper.getValidatorSigningBitArray(ctx, sdk.ConsAddress(addrs[0]), 0) - require.False(t, signed) // treat empty key as unsigned - keeper.setValidatorSigningBitArray(ctx, sdk.ConsAddress(addrs[0]), 0, true) - signed = keeper.getValidatorSigningBitArray(ctx, sdk.ConsAddress(addrs[0]), 0) - require.True(t, signed) // now should be signed + missed := keeper.getValidatorMissedBlockBitArray(ctx, sdk.ConsAddress(addrs[0]), 0) + require.False(t, missed) // treat empty key as not missed + keeper.setValidatorMissedBlockBitArray(ctx, sdk.ConsAddress(addrs[0]), 0, true) + missed = keeper.getValidatorMissedBlockBitArray(ctx, sdk.ConsAddress(addrs[0]), 0) + require.True(t, missed) // now should be missed } diff --git a/x/slashing/test_common.go b/x/slashing/test_common.go index 54eb906eb..a648bfe85 100644 --- a/x/slashing/test_common.go +++ b/x/slashing/test_common.go @@ -88,6 +88,7 @@ func createTestInput(t *testing.T, defaults Params) (sdk.Context, bank.Keeper, s require.Nil(t, err) paramstore := paramsKeeper.Subspace(DefaultParamspace) keeper := NewKeeper(cdc, keySlashing, sk, paramstore, DefaultCodespace) + sk = sk.WithHooks(keeper.Hooks()) require.NotPanics(t, func() { InitGenesis(ctx, keeper, GenesisState{defaults}, genesis) diff --git a/x/slashing/tick_test.go b/x/slashing/tick_test.go index c7617f4d7..a2a2d9f0f 100644 --- a/x/slashing/tick_test.go +++ b/x/slashing/tick_test.go @@ -45,7 +45,7 @@ func TestBeginBlocker(t *testing.T) { require.Equal(t, ctx.BlockHeight(), info.StartHeight) require.Equal(t, int64(1), info.IndexOffset) require.Equal(t, time.Unix(0, 0).UTC(), info.JailedUntil) - require.Equal(t, int64(1), info.SignedBlocksCounter) + require.Equal(t, int64(0), info.MissedBlocksCounter) height := int64(0) diff --git a/x/stake/handler_test.go b/x/stake/handler_test.go index 083bb9a91..8755c91b0 100644 --- a/x/stake/handler_test.go +++ b/x/stake/handler_test.go @@ -847,11 +847,11 @@ func TestConflictingRedelegation(t *testing.T) { keeper.SetParams(ctx, params) // create the validators - msgCreateValidator := newTestMsgCreateValidator(validatorAddr, keep.PKs[0], 10) + msgCreateValidator := NewTestMsgCreateValidator(validatorAddr, keep.PKs[0], 10) got := handleMsgCreateValidator(ctx, msgCreateValidator, keeper) require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") - msgCreateValidator = newTestMsgCreateValidator(validatorAddr2, keep.PKs[1], 10) + msgCreateValidator = NewTestMsgCreateValidator(validatorAddr2, keep.PKs[1], 10) got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator")