From 31df69f9373718e37e722057d82f931d2f3a032c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Brand=C3=A3o?= <37072140+guilherme-brandao@users.noreply.github.com> Date: Thu, 11 Apr 2024 21:28:42 +0200 Subject: [PATCH] Guilherme/ora 1168 do stake weighted binary selection then sort of reputer (#115) Co-authored-by: Kenny --- math/dec.go | 29 ++++- .../forecast_implied_inferences.go | 2 +- .../inference_synthesis/network_inferences.go | 12 +- .../inference_synthesis/network_losses.go | 2 +- x/emissions/keeper/keeper.go | 25 ++++ .../module/rewards/rewards_internal.go | 114 +++++++++++++----- .../module/rewards/rewards_internal_test.go | 44 ++++++- x/emissions/module/rewards/scores.go | 75 +++++++++++- x/emissions/module/rewards/worker_rewards.go | 2 +- 9 files changed, 263 insertions(+), 42 deletions(-) diff --git a/math/dec.go b/math/dec.go index 9199b248d..9b35d493d 100644 --- a/math/dec.go +++ b/math/dec.go @@ -21,7 +21,8 @@ import ( // but when copying the big.Int structure can be shared between Decimal instances causing corruption. // This was originally discovered in regen0-network/mainnet#15. type Dec struct { - dec apd.Decimal + dec apd.Decimal + isNaN bool } // constants for more convenient intent behind dec.Cmp values. @@ -61,6 +62,10 @@ var dec128Context = apd.Context{ Traps: apd.DefaultTraps, } +func NewNaN() Dec { + return Dec{apd.Decimal{}, true} +} + func NewDecFromString(s string) (Dec, error) { if s == "" { s = "0" @@ -70,7 +75,7 @@ func NewDecFromString(s string) (Dec, error) { return Dec{}, ErrInvalidDecString.Wrap(err.Error()) } - d1 := Dec{*d} + d1 := Dec{*d, false} if d1.dec.Form == apd.Infinite { return d1, ErrInfiniteString.Wrapf(s) } @@ -439,10 +444,30 @@ func (x Dec) Cmp(y Dec) int { return x.dec.Cmp(&y.dec) } +func (x Dec) Gt(y Dec) bool { + return x.dec.Cmp(&y.dec) == 1 +} + +func (x Dec) Gte(y Dec) bool { + return x.dec.Cmp(&y.dec) == 1 || x.dec.Cmp(&y.dec) == 0 +} + +func (x Dec) Lt(y Dec) bool { + return x.dec.Cmp(&y.dec) == -1 +} + +func (x Dec) Lte(y Dec) bool { + return x.dec.Cmp(&y.dec) == -1 || x.dec.Cmp(&y.dec) == 0 +} + func (x Dec) Equal(y Dec) bool { return x.dec.Cmp(&y.dec) == 0 } +func (x Dec) IsNaN() bool { + return x.isNaN +} + // IsZero returns true if the decimal is zero. func (x Dec) IsZero() bool { return x.dec.IsZero() diff --git a/x/emissions/keeper/inference_synthesis/forecast_implied_inferences.go b/x/emissions/keeper/inference_synthesis/forecast_implied_inferences.go index c7f85dc47..d8a639214 100644 --- a/x/emissions/keeper/inference_synthesis/forecast_implied_inferences.go +++ b/x/emissions/keeper/inference_synthesis/forecast_implied_inferences.go @@ -103,7 +103,7 @@ func CalcForcastImpliedInferences( maxjRijk = R_ik[j] first = false } else { - if R_ik[j].Cmp(maxjRijk) == alloraMath.GreaterThan { + if R_ik[j].Gt(maxjRijk) { maxjRijk = R_ik[j] } } diff --git a/x/emissions/keeper/inference_synthesis/network_inferences.go b/x/emissions/keeper/inference_synthesis/network_inferences.go index 046c190b6..bdd958d13 100644 --- a/x/emissions/keeper/inference_synthesis/network_inferences.go +++ b/x/emissions/keeper/inference_synthesis/network_inferences.go @@ -42,7 +42,7 @@ func FindMaxRegretAmongWorkersWithLosses( fmt.Println("Error getting inferer regret: ", err) return MaximalRegrets{}, err // TODO: THIS OR continue ?? } - if maxInfererRegret.Cmp(infererRegret.Value) == alloraMath.LessThan { + if maxInfererRegret.Lt(infererRegret.Value) { maxInfererRegret = infererRegret.Value } } @@ -54,7 +54,7 @@ func FindMaxRegretAmongWorkersWithLosses( fmt.Println("Error getting forecaster regret: ", err) return MaximalRegrets{}, err // TODO: THIS OR continue ?? } - if maxForecasterRegret.Cmp(forecasterRegret.Value) == alloraMath.LessThan { + if maxForecasterRegret.Lt(forecasterRegret.Value) { maxForecasterRegret = forecasterRegret.Value } } @@ -67,7 +67,7 @@ func FindMaxRegretAmongWorkersWithLosses( fmt.Println("Error getting forecaster regret: ", err) return MaximalRegrets{}, err // TODO: THIS OR continue ?? } - if maxOneInForecasterRegret[forecaster].Cmp(oneInForecasterRegret.Value) == alloraMath.LessThan { + if maxOneInForecasterRegret[forecaster].Lt(oneInForecasterRegret.Value) { maxOneInForecasterRegret[forecaster] = oneInForecasterRegret.Value } } @@ -76,7 +76,7 @@ func FindMaxRegretAmongWorkersWithLosses( fmt.Println("Error getting one-in forecaster self regret: ", err) return MaximalRegrets{}, err // TODO: THIS OR continue ?? } - if maxOneInForecasterRegret[forecaster].Cmp(oneInForecasterSelfRegret.Value) == alloraMath.LessThan { + if maxOneInForecasterRegret[forecaster].Lt(oneInForecasterSelfRegret.Value) { maxOneInForecasterRegret[forecaster] = oneInForecasterSelfRegret.Value } } @@ -103,7 +103,7 @@ func CalcWeightedInference( epsilon alloraMath.Dec, pInferenceSynthesis alloraMath.Dec, ) (InferenceValue, error) { - if maxRegret.Cmp(epsilon) == alloraMath.LessThan { + if maxRegret.Lt(epsilon) { fmt.Println("Error maxRegret < epsilon: ", maxRegret, epsilon) return InferenceValue{}, emissions.ErrFractionDivideByZero } @@ -185,7 +185,7 @@ func CalcWeightedInference( } // Normalize the network combined inference - if sumWeights.Cmp(epsilon) == alloraMath.LessThan { + if sumWeights.Lt(epsilon) { return InferenceValue{}, emissions.ErrSumWeightsLessThanEta } ret, err := unnormalizedI_i.Quo(sumWeights) diff --git a/x/emissions/keeper/inference_synthesis/network_losses.go b/x/emissions/keeper/inference_synthesis/network_losses.go index 908a50a0a..c95ce4f12 100644 --- a/x/emissions/keeper/inference_synthesis/network_losses.go +++ b/x/emissions/keeper/inference_synthesis/network_losses.go @@ -27,7 +27,7 @@ func RunningWeightedAvgUpdate( if err != nil { return WorkerRunningWeightedLoss{}, err } - if runningWeightedAvg.SumWeight.Cmp(epsilon) == alloraMath.LessThan { + if runningWeightedAvg.SumWeight.Lt(epsilon) { return *runningWeightedAvg, emissions.ErrFractionDivideByZero } weightFrac, err := weight.Quo(runningWeightedAvg.SumWeight) diff --git a/x/emissions/keeper/keeper.go b/x/emissions/keeper/keeper.go index 30a2ea072..c32662d57 100644 --- a/x/emissions/keeper/keeper.go +++ b/x/emissions/keeper/keeper.go @@ -764,6 +764,31 @@ func (k *Keeper) GetWorkerLatestInferenceByTopicId( return k.inferences.Get(ctx, key) } +// GetTopicWorkers returns a list of workers registered for a given topic ID. +func (k *Keeper) GetTopicWorkers(ctx context.Context, topicId TopicId) ([]sdk.AccAddress, error) { + var workers []sdk.AccAddress + + rng := collections.NewPrefixedPairRange[TopicId, Worker](topicId) + + // Iterate over the workers registered for the given topic ID + iter, err := k.topicWorkers.Iterate(ctx, rng) + if err != nil { + return nil, err + } + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + pair, err := iter.Key() + if err != nil { + return nil, err + } + workerAddr := pair.K2() + workers = append(workers, workerAddr) + } + + return workers, nil +} + // Returns the last block height at which rewards emissions were updated func (k *Keeper) GetLastRewardsUpdate(ctx context.Context) (int64, error) { lastRewardsUpdate, err := k.lastRewardsUpdate.Get(ctx) diff --git a/x/emissions/module/rewards/rewards_internal.go b/x/emissions/module/rewards/rewards_internal.go index d4f115f9e..a9ea317d7 100644 --- a/x/emissions/module/rewards/rewards_internal.go +++ b/x/emissions/module/rewards/rewards_internal.go @@ -2,6 +2,7 @@ package rewards import ( "math" + "sort" alloraMath "github.com/allora-network/allora-chain/math" "github.com/allora-network/allora-chain/x/emissions/types" @@ -277,57 +278,94 @@ func GetStakeWeightedLoss(reputersStakes, reputersReportedLosses []alloraMath.De func GetStakeWeightedLossMatrix( reputersAdjustedStakes []alloraMath.Dec, reputersReportedLosses [][]alloraMath.Dec, -) ([]alloraMath.Dec, error) { +) ([]alloraMath.Dec, []alloraMath.Dec, error) { if len(reputersAdjustedStakes) == 0 || len(reputersReportedLosses) == 0 { - return nil, types.ErrInvalidSliceLength + return nil, nil, types.ErrInvalidSliceLength } var err error = nil - // Calculate total stake for normalization - totalStake := alloraMath.ZeroDec() - for _, stake := range reputersAdjustedStakes { - totalStake, err = totalStake.Add(stake) - if err != nil { - return nil, err - } - } - // Ensure every loss array is non-empty and calculate geometric mean stakeWeightedLoss := make([]alloraMath.Dec, len(reputersReportedLosses[0])) + mostDistantValues := make([]alloraMath.Dec, len(reputersReportedLosses[0])) for j := 0; j < len(reputersReportedLosses[0]); j++ { + // Calculate total stake to consider + // Skip stakes of reputers with NaN losses + totalStakeToConsider := alloraMath.ZeroDec() + for i, losses := range reputersReportedLosses { + // Skip if loss is NaN + if losses[j].IsNaN() { + continue + } + + totalStakeToConsider, err = totalStakeToConsider.Add(reputersAdjustedStakes[i]) + if err != nil { + return nil, nil, err + } + } + logSum := alloraMath.ZeroDec() for i, losses := range reputersReportedLosses { + // Skip if loss is NaN + if losses[j].IsNaN() { + continue + } + logLosses, err := alloraMath.Log10(losses[j]) if err != nil { - return nil, err + return nil, nil, err } logLossesTimesStake, err := logLosses.Mul(reputersAdjustedStakes[i]) if err != nil { - return nil, err + return nil, nil, err } - logLossesTimesStakeOverTotalStake, err := logLossesTimesStake.Quo(totalStake) + logLossesTimesStakeOverTotalStake, err := logLossesTimesStake.Quo(totalStakeToConsider) if err != nil { - return nil, err + return nil, nil, err } logSum, err = logSum.Add(logLossesTimesStakeOverTotalStake) if err != nil { - return nil, err + return nil, nil, err } } ten := alloraMath.NewDecFromInt64(10) stakeWeightedLoss[j], err = alloraMath.Pow(ten, logSum) if err != nil { - return nil, err + return nil, nil, err + } + + // Find most distant value from consensus value + maxDistance, err := alloraMath.OneDec().Mul(alloraMath.MustNewDecFromString("-1")) // Initialize with an impossible value + if err != nil { + return nil, nil, err + } + for _, losses := range reputersReportedLosses { + // Skip if loss is NaN + if losses[j].IsNaN() { + continue + } + + logLosses, err := alloraMath.Log10(losses[j]) + if err != nil { + return nil, nil, err + } + distance, err := logSum.Sub(logLosses) + if err != nil { + return nil, nil, err + } + if distance.Gt(maxDistance) { + maxDistance = distance + mostDistantValues[j] = losses[j] + } } } - return stakeWeightedLoss, nil + return stakeWeightedLoss, mostDistantValues, nil } // GetConsensusScore calculates the proximity to consensus score for a reputer. // T_im -func GetConsensusScore(reputerLosses, consensusLosses []alloraMath.Dec) (alloraMath.Dec, error) { - fTolerance := alloraMath.MustNewDecFromString("0.01") +func GetConsensusScore(reputerLosses, consensusLosses, mostDistantValues []alloraMath.Dec) (alloraMath.Dec, error) { + fTolerance := alloraMath.MustNewDecFromString("0.01") // TODO: Use module param if len(reputerLosses) != len(consensusLosses) { return alloraMath.ZeroDec(), types.ErrInvalidSliceLength } @@ -355,6 +393,10 @@ func GetConsensusScore(reputerLosses, consensusLosses []alloraMath.Dec) (alloraM var distanceSquared alloraMath.Dec for i, rLoss := range reputerLosses { + // Attribute most distant value if loss is NaN + if rLoss.IsNaN() { + rLoss = mostDistantValues[i] + } rLossOverConsensusLoss, err := rLoss.Quo(consensusLosses[i]) if err != nil { return alloraMath.ZeroDec(), err @@ -363,7 +405,7 @@ func GetConsensusScore(reputerLosses, consensusLosses []alloraMath.Dec) (alloraM if err != nil { return alloraMath.ZeroDec(), err } - log10RLossOverCLossSquared, err := log10RLossOverCLoss.Mul(log10RLossOverCLoss) + log10RLossOverCLossSquared, err := log10RLossOverCLoss.Mul(log10RLossOverCLoss) // == Pow(x,2) if err != nil { return alloraMath.ZeroDec(), err } @@ -418,8 +460,8 @@ func GetAllConsensusScores( adjustedStakes = append(adjustedStakes, adjustedStake) } - // Get consensus loss vector - consensus, err := GetStakeWeightedLossMatrix(adjustedStakes, allLosses) + // Get consensus loss vector and retrieve most distant values from + consensus, mostDistantValues, err := GetStakeWeightedLossMatrix(adjustedStakes, allLosses) if err != nil { return nil, err } @@ -428,7 +470,7 @@ func GetAllConsensusScores( scores := make([]alloraMath.Dec, numReputers) for i := int64(0); i < numReputers; i++ { losses := allLosses[i] - scores[i], err = GetConsensusScore(losses, consensus) + scores[i], err = GetConsensusScore(losses, consensus, mostDistantValues) if err != nil { return nil, err } @@ -466,8 +508,7 @@ func GetAllReputersOutput( var maxGradient alloraMath.Dec = alloraMath.OneDec() finalScores := make([]alloraMath.Dec, numReputers) - for maxGradient.Cmp(maxGradientThreshold) == alloraMath.GreaterThan && - i.Cmp(imax) == alloraMath.LessThan { + for maxGradient.Gt(maxGradientThreshold) && i.Lt(imax) { i, err = i.Add(alloraMath.OneDec()) if err != nil { return nil, nil, err @@ -561,7 +602,7 @@ func GetAllReputersOutput( if err != nil { return nil, nil, err } - if listenedStakeFraction.Cmp(minStakeFraction) == alloraMath.LessThan { + if listenedStakeFraction.Lt(minStakeFraction) { for l := range coefficients { coeffDiff, err := coefficients[l].Sub(oldCoefficients[l]) if err != nil { @@ -644,7 +685,7 @@ func maxAbsDifference(a, b []alloraMath.Dec) (alloraMath.Dec, error) { return alloraMath.Dec{}, err } diff := subtraction.Abs() - if diff.Cmp(maxDiff) == alloraMath.GreaterThan { + if diff.Gt(maxDiff) { maxDiff = diff } } @@ -1113,19 +1154,34 @@ func ExtractValues(bundle *types.ValueBundle) []alloraMath.Dec { // Extract direct alloraMath.Dec values values = append(values, bundle.CombinedValue, bundle.NaiveValue) - // Extract values from slices of WorkerAttributedValue + // Sort and Extract values from slices of ValueBundle + sort.Slice(bundle.InfererValues, func(i, j int) bool { + return bundle.InfererValues[i].Worker < bundle.InfererValues[j].Worker + }) for _, v := range bundle.InfererValues { values = append(values, v.Value) } + sort.Slice(bundle.ForecasterValues, func(i, j int) bool { + return bundle.ForecasterValues[i].Worker < bundle.ForecasterValues[j].Worker + }) for _, v := range bundle.ForecasterValues { values = append(values, v.Value) } + sort.Slice(bundle.OneOutInfererValues, func(i, j int) bool { + return bundle.OneOutInfererValues[i].Worker < bundle.OneOutInfererValues[j].Worker + }) for _, v := range bundle.OneOutInfererValues { values = append(values, v.Value) } + sort.Slice(bundle.OneOutForecasterValues, func(i, j int) bool { + return bundle.OneOutForecasterValues[i].Worker < bundle.OneOutForecasterValues[j].Worker + }) for _, v := range bundle.OneOutForecasterValues { values = append(values, v.Value) } + sort.Slice(bundle.OneInForecasterValues, func(i, j int) bool { + return bundle.OneInForecasterValues[i].Worker < bundle.OneInForecasterValues[j].Worker + }) for _, v := range bundle.OneInForecasterValues { values = append(values, v.Value) } diff --git a/x/emissions/module/rewards/rewards_internal_test.go b/x/emissions/module/rewards/rewards_internal_test.go index db0ac56c6..2e35a421b 100644 --- a/x/emissions/module/rewards/rewards_internal_test.go +++ b/x/emissions/module/rewards/rewards_internal_test.go @@ -417,7 +417,49 @@ func TestGetStakeWeightedLossMatrix(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := rewards.GetStakeWeightedLossMatrix(tt.reputersAdjustedStakes, tt.reputersReportedLosses) + got, _, err := rewards.GetStakeWeightedLossMatrix(tt.reputersAdjustedStakes, tt.reputersReportedLosses) + + if (err != nil) != tt.wantErr { + t.Errorf("GetStakeWeightedLossMatrix() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !alloraMath.SlicesInDelta(got, tt.want, alloraMath.MustNewDecFromString("1e-5")) { + t.Errorf("GetStakeWeightedLossMatrix() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetStakeWeightedLossMatrixWithMissingLosses(t *testing.T) { + tests := []struct { + name string + reputersAdjustedStakes []alloraMath.Dec + reputersReportedLosses [][]alloraMath.Dec + want []alloraMath.Dec + wantErr bool + }{ + { + name: "basic", + reputersAdjustedStakes: []alloraMath.Dec{ + alloraMath.MustNewDecFromString("1.0"), + alloraMath.MustNewDecFromString("1.0"), + alloraMath.MustNewDecFromString("1.0"), + }, + reputersReportedLosses: [][]alloraMath.Dec{ + {alloraMath.MustNewDecFromString("1.0"), alloraMath.MustNewDecFromString("2.0"), alloraMath.MustNewDecFromString("3.0"), alloraMath.MustNewDecFromString("4.0")}, + {alloraMath.MustNewDecFromString("2.0"), alloraMath.NewNaN(), alloraMath.MustNewDecFromString("5.0"), alloraMath.MustNewDecFromString("3.0")}, + {alloraMath.NewNaN(), alloraMath.NewNaN(), alloraMath.MustNewDecFromString("1.0"), alloraMath.MustNewDecFromString("2.0")}, + }, + want: []alloraMath.Dec{ + alloraMath.MustNewDecFromString("1.41421"), alloraMath.MustNewDecFromString("2.00000"), alloraMath.MustNewDecFromString("2.46621"), alloraMath.MustNewDecFromString("2.88449"), + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _, err := rewards.GetStakeWeightedLossMatrix(tt.reputersAdjustedStakes, tt.reputersReportedLosses) if (err != nil) != tt.wantErr { t.Errorf("GetStakeWeightedLossMatrix() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/x/emissions/module/rewards/scores.go b/x/emissions/module/rewards/scores.go index 7e6eef00c..37b333fcb 100644 --- a/x/emissions/module/rewards/scores.go +++ b/x/emissions/module/rewards/scores.go @@ -22,7 +22,12 @@ func GenerateReputerScores( block int64, reportedLosses types.ReputerValueBundles, ) ([]types.Score, error) { - // Get reputers data + // Ensure all workers are present in the reported losses + // This is necessary to ensure that all workers are accounted for in the final scores + // If a worker is missing from the reported losses, it will be added with a NaN value + reportedLosses = ensureWorkerPresence(reportedLosses) + + // Fetch reputers data var reputerAddresses []sdk.AccAddress var reputerStakes []alloraMath.Dec var reputerListeningCoefficients []alloraMath.Dec @@ -185,3 +190,71 @@ func GenerateForecastScores( return newScores, nil } + +// Check if all workers are present in the reported losses and add NaN values for missing workers +// Returns the reported losses adding NaN values for missing workers in uncompleted reported losses +func ensureWorkerPresence(reportedLosses types.ReputerValueBundles) types.ReputerValueBundles { + // Consolidate all unique worker addresses from the three slices + allWorkersOneOutInferer := make(map[string]struct{}) + allWorkersOneOutForecaster := make(map[string]struct{}) + allWorkersOneInForecaster := make(map[string]struct{}) + + for _, bundle := range reportedLosses.ReputerValueBundles { + for _, workerValue := range bundle.ValueBundle.OneOutInfererValues { + allWorkersOneOutInferer[workerValue.Worker] = struct{}{} + } + for _, workerValue := range bundle.ValueBundle.OneOutForecasterValues { + allWorkersOneOutForecaster[workerValue.Worker] = struct{}{} + } + for _, workerValue := range bundle.ValueBundle.OneInForecasterValues { + allWorkersOneInForecaster[workerValue.Worker] = struct{}{} + } + } + + // Ensure each set has all workers, add NaN value for missing workers + for _, bundle := range reportedLosses.ReputerValueBundles { + bundle.ValueBundle.OneOutInfererValues = ensureAllWorkersPresentWithheld(bundle.ValueBundle.OneOutInfererValues, allWorkersOneOutInferer) + bundle.ValueBundle.OneOutForecasterValues = ensureAllWorkersPresentWithheld(bundle.ValueBundle.OneOutForecasterValues, allWorkersOneOutForecaster) + bundle.ValueBundle.OneInForecasterValues = ensureAllWorkersPresent(bundle.ValueBundle.OneInForecasterValues, allWorkersOneInForecaster) + } + + return reportedLosses +} + +// ensureAllWorkersPresent checks and adds missing workers with NaN values for a given slice of WorkerAttributedValue +func ensureAllWorkersPresent(values []*types.WorkerAttributedValue, allWorkers map[string]struct{}) []*types.WorkerAttributedValue { + foundWorkers := make(map[string]bool) + for _, value := range values { + foundWorkers[value.Worker] = true + } + + for worker := range allWorkers { + if !foundWorkers[worker] { + values = append(values, &types.WorkerAttributedValue{ + Worker: worker, + Value: alloraMath.NewNaN(), + }) + } + } + + return values +} + +// ensureAllWorkersPresentWithheld checks and adds missing workers with NaN values for a given slice of WithheldWorkerAttributedValue +func ensureAllWorkersPresentWithheld(values []*types.WithheldWorkerAttributedValue, allWorkers map[string]struct{}) []*types.WithheldWorkerAttributedValue { + foundWorkers := make(map[string]bool) + for _, value := range values { + foundWorkers[value.Worker] = true + } + + for worker := range allWorkers { + if !foundWorkers[worker] { + values = append(values, &types.WithheldWorkerAttributedValue{ + Worker: worker, + Value: alloraMath.NewNaN(), + }) + } + } + + return values +} diff --git a/x/emissions/module/rewards/worker_rewards.go b/x/emissions/module/rewards/worker_rewards.go index 72b7742db..9122aa7f1 100644 --- a/x/emissions/module/rewards/worker_rewards.go +++ b/x/emissions/module/rewards/worker_rewards.go @@ -146,7 +146,7 @@ func GetRewardsWithOutTax( if err != nil { continue } - if reward.Reward.Cmp(alloraMath.ZeroDec()) == alloraMath.LessThan { + if reward.Reward.Lt(alloraMath.ZeroDec()) { reward.Reward = alloraMath.ZeroDec() } result = append(result, TaskRewards{