Skip to content

Commit

Permalink
chore: revert ostracon slashing changes (revert Finschia#347)
Browse files Browse the repository at this point in the history
  • Loading branch information
zemyblue committed Nov 23, 2023
1 parent e5fdd7a commit 17bfbe8
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 124 deletions.
2 changes: 1 addition & 1 deletion x/slashing/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

func (k Keeper) AfterValidatorBonded(ctx sdk.Context, address sdk.ConsAddress, _ sdk.ValAddress) {
// Update the signing info voter set counter or create a new signing info
// Update the signing info start height or create a new signing info
_, found := k.GetValidatorSigningInfo(ctx, address)
if !found {
signingInfo := types.NewValidatorSigningInfo(
Expand Down
11 changes: 5 additions & 6 deletions x/slashing/keeper/infractions.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre
}

// this is a relative index, so it counts blocks the validator *should* have signed
// will use the 0-value default signing info if not present, except for the beginning
// will use the 0-value default signing info if not present, except for start height
index := signInfo.IndexOffset % k.SignedBlocksWindow(ctx)
signInfo.IndexOffset++

Expand Down Expand Up @@ -69,12 +69,11 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre
)
}

numVotes := signInfo.IndexOffset
minVotes := k.SignedBlocksWindow(ctx)
minHeight := signInfo.StartHeight + k.SignedBlocksWindow(ctx)
maxMissed := k.SignedBlocksWindow(ctx) - minSignedPerWindow

// if we have joined enough times to voter set and the validator has missed too many blocks, punish them
if numVotes >= minVotes && signInfo.MissedBlocksCounter > maxMissed {
// if we are past the minimum height and the validator has missed too many blocks, punish them
if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
validator := k.sk.ValidatorByConsAddr(ctx, consAddr)
if validator != nil && !validator.IsJailed() {
// Downtime confirmed: slash and jail the validator
Expand Down Expand Up @@ -108,7 +107,7 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre
"slashing and jailing validator due to liveness fault",
"height", height,
"validator", consAddr.String(),
"min_votes", minVotes,
"min_height", minHeight,
"threshold", minSignedPerWindow,
"slashed", k.SlashFractionDowntime(ctx).String(),
"jailed_until", signInfo.JailedUntil,
Expand Down
57 changes: 15 additions & 42 deletions x/slashing/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestUnJailNotBonded(t *testing.T) {
}

// Test a new validator entering the validator set
// Ensure that SigningInfo.VoterSetCounter is set correctly
// Ensure that SigningInfo.StartHeight is set correctly
// and that they are not immediately jailed
func TestHandleNewValidator(t *testing.T) {
app := simapp.Setup(false)
Expand Down Expand Up @@ -173,7 +173,7 @@ func TestHandleAlreadyJailed(t *testing.T) {

// Test a validator dipping in and out of the validator set
// Ensure that missed blocks are tracked correctly and that
// the voter set counter of the signing info is reset correctly
// the start height of the signing info is reset correctly
func TestValidatorDippingInAndOut(t *testing.T) {
// initial setup
// TestParams set the SignedBlocksWindow to 1000 and MaxMissedBlocksPerWindow to 500
Expand Down Expand Up @@ -222,45 +222,30 @@ func TestValidatorDippingInAndOut(t *testing.T) {
tstaking.CheckValidator(valAddr, stakingtypes.Bonded, false)
newPower := int64(150)

// validator misses 501 blocks exceeding the liveness threshold
// validator misses a block
app.SlashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, false)
height++

// shouldn't be jailed/kicked yet
tstaking.CheckValidator(valAddr, stakingtypes.Bonded, false)

// validator misses 500 more blocks, 501 total
latest := height
for ; height < latest+501; height++ {
for ; height < latest+500; height++ {
ctx = ctx.WithBlockHeight(height)
app.SlashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, false)
}

// 398 more blocks happened
latest = height
for ; height < latest+398; height++ {
ctx = ctx.WithBlockHeight(height)
app.SlashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, true)
}

// shouldn't be jailed/kicked yet because it have not joined to vote set 1000 times
// 100 times + (kicked) + 501 times + 398 times = 999 times
tstaking.CheckValidator(valAddr, stakingtypes.Bonded, false)

// check all the signing information
signInfo, found := app.SlashingKeeper.GetValidatorSigningInfo(ctx, consAddr)
require.True(t, found)
require.Equal(t, int64(0), signInfo.StartHeight)
require.Equal(t, int64(999), signInfo.IndexOffset)
require.Equal(t, int64(501), signInfo.MissedBlocksCounter)

// another block happened
ctx = ctx.WithBlockHeight(height)
app.SlashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, true)

// should now be jailed & kicked
staking.EndBlocker(ctx, app.StakingKeeper)
tstaking.CheckValidator(valAddr, stakingtypes.Unbonding, true)

// check all the signing information
signInfo, found = app.SlashingKeeper.GetValidatorSigningInfo(ctx, consAddr)
signInfo, found := app.SlashingKeeper.GetValidatorSigningInfo(ctx, consAddr)
require.True(t, found)
require.Equal(t, int64(0), signInfo.StartHeight)
require.Equal(t, int64(0), signInfo.IndexOffset)
require.Equal(t, int64(0), signInfo.MissedBlocksCounter)
require.Equal(t, int64(0), signInfo.IndexOffset)
// array should be cleared
for offset := int64(0); offset < app.SlashingKeeper.SignedBlocksWindow(ctx); offset++ {
missed := app.SlashingKeeper.GetValidatorMissedBlockBitArray(ctx, consAddr, offset)
Expand All @@ -280,24 +265,12 @@ func TestValidatorDippingInAndOut(t *testing.T) {
staking.EndBlocker(ctx, app.StakingKeeper)
tstaking.CheckValidator(valAddr, stakingtypes.Bonded, false)

// 1000 blocks happened
// validator misses 501 blocks
latest = height
for ; height < latest+1000; height++ {
ctx = ctx.WithBlockHeight(height)
app.SlashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, true)
}

// validator misses 500 blocks
latest = height
for ; height < latest+500; height++ {
for ; height < latest+501; height++ {
ctx = ctx.WithBlockHeight(height)
app.SlashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, false)
}
tstaking.CheckValidator(valAddr, stakingtypes.Bonded, false)

// validator misses another block
ctx = ctx.WithBlockHeight(height)
app.SlashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, false)

// validator should now be jailed & kicked
staking.EndBlocker(ctx, app.StakingKeeper)
Expand Down
130 changes: 58 additions & 72 deletions x/slashing/spec/04_begin_block.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,106 +7,92 @@ order: 4
## Liveness Tracking

At the beginning of each block, we update the `ValidatorSigningInfo` for each
voter and check if they've crossed below the liveness threshold over a
validator and check if they've crossed below the liveness threshold over a
sliding window. This sliding window is defined by `SignedBlocksWindow` and the
index in this window is determined by `VoterSetCounter` found in the voter's
`ValidatorSigningInfo`. For each vote processed, the `VoterSetCounter` is incremented
regardless if the voter signed or not. Once the index is determined, the
index in this window is determined by `IndexOffset` found in the validator's
`ValidatorSigningInfo`. For each block processed, the `IndexOffset` is incremented
regardless if the validator signed or not. Once the index is determined, the
`MissedBlocksBitArray` and `MissedBlocksCounter` are updated accordingly.

Finally, in order to determine if a voter crosses below the liveness threshold,
Finally, in order to determine if a validator crosses below the liveness threshold,
we fetch the maximum number of blocks missed, `maxMissed`, which is
`SignedBlocksWindow - (MinSignedPerWindow * SignedBlocksWindow)` and the minimum
height at which we can determine liveness, `minVoterSetCount`. If the voter set counter is
greater than `minVoterSetCount` and the voter's `MissedBlocksCounter` is greater than
height at which we can determine liveness, `minHeight`. If the current block is
greater than `minHeight` and the validator's `MissedBlocksCounter` is greater than
`maxMissed`, they will be slashed by `SlashFractionDowntime`, will be jailed
for `DowntimeJailDuration`, and have the following values reset:
`MissedBlocksBitArray` and `MissedBlocksCounter`.
`MissedBlocksBitArray`, `MissedBlocksCounter`, and `IndexOffset`.

**Note**: Liveness slashes do **NOT** lead to a tombstombing.

```go
height := ctx.BlockHeight()
height := block.Height

for _, voteInfo := range req.LastCommitInfo.getVotes() {
// fetch the validator public key
consAddr := sdk.BytesToConsAddress(voteInfo.Validator.Address)
if _, err := k.GetPubkey(ctx, addr); err != nil {
panic(fmt.Sprintf("Validator consensus-address %s not found", consAddr))
}
for vote in block.LastCommitInfo.Votes {
signInfo := GetValidatorSigningInfo(vote.Validator.Address)

// fetch signing info
signInfo, found := k.GetValidatorSigningInfo(ctx, consAddr)
if !found {
panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr))
}
// This is a relative index, so we counts blocks the validator SHOULD have
// signed. We use the 0-value default signing info if not present, except for
// start height.
index := signInfo.IndexOffset % SignedBlocksWindow()
signInfo.IndexOffset++

// Update MissedBlocksBitArray and MissedBlocksCounter. The MissedBlocksCounter
// just tracks the sum of MissedBlocksBitArray. That way we avoid needing to
// read/write the whole array each time.
missedPrevious := GetValidatorMissedBlockBitArray(vote.Validator.Address, index)
missed := !signed

// this is a relative index, so it counts blocks the validator *should* have signed
// will use the 0-value default signing info if not present, except for the beginning
voterSetCounter := signInfo.VoterSetCounter
signInfo.VoterSetCounter++
index := voterSetCounter % k.SignedBlocksWindow(ctx)

// 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.GetValidatorMissedBlockBitArray(ctx, consAddr, index)
missed := !voteInfo.SignedLastBlock
switch {
case !previous && missed:
// Array value has changed from not missed to missed, increment counter
k.SetValidatorMissedBlockBitArray(ctx, consAddr, index, true)
case !missedPrevious && missed:
// array index has changed from not missed to missed, increment counter
SetValidatorMissedBlockBitArray(vote.Validator.Address, index, true)
signInfo.MissedBlocksCounter++
case previous && !missed:
// Array value has changed from missed to not missed, decrement counter
k.SetValidatorMissedBlockBitArray(ctx, consAddr, index, false)

case missedPrevious && !missed:
// array index has changed from missed to not missed, decrement counter
SetValidatorMissedBlockBitArray(vote.Validator.Address, index, false)
signInfo.MissedBlocksCounter--

default:
// Array value at this index has not changed, no need to update counter
// array index at this index has not changed; no need to update counter
}

minSignedPerWindow := k.MinSignedPerWindow(ctx)

if missed {
// emit events...
}

minVoterSetCount := k.SignedBlocksWindow(ctx)
maxMissed := k.SignedBlocksWindow(ctx) - minSignedPerWindow
minHeight := signInfo.StartHeight + SignedBlocksWindow()
maxMissed := SignedBlocksWindow() - MinSignedPerWindow()

// if we have joined enough times to voter set and the validator has missed too many blocks, punish them
if voterSetCounter >= minVoterSetCount && signInfo.MissedBlocksCounter > maxMissed {
validator := k.sk.ValidatorByConsAddr(ctx, consAddr)
if validator != nil && !validator.IsJailed() {
// Downtime confirmed: slash and jail the validator
// We need to retrieve the stake distribution which signed the block, so we subtract ValidatorUpdateDelay from the evidence height,
// and subtract an additional 1 since this is the LastCommit.
// Note that this *can* result in a negative "distributionHeight" up to -ValidatorUpdateDelay-1,
// i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block.
// That's fine since this is just used to filter unbonding delegations & redelegations.
distributionHeight := height - sdk.ValidatorUpdateDelay - 1
// If we are past the minimum height and the validator has missed too many
// jail and slash them.
if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
validator := ValidatorByConsAddr(vote.Validator.Address)

// emit events...

k.sk.Slash(ctx, consAddr, distributionHeight, voteInfo.Validator.Power, k.SlashFractionDowntime(ctx))
k.sk.Jail(ctx, consAddr)

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.VoterSetCounter = 0
k.clearValidatorMissedBlockBitArray(ctx, consAddr)

// log events...
} else {
// validator was (a) not found or (b) already jailed so we do not slash
// emit events...

// log events...
}
// We need to retrieve the stake distribution which signed the block, so we
// subtract ValidatorUpdateDelay from the block height, and subtract an
// additional 1 since this is the LastCommit.
//
// Note, that this CAN result in a negative "distributionHeight" up to
// -ValidatorUpdateDelay-1, i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block.
// That's fine since this is just used to filter unbonding delegations & redelegations.
distributionHeight := height - sdk.ValidatorUpdateDelay - 1

Slash(vote.Validator.Address, distributionHeight, vote.Validator.Power, SlashFractionDowntime())
Jail(vote.Validator.Address)

signInfo.JailedUntil = block.Time.Add(DowntimeJailDuration())

// 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
ClearValidatorMissedBlockBitArray(vote.Validator.Address)
}

// Set the updated signing info
k.SetValidatorSigningInfo(ctx, consAddr, signInfo)
SetValidatorSigningInfo(vote.Validator.Address, signInfo)
}
```
7 changes: 4 additions & 3 deletions x/slashing/spec/05_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ The following hooks impact the slashing state:
## Validator Bonded

Upon successful first-time bonding of a new validator, we create a new `ValidatorSigningInfo` structure for the
now-bonded validator.
now-bonded validator, which `StartHeight` of the current block.

```
onValidatorBonded(address sdk.ValAddress)
signingInfo, found = GetValidatorSigningInfo(address)
if !found {
signingInfo = ValidatorSigningInfo {
StartHeight : CurrentHeight,
IndexOffset : 0,
JailedUntil : time.Unix(0, 0),
Tombstone : false,
MissedBloskCounter : 0,
VoterSetCounter : 0,
MissedBloskCounter : 0
}
setValidatorSigningInfo(signingInfo)
}
Expand Down

0 comments on commit 17bfbe8

Please sign in to comment.