Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

R4R: Fix signing info handling bugs & faulty slashing #2480

Merged
merged 23 commits into from
Oct 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion client/lcd/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion client/lcd/lcd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
22 changes: 12 additions & 10 deletions docs/spec/slashing/begin-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions docs/spec/slashing/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions docs/spec/slashing/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
}

```
Expand All @@ -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

Expand Down
4 changes: 0 additions & 4 deletions docs/spec/slashing/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions x/distribution/keeper/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
6 changes: 3 additions & 3 deletions x/distribution/types/validator_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down
6 changes: 1 addition & 5 deletions x/slashing/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
2 changes: 1 addition & 1 deletion x/slashing/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 15 additions & 1 deletion x/slashing/hooks.go
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down
36 changes: 21 additions & 15 deletions x/slashing/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,33 +102,35 @@ 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++

// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to keep this dependent on the StartHeight ?

That way a validator could unbond every SignedBlocksWindow-2 blocks and then rebond to avoid slashing since the StartHeight is reset now.

Copy link
Contributor Author

@cwgoes cwgoes Oct 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, of course, in the case when they aren't jailed at all.

I think I'll try the other strategy, deleting the array shouldn't be too expensive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we never reset the start height, and instead reset the counter & clear the array when the validator is slashed for downtime.

validator := k.validatorSet.ValidatorByConsAddr(ctx, consAddr)
if validator != nil && !validator.GetJailed() {
// Downtime confirmed: slash and jail the validator
Expand All @@ -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",
Expand Down
Loading