Merge PR #2480: Fix signing info handling bugs & faulty slashing
This commit is contained in:
commit
55f4f61493
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
Loading…
Reference in New Issue