diff --git a/x/oracle/abci.go b/x/oracle/abci.go index 349559ab45..ab80d93a5e 100755 --- a/x/oracle/abci.go +++ b/x/oracle/abci.go @@ -39,14 +39,17 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) { } voteTargets := make(map[string]types.Denom) + totalTargets := 0 k.IterateVoteTargets(ctx, func(denom string, denomInfo types.Denom) bool { voteTargets[denom] = denomInfo + totalTargets++ return false }) // Clear all exchange rates k.IterateBaseExchangeRates(ctx, func(denom string, _ sdk.Dec) (stop bool) { - k.DeleteBaseExchangeRate(ctx, denom) + // TODO: replace this with an indicator of staleness + // k.DeleteBaseExchangeRate(ctx, denom) return false }) @@ -54,15 +57,15 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) { // NOTE: **Filter out inactive or jailed validators** // NOTE: **Make abstain votes to have zero vote power** voteMap := k.OrganizeBallotByDenom(ctx, validatorClaimMap) + // belowThresholdVoteMap has assets that failed to meet threshold + referenceDenom, belowThresholdVoteMap := pickReferenceDenom(ctx, k, voteTargets, voteMap) - if referenceDenom := pickReferenceDenom(ctx, k, voteTargets, voteMap); referenceDenom != "" { + if referenceDenom != "" { // make voteMap of Reference denom to calculate cross exchange rates ballotRD := voteMap[referenceDenom] voteMapRD := ballotRD.ToMap() - var exchangeRateRD sdk.Dec - - exchangeRateRD = ballotRD.WeightedMedianWithAssertion() + var exchangeRateRD sdk.Dec = ballotRD.WeightedMedianWithAssertion() // Iterate through ballots and update exchange rates; drop if not enough votes have been achieved. for denom, ballot := range voteMap { @@ -84,14 +87,26 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) { // Set the exchange rate, emit ABCI event k.SetBaseExchangeRateWithEvent(ctx, denom, exchangeRate) } + + for _, ballot := range belowThresholdVoteMap { + // perform tally for below threshold assets to calculate total win count + Tally(ctx, ballot, params.RewardBand, validatorClaimMap) + } + } else { + // in this case, all assets would be in the belowThresholdVoteMap + for _, ballot := range belowThresholdVoteMap { + // perform tally for below threshold assets to calculate total win count + Tally(ctx, ballot, params.RewardBand, validatorClaimMap) + } } //--------------------------- // Do miss counting & slashing - voteTargetsLen := len(voteTargets) for _, claim := range validatorClaimMap { // Skip abstain & valid voters - if int(claim.WinCount) == voteTargetsLen { + // we require validator to have submitted in-range data + // for all assets to not be counted as a miss + if int(claim.WinCount) == totalTargets { continue } @@ -111,6 +126,4 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) { if utils.IsPeriodLastBlock(ctx, params.SlashWindow) { k.SlashAndResetMissCounters(ctx) } - - return } diff --git a/x/oracle/abci_test.go b/x/oracle/abci_test.go index 6ca11d4b7f..9da525017a 100755 --- a/x/oracle/abci_test.go +++ b/x/oracle/abci_test.go @@ -104,7 +104,9 @@ func TestOracleThreshold(t *testing.T) { oracle.EndBlocker(input.Ctx.WithBlockHeight(1), input.OracleKeeper) _, err = input.OracleKeeper.GetBaseExchangeRate(input.Ctx.WithBlockHeight(1), utils.MicroAtomDenom) - require.Error(t, err) + require.NoError(t, err) + require.Equal(t, randomExchangeRate, rate) + // TODO: add check for staleness } func TestOracleDrop(t *testing.T) { @@ -118,8 +120,10 @@ func TestOracleDrop(t *testing.T) { // Immediately swap halt after an illiquid oracle vote oracle.EndBlocker(input.Ctx, input.OracleKeeper) - _, err := input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom) - require.Error(t, err) + rate, err := input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom) + require.NoError(t, err) + require.Equal(t, randomExchangeRate, rate) + // TODO: add check for staleness } func TestOracleTally(t *testing.T) { @@ -326,8 +330,124 @@ func TestNotPassedBallotSlashing(t *testing.T) { oracle.EndBlocker(input.Ctx, input.OracleKeeper) require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[0])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[2])) +} + +func TestNotPassedBallotSlashingInvalidVotes(t *testing.T) { + input, h := setupN(t, 7) + params := input.OracleKeeper.GetParams(input.Ctx) + params.Whitelist = types.DenomList{{Name: utils.MicroAtomDenom}} + input.OracleKeeper.SetParams(input.Ctx, params) + + input.OracleKeeper.ClearVoteTargets(input.Ctx) + input.OracleKeeper.SetVoteTarget(input.Ctx, utils.MicroAtomDenom) + + input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1) + + // Account 1 + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 0) + // Account 2 + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 1) + // Account 3 + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate.Add(sdk.NewDec(100000000000000))}}, 2) + + oracle.EndBlocker(input.Ctx, input.OracleKeeper) + + // 4-7 should be missed due to not voting + // 3 should be missed due to out of bounds + require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[0])) + require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[2])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[3])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[4])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[5])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[6])) +} + +func TestInvalidVoteOnAssetUnderThresholdMisses(t *testing.T) { + input, h := setupN(t, 7) + params := input.OracleKeeper.GetParams(input.Ctx) + params.Whitelist = types.DenomList{{Name: utils.MicroAtomDenom}, {Name: utils.MicroEthDenom}} + input.OracleKeeper.SetParams(input.Ctx, params) + + input.OracleKeeper.ClearVoteTargets(input.Ctx) + input.OracleKeeper.SetVoteTarget(input.Ctx, utils.MicroAtomDenom) + input.OracleKeeper.SetVoteTarget(input.Ctx, utils.MicroEthDenom) + + input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1) + + // Account 1 + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 0) + // Account 2 + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 1) + // Account 3 + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 2) + + // rest of accounts + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 3) + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: randomExchangeRate}}, 4) + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 5) + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 6) + + oracle.EndBlocker(input.Ctx, input.OracleKeeper) + + // 6 and 7 should be missed due to not voting on second asset + require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[0])) require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1])) require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[2])) + require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[3])) + require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[4])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[5])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[6])) + + input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1) + + rate, err := input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom) + require.NoError(t, err) + require.Equal(t, randomExchangeRate, rate) + + rate, err = input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroEthDenom) + require.NoError(t, err) + require.Equal(t, randomExchangeRate, rate) + + input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1) + + // Account 1 + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: anotherRandomExchangeRate}}, 0) + // Account 2 + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: anotherRandomExchangeRate}}, 1) + // Account 3 + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}, {Denom: utils.MicroEthDenom, Amount: anotherRandomExchangeRate.Add(sdk.NewDec(100000000000000))}}, 2) + + // rest of accounts meet threshold only for one asset + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}}, 3) + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}}, 4) + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}}, 5) + makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: anotherRandomExchangeRate}}, 6) + + oracle.EndBlocker(input.Ctx, input.OracleKeeper) + + // 4-7 should be missed due to not voting on second asset + // 3 should have missed due to out of bounds value even though it didnt meet voting threshold + require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[0])) + require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[2])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[3])) + require.Equal(t, uint64(1), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[4])) + require.Equal(t, uint64(2), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[5])) + require.Equal(t, uint64(2), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[6])) + + input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1) + + rate, err = input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom) + require.NoError(t, err) + require.Equal(t, anotherRandomExchangeRate, rate) + + // the old value should be persisted because asset didnt meet ballot threshold + rate, err = input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroEthDenom) + require.NoError(t, err) + require.Equal(t, randomExchangeRate, rate) } func TestAbstainSlashing(t *testing.T) { @@ -341,25 +461,29 @@ func TestAbstainSlashing(t *testing.T) { votePeriodsPerWindow := sdk.NewDec(int64(input.OracleKeeper.SlashWindow(input.Ctx))).QuoInt64(int64(input.OracleKeeper.VotePeriod(input.Ctx))).TruncateInt64() minValidPerWindow := input.OracleKeeper.MinValidPerWindow(input.Ctx) + slashFraction := input.OracleKeeper.SlashFraction(input.Ctx) - for i := uint64(0); i <= uint64(sdk.OneDec().Sub(minValidPerWindow).MulInt64(votePeriodsPerWindow).TruncateInt64()); i++ { + limit := uint64(sdk.OneDec().Sub(minValidPerWindow).MulInt64(votePeriodsPerWindow).TruncateInt64()) + for i := uint64(0); i <= limit; i++ { input.Ctx = input.Ctx.WithBlockHeight(input.Ctx.BlockHeight() + 1) // Account 1, KRW makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 0) - // Account 2, KRW, abstain vote + // Account 2, KRW, abstain vote - should count as miss makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.ZeroDec()}}, 1) // Account 3, KRW makeAggregatePrevoteAndVote(t, input, h, 0, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: randomExchangeRate}}, 2) oracle.EndBlocker(input.Ctx, input.OracleKeeper) - require.Equal(t, uint64(0), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1])) + require.Equal(t, uint64(i+1%limit), input.OracleKeeper.GetMissCounter(input.Ctx, keeper.ValAddrs[1])) } + input.Ctx = input.Ctx.WithBlockHeight(votePeriodsPerWindow - 1) + oracle.EndBlocker(input.Ctx, input.OracleKeeper) validator := input.StakingKeeper.Validator(input.Ctx, keeper.ValAddrs[1]) - require.Equal(t, stakingAmt, validator.GetBondedTokens()) + require.Equal(t, sdk.OneDec().Sub(slashFraction).MulInt(stakingAmt).TruncateInt(), validator.GetBondedTokens()) } func TestVoteTargets(t *testing.T) { diff --git a/x/oracle/common_test.go b/x/oracle/common_test.go index cc22200445..c61d8da41a 100755 --- a/x/oracle/common_test.go +++ b/x/oracle/common_test.go @@ -85,3 +85,25 @@ func setupVal5(t *testing.T) (keeper.TestInput, sdk.Handler) { return input, h } + +func setupN(t *testing.T, num int) (keeper.TestInput, sdk.Handler) { + input := keeper.CreateTestInput(t) + params := input.OracleKeeper.GetParams(input.Ctx) + params.VotePeriod = 1 + params.SlashWindow = 100 + input.OracleKeeper.SetParams(input.Ctx, params) + h := oracle.NewHandler(input.OracleKeeper) + + sh := staking.NewHandler(input.StakingKeeper) + + require.LessOrEqual(t, num, len(keeper.ValAddrs)) + + // Validator created + for i := 0; i < num; i++ { + _, err := sh(input.Ctx, keeper.NewTestMsgCreateValidator(keeper.ValAddrs[i], keeper.ValPubKeys[i], stakingAmt)) + require.NoError(t, err) + } + staking.EndBlocker(input.Ctx, input.StakingKeeper) + + return input, h +} diff --git a/x/oracle/keeper/test_utils.go b/x/oracle/keeper/test_utils.go index 21292accb8..f43935fcac 100755 --- a/x/oracle/keeper/test_utils.go +++ b/x/oracle/keeper/test_utils.go @@ -83,7 +83,7 @@ func MakeEncodingConfig(_ *testing.T) simparams.EncodingConfig { // Test addresses var ( - ValPubKeys = simapp.CreateTestPubKeys(5) + ValPubKeys = simapp.CreateTestPubKeys(7) pubKeys = []crypto.PubKey{ secp256k1.GenPrivKey().PubKey(), @@ -91,6 +91,8 @@ var ( secp256k1.GenPrivKey().PubKey(), secp256k1.GenPrivKey().PubKey(), secp256k1.GenPrivKey().PubKey(), + secp256k1.GenPrivKey().PubKey(), + secp256k1.GenPrivKey().PubKey(), } Addrs = []sdk.AccAddress{ @@ -99,6 +101,8 @@ var ( sdk.AccAddress(pubKeys[2].Address()), sdk.AccAddress(pubKeys[3].Address()), sdk.AccAddress(pubKeys[4].Address()), + sdk.AccAddress(pubKeys[5].Address()), + sdk.AccAddress(pubKeys[6].Address()), } ValAddrs = []sdk.ValAddress{ @@ -107,6 +111,8 @@ var ( sdk.ValAddress(pubKeys[2].Address()), sdk.ValAddress(pubKeys[3].Address()), sdk.ValAddress(pubKeys[4].Address()), + sdk.ValAddress(pubKeys[5].Address()), + sdk.ValAddress(pubKeys[6].Address()), } InitTokens = sdk.TokensFromConsensusPower(200, sdk.DefaultPowerReduction) diff --git a/x/oracle/tally.go b/x/oracle/tally.go index da8bd8ef3b..187572e6d2 100755 --- a/x/oracle/tally.go +++ b/x/oracle/tally.go @@ -21,10 +21,10 @@ func Tally(ctx sdk.Context, pb types.ExchangeRateBallot, rewardBand sdk.Dec, val } for _, vote := range pb { - // Filter ballot winners & abstain voters - if (vote.ExchangeRate.GTE(weightedMedian.Sub(rewardSpread)) && - vote.ExchangeRate.LTE(weightedMedian.Add(rewardSpread))) || - !vote.ExchangeRate.IsPositive() { + // Filter ballot winners + // abstaining counts as out of range and will be eventually penalized + if vote.ExchangeRate.GTE(weightedMedian.Sub(rewardSpread)) && + vote.ExchangeRate.LTE(weightedMedian.Add(rewardSpread)) { key := vote.Voter.String() claim := validatorClaimMap[key] @@ -46,9 +46,10 @@ func ballotIsPassing(ballot types.ExchangeRateBallot, thresholdVotes sdk.Int) (s // choose reference denom with the highest voter turnout // If the voting power of the two denominations is the same, // select reference denom in alphabetical order. -func pickReferenceDenom(ctx sdk.Context, k keeper.Keeper, voteTargets map[string]types.Denom, voteMap map[string]types.ExchangeRateBallot) string { +func pickReferenceDenom(ctx sdk.Context, k keeper.Keeper, voteTargets map[string]types.Denom, voteMap map[string]types.ExchangeRateBallot) (referenceDenom string, belowThresholdVoteMap map[string]types.ExchangeRateBallot) { largestBallotPower := int64(0) - referenceDenom := "" + referenceDenom = "" + belowThresholdVoteMap = map[string]types.ExchangeRateBallot{} totalBondedPower := sdk.TokensToConsensusPower(k.StakingKeeper.TotalBondedTokens(ctx), k.StakingKeeper.PowerReduction(ctx)) voteThreshold := k.VoteThreshold(ctx) @@ -69,6 +70,8 @@ func pickReferenceDenom(ctx sdk.Context, k keeper.Keeper, voteTargets map[string if power, ok := ballotIsPassing(ballot, thresholdVotes); ok { ballotPower = power.Int64() } else { + // add assets below threshold to separate map for tally evaluation + belowThresholdVoteMap[denom] = voteMap[denom] delete(voteTargets, denom) delete(voteMap, denom) continue @@ -82,5 +85,5 @@ func pickReferenceDenom(ctx sdk.Context, k keeper.Keeper, voteTargets map[string } } - return referenceDenom + return }