Skip to content

Commit

Permalink
jail the validator after oracle slashing
Browse files Browse the repository at this point in the history
  • Loading branch information
Yun committed Nov 12, 2019
1 parent f6a3b99 commit 5006cad
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 34 deletions.
17 changes: 12 additions & 5 deletions x/oracle/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ func EndBlocker(ctx sdk.Context, k Keeper) {
// Build valid votes counter and winner map over all validators in active set
validVotesCounterMap := make(map[string]int64)
winnerMap := make(map[string]types.Claim)
powerMap := make(map[string]int64)
k.StakingKeeper.IterateValidators(ctx, func(index int64, validator exported.ValidatorI) bool {
valAddr := validator.GetOperator()
validVotesCounterMap[valAddr.String()] = int64(0)
winnerMap[valAddr.String()] = types.NewClaim(0, valAddr)

// Exclude not bonded vaildator or jailed validators from tallying
if validator.IsBonded() && !validator.IsJailed() {
valAddr := validator.GetOperator()
validVotesCounterMap[valAddr.String()] = int64(0)
winnerMap[valAddr.String()] = types.NewClaim(0, valAddr)
powerMap[valAddr.String()] = validator.GetConsensusPower()
}

return false
})

Expand Down Expand Up @@ -55,12 +62,12 @@ func EndBlocker(ctx sdk.Context, k Keeper) {
for denom, ballot := range voteMap {

// If denom is not in the whitelist, or the ballot for it has failed, then skip
if _, exists := whitelist[denom]; !exists || !ballotIsPassing(ctx, ballot, k) {
if _, exists := whitelist[denom]; !exists || !ballotIsPassing(ctx, ballot, k, powerMap) {
continue
}

// Get weighted median exchange rates, and faithful respondants
ballotMedian, ballotWinningClaims := tally(ctx, ballot, k)
ballotMedian, ballotWinningClaims := tally(ctx, ballot, powerMap, params.RewardBand)

// Set the exchange rate
k.SetLunaExchangeRate(ctx, denom, ballotMedian)
Expand Down
20 changes: 17 additions & 3 deletions x/oracle/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import (
"github.com/terra-project/core/x/oracle/internal/types"
)

func buildPowerMap(sk types.DummyStakingKeeper) map[string]int64 {
powerMap := make(map[string]int64)
for _, validator := range sk.Validators() {
if validator.IsBonded() && !validator.IsJailed() {
valAddr := validator.GetOperator()
powerMap[valAddr.String()] = validator.GetConsensusPower()
}
}

return powerMap
}

func TestOracleThreshold(t *testing.T) {
input, h := setup(t)

Expand Down Expand Up @@ -230,9 +242,11 @@ func TestOracleTally(t *testing.T) {
}
}

powerMap := buildPowerMap(stakingKeeper)

rewardees := []sdk.AccAddress{}
weightedMedian := ballot.WeightedMedian(input.Ctx, stakingKeeper)
standardDeviation := ballot.StandardDeviation(input.Ctx, stakingKeeper)
weightedMedian := ballot.WeightedMedian(input.Ctx, powerMap)
standardDeviation := ballot.StandardDeviation(input.Ctx, powerMap)
maxSpread := input.OracleKeeper.RewardBand(input.Ctx).QuoInt64(2)

if standardDeviation.GT(maxSpread) {
Expand All @@ -245,7 +259,7 @@ func TestOracleTally(t *testing.T) {
}
}

tallyMedian, ballotWinner := tally(input.Ctx, ballot, input.OracleKeeper)
tallyMedian, ballotWinner := tally(input.Ctx, ballot, powerMap, input.OracleKeeper.RewardBand(input.Ctx))

require.Equal(t, len(rewardees), len(ballotWinner))
require.Equal(t, tallyMedian.MulInt64(100).TruncateInt(), weightedMedian.MulInt64(100).TruncateInt())
Expand Down
1 change: 1 addition & 0 deletions x/oracle/internal/keeper/slashing.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (k Keeper) SlashAndResetMissCounters(ctx sdk.Context) {
ctx, validator.GetConsAddr(),
distributionHeight, validator.GetConsensusPower(), slashFraction,
)
k.StakingKeeper.Jail(ctx, validator.GetConsAddr())
}

k.SetMissCounter(ctx, operator, 0)
Expand Down
14 changes: 7 additions & 7 deletions x/oracle/internal/types/ballot.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,26 @@ import (
type ExchangeRateBallot []ExchangeRateVote

// Power returns the total amount of voting power in the ballot
func (pb ExchangeRateBallot) Power(ctx sdk.Context, sk StakingKeeper) int64 {
func (pb ExchangeRateBallot) Power(ctx sdk.Context, powerMap map[string]int64) int64 {
totalPower := int64(0)
for _, vote := range pb {
totalPower += vote.getPower(ctx, sk)
totalPower += vote.getPower(ctx, powerMap)
}

return totalPower
}

// WeightedMedian returns the median weighted by the power of the ExchangeRateVote.
func (pb ExchangeRateBallot) WeightedMedian(ctx sdk.Context, sk StakingKeeper) sdk.Dec {
totalPower := pb.Power(ctx, sk)
func (pb ExchangeRateBallot) WeightedMedian(ctx sdk.Context, powerMap map[string]int64) sdk.Dec {
totalPower := pb.Power(ctx, powerMap)
if pb.Len() > 0 {
if !sort.IsSorted(pb) {
sort.Sort(pb)
}

pivot := int64(0)
for _, v := range pb {
votePower := v.getPower(ctx, sk)
votePower := v.getPower(ctx, powerMap)

pivot += votePower
if pivot >= (totalPower / 2) {
Expand All @@ -44,12 +44,12 @@ func (pb ExchangeRateBallot) WeightedMedian(ctx sdk.Context, sk StakingKeeper) s
}

// StandardDeviation returns the standard deviation by the power of the ExchangeRateVote.
func (pb ExchangeRateBallot) StandardDeviation(ctx sdk.Context, sk StakingKeeper) (standardDeviation sdk.Dec) {
func (pb ExchangeRateBallot) StandardDeviation(ctx sdk.Context, powerMap map[string]int64) (standardDeviation sdk.Dec) {
if len(pb) == 0 {
return sdk.ZeroDec()
}

median := pb.WeightedMedian(ctx, sk)
median := pb.WeightedMedian(ctx, powerMap)

sum := sdk.ZeroDec()
for _, v := range pb {
Expand Down
26 changes: 21 additions & 5 deletions x/oracle/internal/types/ballot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,45 @@ func TestSqrt(t *testing.T) {
require.Equal(t, sdk.NewDecWithPrec(12, 2), num)
}

func buildPowerMap(sk DummyStakingKeeper) map[string]int64 {
powerMap := make(map[string]int64)
for _, validator := range sk.Validators() {
if validator.IsBonded() && !validator.IsJailed() {
valAddr := validator.GetOperator()
powerMap[valAddr.String()] = validator.GetConsensusPower()
}
}

return powerMap
}

func TestPBPower(t *testing.T) {

ctx := sdk.NewContext(nil, abci.Header{}, false, nil)
_, valAccAddrs, sk := GenerateRandomTestCase()
pb := ExchangeRateBallot{}
ballotPower := int64(0)

powerMap := buildPowerMap(sk)

for i := 0; i < len(sk.Validators()); i++ {
vote := NewExchangeRateVote(sdk.ZeroDec(), core.MicroSDRDenom, sdk.ValAddress(valAccAddrs[i]))
pb = append(pb, vote)

valPower := vote.getPower(ctx, sk)
valPower := vote.getPower(ctx, powerMap)
require.NotEqual(t, int64(0), valPower)

ballotPower += valPower
}

require.Equal(t, ballotPower, pb.Power(ctx, sk))
require.Equal(t, ballotPower, pb.Power(ctx, powerMap))

// Mix in a fake validator, the total power should not have changed.
pubKey := secp256k1.GenPrivKey().PubKey()
faceValAddr := sdk.ValAddress(pubKey.Address())
fakeVote := NewExchangeRateVote(sdk.OneDec(), core.MicroSDRDenom, faceValAddr)
pb = append(pb, fakeVote)
require.Equal(t, ballotPower, pb.Power(ctx, sk))
require.Equal(t, ballotPower, pb.Power(ctx, powerMap))
}

func TestPBWeightedMedian(t *testing.T) {
Expand Down Expand Up @@ -111,9 +125,10 @@ func TestPBWeightedMedian(t *testing.T) {
}

sk := NewDummyStakingKeeper(mockValset)
powerMap := buildPowerMap(sk)

ctx := sdk.NewContext(nil, abci.Header{}, false, nil)
require.Equal(t, tc.median, pb.WeightedMedian(ctx, sk))
require.Equal(t, tc.median, pb.WeightedMedian(ctx, powerMap))
}
}

Expand Down Expand Up @@ -172,9 +187,10 @@ func TestPBStandardDeviation(t *testing.T) {
}

sk := NewDummyStakingKeeper(mockValset)
powerMap := buildPowerMap(sk)

ctx := sdk.NewContext(nil, abci.Header{}, false, nil)
require.Equal(t, tc.standardDeviation, pb.StandardDeviation(ctx, sk))
require.Equal(t, tc.standardDeviation, pb.StandardDeviation(ctx, powerMap))
}
}

Expand Down
1 change: 1 addition & 0 deletions x/oracle/internal/types/expected_keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type StakingKeeper interface {
Validator(ctx sdk.Context, address sdk.ValAddress) stakingexported.ValidatorI // get validator by operator address; nil when validator not found
TotalBondedTokens(sdk.Context) sdk.Int // total bonded tokens within the validator set
Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec) // slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction
Jail(sdk.Context, sdk.ConsAddress) // jail a validator
IterateValidators(sdk.Context, func(index int64, validator stakingexported.ValidatorI) (stop bool))
}

Expand Down
4 changes: 4 additions & 0 deletions x/oracle/internal/types/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ func (DummyStakingKeeper) Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.
func (DummyStakingKeeper) IterateValidators(sdk.Context, func(index int64, validator exported.ValidatorI) (stop bool)) {
}

// Jail nolint
func (DummyStakingKeeper) Jail(sdk.Context, sdk.ConsAddress) {
}

type MockValidator struct {
power int64
operator sdk.ValAddress
Expand Down
9 changes: 4 additions & 5 deletions x/oracle/internal/types/vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,12 @@ func NewExchangeRateVote(rate sdk.Dec, denom string, voter sdk.ValAddress) Excha
}
}

func (pv ExchangeRateVote) getPower(ctx sdk.Context, sk StakingKeeper) int64 {
validator := sk.Validator(ctx, pv.Voter)
if validator == nil {
return 0
func (pv ExchangeRateVote) getPower(ctx sdk.Context, powerMap map[string]int64) int64 {
if power, ok := powerMap[pv.Voter.String()]; ok {
return power
}

return validator.GetConsensusPower()
return 0
}

// String implements fmt.Stringer interface
Expand Down
16 changes: 7 additions & 9 deletions x/oracle/tally.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,23 @@ import (

// Calculates the median and returns it. Sets the set of voters to be rewarded, i.e. voted within
// a reasonable spread from the weighted median to the store
func tally(ctx sdk.Context, pb types.ExchangeRateBallot, k Keeper) (weightedMedian sdk.Dec, ballotWinners []types.Claim) {
func tally(ctx sdk.Context, pb types.ExchangeRateBallot, powerMap map[string]int64, rewardBand sdk.Dec) (weightedMedian sdk.Dec, ballotWinners []types.Claim) {
if !sort.IsSorted(pb) {
sort.Sort(pb)
}

weightedMedian = pb.WeightedMedian(ctx, k.StakingKeeper)
standardDeviation := pb.StandardDeviation(ctx, k.StakingKeeper)
rewardSpread := k.RewardBand(ctx).QuoInt64(2)
weightedMedian = pb.WeightedMedian(ctx, powerMap)
standardDeviation := pb.StandardDeviation(ctx, powerMap)
rewardSpread := rewardBand.QuoInt64(2)

if standardDeviation.GT(rewardSpread) {
rewardSpread = standardDeviation
}

for _, vote := range pb {
// If a validator is not found, then just ignore the vote
if validator := k.StakingKeeper.Validator(ctx, vote.Voter); validator != nil {
if power, ok := powerMap[vote.Voter.String()]; ok {
if vote.ExchangeRate.GTE(weightedMedian.Sub(rewardSpread)) && vote.ExchangeRate.LTE(weightedMedian.Add(rewardSpread)) {
power := validator.GetConsensusPower()

ballotWinners = append(ballotWinners, types.Claim{
Recipient: vote.Voter,
Weight: power,
Expand All @@ -40,10 +38,10 @@ func tally(ctx sdk.Context, pb types.ExchangeRateBallot, k Keeper) (weightedMedi
}

// ballot for the asset is passing the threshold amount of voting power
func ballotIsPassing(ctx sdk.Context, ballot types.ExchangeRateBallot, k Keeper) bool {
func ballotIsPassing(ctx sdk.Context, ballot types.ExchangeRateBallot, k Keeper, powerMap map[string]int64) bool {
totalBondedPower := sdk.TokensToConsensusPower(k.StakingKeeper.TotalBondedTokens(ctx))
voteThreshold := k.VoteThreshold(ctx)
thresholdVotes := voteThreshold.MulInt64(totalBondedPower).RoundInt()
ballotPower := sdk.NewInt(ballot.Power(ctx, k.StakingKeeper))
ballotPower := sdk.NewInt(ballot.Power(ctx, powerMap))
return ballotPower.GTE(thresholdVotes)
}

0 comments on commit 5006cad

Please sign in to comment.