Skip to content
Open
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
6 changes: 6 additions & 0 deletions x/oracle/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"time"

core "github.com/classic-terra/core/v3/types"
"github.com/classic-terra/core/v3/types/util"
"github.com/classic-terra/core/v3/x/oracle/keeper"
"github.com/classic-terra/core/v3/x/oracle/types"

Expand Down Expand Up @@ -117,5 +118,10 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) {
// reset miss counters of all validators at the last block of slash window
if core.IsPeriodLastBlock(ctx, params.SlashWindow) {
k.SlashAndResetMissCounters(ctx)
} else if core.IsPeriodLastBlock(ctx, util.BlocksPerHour*12) {
// Every ~12 hours, check if any validator has exceeded the maximum allowed misses
// and can no longer recover to meet MinValidPerWindow - slash them immediately
// rather than waiting until the end of SlashWindow
k.SlashExceedingMissCounters(ctx)
}
}
30 changes: 26 additions & 4 deletions x/oracle/keeper/slash.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// SlashAndResetMissCounters do slash any operator who over criteria & clear all operators miss counter to zero
func (k Keeper) SlashAndResetMissCounters(ctx sdk.Context) {
// SlashExceedingMissCounters slashes validators whose miss count has already exceeded the
// maximum allowed threshold, meaning they cannot possibly recover to meet MinValidPerWindow
// by the end of the current SlashWindow. This allows for early slashing/jailing rather than
// waiting until the end of the SlashWindow.
func (k Keeper) SlashExceedingMissCounters(ctx sdk.Context) {
height := ctx.BlockHeight()
distributionHeight := height - sdk.ValidatorUpdateDelay - 1

Expand All @@ -15,17 +18,24 @@ func (k Keeper) SlashAndResetMissCounters(ctx sdk.Context) {
QuoInt64(int64(k.VotePeriod(ctx))).
TruncateInt64(),
)

minValidPerWindow := k.MinValidPerWindow(ctx)
slashFraction := k.SlashFraction(ctx)
powerReduction := k.StakingKeeper.PowerReduction(ctx)

k.IterateMissCounters(ctx, func(operator sdk.ValAddress, missCounter uint64) bool {
// Calculate valid vote rate; (SlashWindow - MissCounter)/SlashWindow
// Calculate valid vote rate; (votePeriodsPerWindow - missCounter) / votePeriodsPerWindow
// This is the BEST CASE scenario assuming perfect voting for the rest of the window
if missCounter >= votePeriodsPerWindow {
// Already exceeded total periods - definitely slash
missCounter = votePeriodsPerWindow
}

validVoteRate := sdk.NewDecFromInt(
sdk.NewInt(int64(votePeriodsPerWindow - missCounter))).
QuoInt64(int64(votePeriodsPerWindow))

// Penalize the validator whose the valid vote rate is smaller than min threshold
// If even the best case valid vote rate is below threshold, validator cannot recover
if validVoteRate.LT(minValidPerWindow) {
validator := k.StakingKeeper.Validator(ctx, operator)
if validator.IsBonded() && !validator.IsJailed() {
Expand All @@ -42,6 +52,18 @@ func (k Keeper) SlashAndResetMissCounters(ctx sdk.Context) {
}
}

return false
})
}

// SlashAndResetMissCounters slashes any operator who is over the miss threshold
// and clears all operators' miss counters to zero at the end of the SlashWindow.
func (k Keeper) SlashAndResetMissCounters(ctx sdk.Context) {
// Slash validators who have exceeded the threshold
k.SlashExceedingMissCounters(ctx)

// Reset all miss counters
k.IterateMissCounters(ctx, func(operator sdk.ValAddress, _ uint64) bool {
k.DeleteMissCounter(ctx, operator)
return false
})
Expand Down
73 changes: 73 additions & 0 deletions x/oracle/keeper/slash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,76 @@ func TestSlashAndResetMissCounters(t *testing.T) {
validator, _ = input.StakingKeeper.GetValidator(input.Ctx, ValAddrs[0])
require.Equal(t, amt, validator.Tokens)
}

func TestSlashExceedingMissCounters(t *testing.T) {
// initial setup
input := CreateTestInput(t)
addr, val := ValAddrs[0], ValPubKeys[0]
addr1, val1 := ValAddrs[1], ValPubKeys[1]
amt := sdk.TokensFromConsensusPower(100, sdk.DefaultPowerReduction)
stakingMsgSvr := stakingkeeper.NewMsgServerImpl(input.StakingKeeper)
ctx := input.Ctx

// Validator created
_, err := stakingMsgSvr.CreateValidator(ctx, NewTestMsgCreateValidator(addr, val, amt))
require.NoError(t, err)
_, err = stakingMsgSvr.CreateValidator(ctx, NewTestMsgCreateValidator(addr1, val1, amt))
require.NoError(t, err)
staking.EndBlocker(ctx, input.StakingKeeper)

require.Equal(t, amt, input.StakingKeeper.Validator(ctx, addr).GetBondedTokens())
require.Equal(t, amt, input.StakingKeeper.Validator(ctx, addr1).GetBondedTokens())

votePeriodsPerWindow := sdk.NewDec(int64(input.OracleKeeper.SlashWindow(input.Ctx))).QuoInt64(int64(input.OracleKeeper.VotePeriod(input.Ctx))).TruncateInt64()
slashFraction := input.OracleKeeper.SlashFraction(input.Ctx)
minValidVotes := input.OracleKeeper.MinValidPerWindow(input.Ctx).MulInt64(votePeriodsPerWindow).TruncateInt64()

// Case 1: Miss counter at threshold boundary - should NOT slash (validator can still meet minValidPerWindow)
// If missCounter = votePeriodsPerWindow - minValidVotes, validVoteRate = minValidVotes/votePeriodsPerWindow = minValidPerWindow (exactly at threshold, not below)
input.OracleKeeper.SetMissCounter(input.Ctx, ValAddrs[0], uint64(votePeriodsPerWindow-minValidVotes))
input.OracleKeeper.SlashExceedingMissCounters(input.Ctx)
staking.EndBlocker(input.Ctx, input.StakingKeeper)

validator, _ := input.StakingKeeper.GetValidator(input.Ctx, ValAddrs[0])
require.Equal(t, amt, validator.GetBondedTokens())
require.False(t, validator.IsJailed())

// Case 2: Miss counter exceeds threshold by 1 - should slash (validVoteRate < minValidPerWindow, cannot recover)
input.OracleKeeper.SetMissCounter(input.Ctx, ValAddrs[0], uint64(votePeriodsPerWindow-minValidVotes+1))
input.OracleKeeper.SlashExceedingMissCounters(input.Ctx)

validator, _ = input.StakingKeeper.GetValidator(input.Ctx, ValAddrs[0])
require.Equal(t, amt.Sub(slashFraction.MulInt(amt).TruncateInt()), validator.GetBondedTokens())
require.True(t, validator.IsJailed())

// Case 3: Unbonded validator should not be slashed
validator, _ = input.StakingKeeper.GetValidator(input.Ctx, ValAddrs[1])
validator.Status = stakingtypes.Unbonded
validator.Jailed = false
validator.Tokens = amt
input.StakingKeeper.SetValidator(input.Ctx, validator)

input.OracleKeeper.SetMissCounter(input.Ctx, ValAddrs[1], uint64(votePeriodsPerWindow-minValidVotes+1))
input.OracleKeeper.SlashExceedingMissCounters(input.Ctx)

validator, _ = input.StakingKeeper.GetValidator(input.Ctx, ValAddrs[1])
require.Equal(t, amt, validator.Tokens)
require.False(t, validator.IsJailed())

// Case 4: Already jailed validator should not be slashed again
validator, _ = input.StakingKeeper.GetValidator(input.Ctx, ValAddrs[1])
validator.Status = stakingtypes.Bonded
validator.Jailed = true
validator.Tokens = amt
input.StakingKeeper.SetValidator(input.Ctx, validator)

input.OracleKeeper.SetMissCounter(input.Ctx, ValAddrs[1], uint64(votePeriodsPerWindow-minValidVotes+1))
input.OracleKeeper.SlashExceedingMissCounters(input.Ctx)

validator, _ = input.StakingKeeper.GetValidator(input.Ctx, ValAddrs[1])
require.Equal(t, amt, validator.Tokens)

// Case 5: Verify miss counter is NOT deleted (only SlashAndResetMissCounters deletes it)
missCounter := input.OracleKeeper.GetMissCounter(input.Ctx, ValAddrs[1])
require.Equal(t, uint64(votePeriodsPerWindow-minValidVotes+1), missCounter)
}
Loading