diff --git a/x/oracle/abci.go b/x/oracle/abci.go index f013f9093..316838615 100644 --- a/x/oracle/abci.go +++ b/x/oracle/abci.go @@ -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 }) @@ -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) diff --git a/x/oracle/abci_test.go b/x/oracle/abci_test.go index 9ca789f35..edc2cb2a7 100644 --- a/x/oracle/abci_test.go +++ b/x/oracle/abci_test.go @@ -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) @@ -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) { @@ -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()) diff --git a/x/oracle/internal/keeper/slashing.go b/x/oracle/internal/keeper/slashing.go index e3cca88fe..4985b3c76 100644 --- a/x/oracle/internal/keeper/slashing.go +++ b/x/oracle/internal/keeper/slashing.go @@ -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) diff --git a/x/oracle/internal/keeper/slashing_test.go b/x/oracle/internal/keeper/slashing_test.go index 7d3ce2fa7..acb0bbf7d 100644 --- a/x/oracle/internal/keeper/slashing_test.go +++ b/x/oracle/internal/keeper/slashing_test.go @@ -36,4 +36,5 @@ func TestSlashAndResetMissCounters(t *testing.T) { input.OracleKeeper.SlashAndResetMissCounters(input.Ctx) validator = input.StakingKeeper.Validator(input.Ctx, ValAddrs[0]) require.Equal(t, amt.Sub(slashFraction.MulInt(amt).TruncateInt()), validator.GetBondedTokens()) + require.True(t, validator.IsJailed()) } diff --git a/x/oracle/internal/types/ballot.go b/x/oracle/internal/types/ballot.go index eab318f98..fea49ad24 100644 --- a/x/oracle/internal/types/ballot.go +++ b/x/oracle/internal/types/ballot.go @@ -13,18 +13,18 @@ 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) @@ -32,7 +32,7 @@ func (pb ExchangeRateBallot) WeightedMedian(ctx sdk.Context, sk StakingKeeper) s pivot := int64(0) for _, v := range pb { - votePower := v.getPower(ctx, sk) + votePower := v.getPower(ctx, powerMap) pivot += votePower if pivot >= (totalPower / 2) { @@ -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 { diff --git a/x/oracle/internal/types/ballot_test.go b/x/oracle/internal/types/ballot_test.go index f6b22abfb..e8a2552b6 100644 --- a/x/oracle/internal/types/ballot_test.go +++ b/x/oracle/internal/types/ballot_test.go @@ -29,6 +29,18 @@ 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) @@ -36,24 +48,26 @@ func TestPBPower(t *testing.T) { 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) { @@ -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)) } } @@ -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)) } } diff --git a/x/oracle/internal/types/expected_keeper.go b/x/oracle/internal/types/expected_keeper.go index d6602b72b..a27d45d09 100644 --- a/x/oracle/internal/types/expected_keeper.go +++ b/x/oracle/internal/types/expected_keeper.go @@ -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)) } diff --git a/x/oracle/internal/types/test_utils.go b/x/oracle/internal/types/test_utils.go index 55b38f3ba..53dd54d62 100644 --- a/x/oracle/internal/types/test_utils.go +++ b/x/oracle/internal/types/test_utils.go @@ -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 diff --git a/x/oracle/internal/types/vote.go b/x/oracle/internal/types/vote.go index ef74f0aa8..1ef32ea11 100644 --- a/x/oracle/internal/types/vote.go +++ b/x/oracle/internal/types/vote.go @@ -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 diff --git a/x/oracle/tally.go b/x/oracle/tally.go index b5c787acc..b4194cfc6 100644 --- a/x/oracle/tally.go +++ b/x/oracle/tally.go @@ -9,14 +9,14 @@ 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 @@ -24,10 +24,8 @@ func tally(ctx sdk.Context, pb types.ExchangeRateBallot, k Keeper) (weightedMedi 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, @@ -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) }