Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sortition penalties #712

Merged
merged 15 commits into from
Dec 19, 2024
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#703](https://github.com/allora-network/allora-chain/pull/703) Add outlier detection to inferences
* [#714](https://github.com/allora-network/allora-chain/pull/714) Add initialization of actors' EMA scores
* [#716](https://github.com/allora-network/allora-chain/pull/716) Add global workers, reputers, admins + bulk operations
* [#712](https://github.com/allora-network/allora-chain/pull/712) Apply sortition penalties based on liveness

### Changed

Expand Down
55 changes: 54 additions & 1 deletion math/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,59 @@ func CalcEma(
return ret, nil
}

// NCalcEma is a generalisation of calculating the EMA update n times.
// This function computes = (1-α)^n * x + (1-(1-α)^n) * asymptote
// As `n` approaches infinity, the function converges to the asymptote.
// For smaller `n`, the function returns a value closer to `x`, as much as `alpha` affords.
func NCalcEma(
alpha,
asymptote,
x Dec,
n uint64,
) (Dec, error) {
if asymptote.isNaN {
return ZeroDec(), errorsmod.Wrap(ErrNaN, "NCalcEma asymptote EMA operand should not be NaN")
}
if x.isNaN {
return ZeroDec(), errorsmod.Wrap(ErrNaN, "NCalcEma x EMA operand should not be NaN")
}
if alpha.isNaN {
return ZeroDec(), errorsmod.Wrap(ErrNaN, "NCalcEma alpha EMA operand should not be NaN")
}

nDec, err := NewDecFromUint64(n)
if err != nil {
return ZeroDec(), err
}
oneMinusAlpha, err := OneDec().Sub(alpha)
if err != nil {
return ZeroDec(), err
}
oneMinusAlphaExpN, err := Pow(oneMinusAlpha, nDec)
if err != nil {
return ZeroDec(), err
}
oneMinusAlphaExpNTimesOldVal, err := oneMinusAlphaExpN.Mul(x)
if err != nil {
return ZeroDec(), err
}

oneMinusOneMinusAlphaExpN, err := OneDec().Sub(oneMinusAlphaExpN)
if err != nil {
return ZeroDec(), err
}
UpdateTimesOneMinusOneMinusAlphaExpN, err := asymptote.Mul(oneMinusOneMinusAlphaExpN)
if err != nil {
return ZeroDec(), err
}

ret, err := oneMinusAlphaExpNTimesOldVal.Add(UpdateTimesOneMinusOneMinusAlphaExpN)
if err != nil {
return ZeroDec(), err
}
return ret, nil
}

// Generic function that sorts the keys of a map
// Used for deterministic ranging of maps
func GetSortedKeys[K cmp.Ordered, V any](m map[K]V) []K {
Expand Down Expand Up @@ -491,7 +544,7 @@ func GetQuantileOfDecs(
decs []Dec,
quantile Dec,
) (Dec, error) {
// If there are no decs then the quantile of scores is 0.
// If there are no decs then the quantile of the `decs` is 0.
// This better ensures chain continuity without consequence because in this situation
// there is no meaningful quantile to calculate.
if len(decs) == 0 {
Expand Down
72 changes: 72 additions & 0 deletions math/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,78 @@ func TestCalcEmaWithNaN(t *testing.T) {
require.ErrorIs(t, err, alloraMath.ErrNaN)
}

func TestNCalcEma(t *testing.T) {
cases := []struct {
name string
alpha alloraMath.Dec
update alloraMath.Dec
score alloraMath.Dec
n uint64
expectedResult alloraMath.Dec
expectedErr error
}{
{
name: "n=1",
alpha: alloraMath.MustNewDecFromString("0.1"),
update: alloraMath.MustNewDecFromString("300"),
score: alloraMath.MustNewDecFromString("200"),
n: 1,
expectedResult: alloraMath.MustNewDecFromString("210"),
expectedErr: nil,
},
{
name: "n=4",
alpha: alloraMath.MustNewDecFromString("0.1"),
update: alloraMath.MustNewDecFromString("300"),
score: alloraMath.MustNewDecFromString("200"),
n: 4,
expectedResult: alloraMath.MustNewDecFromString("234.39"),
expectedErr: nil,
},
{
name: "error alpha NaN",
alpha: alloraMath.NewNaN(),
update: alloraMath.MustNewDecFromString("300"),
score: alloraMath.MustNewDecFromString("200"),
n: 3,
expectedResult: alloraMath.ZeroDec(),
expectedErr: alloraMath.ErrNaN,
},
{
name: "error update NaN",
alpha: alloraMath.MustNewDecFromString("0.1"),
update: alloraMath.NewNaN(),
score: alloraMath.MustNewDecFromString("200"),
n: 3,
expectedResult: alloraMath.ZeroDec(),
expectedErr: alloraMath.ErrNaN,
},
{
name: "error score NaN",
alpha: alloraMath.MustNewDecFromString("0.1"),
update: alloraMath.MustNewDecFromString("300"),
score: alloraMath.NewNaN(),
n: 3,
expectedResult: alloraMath.ZeroDec(),
expectedErr: alloraMath.ErrNaN,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result, err := alloraMath.NCalcEma(tc.alpha, tc.update, tc.score, tc.n)
if tc.expectedErr != nil {
require.ErrorIs(t, err, tc.expectedErr)
} else {
require.NoError(t, err)
inDelta, err := alloraMath.InDelta(tc.expectedResult, result, alloraMath.MustNewDecFromString("0.0001"))
require.NoError(t, err)
require.True(t, inDelta, "expected %s, got %s", tc.expectedResult.String(), result.String())
}
})
}
}

func TestStdDev(t *testing.T) {
tests := []struct {
name string
Expand Down
146 changes: 146 additions & 0 deletions x/emissions/keeper/actor_penalties.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package keeper

import (
"cosmossdk.io/math"
alloraMath "github.com/allora-network/allora-chain/math"
"github.com/allora-network/allora-chain/x/emissions/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

// ApplyLivenessPenaltyToInferer penalises an inferer for missing previous epochs. It saves and returns the new EMA score.
// If the inferer didn't miss any epochs this is a no-op, the EMA score is returned as is.
func (k *Keeper) ApplyLivenessPenaltyToInferer(
ctx sdk.Context,
topic types.Topic,
nonceBlockHeight types.BlockHeight,
emaScore types.Score,
) (types.Score, error) {
return ApplyLivenessPenaltyToActor(
ctx,
CountWorkerContiguousMissedEpochs,
func(topicId TopicId) (alloraMath.Dec, error) {
return k.GetTopicInitialInfererEmaScore(ctx, topicId)
},
func(topicId TopicId, score types.Score) error {
return k.SetInfererScoreEma(ctx, topicId, score.Address, score)
},
topic,
nonceBlockHeight,
emaScore,
)
}

// ApplyLivenessPenaltyToForecaster penalises a forecaster for missing previous epochs. It saves and returns the new EMA score.
// If the forecaster didn't miss any epochs this is a no-op, the EMA score is returned as is.
func (k *Keeper) ApplyLivenessPenaltyToForecaster(
ctx sdk.Context,
topic types.Topic,
nonceBlockHeight types.BlockHeight,
emaScore types.Score,
) (types.Score, error) {
return ApplyLivenessPenaltyToActor(
ctx,
CountWorkerContiguousMissedEpochs,
func(topicId TopicId) (alloraMath.Dec, error) {
return k.GetTopicInitialForecasterEmaScore(ctx, topicId)
},
func(topicId TopicId, score types.Score) error {
return k.SetForecasterScoreEma(ctx, topicId, score.Address, score)
},
topic,
nonceBlockHeight,
emaScore,
)
}

// ApplyLivenessPenaltyToReputer penalises a reputer for missing previous epochs. It saves and returns the new EMA score.
// If the reputer didn't miss any epochs this is a no-op, the EMA score is returned as is.
func (k *Keeper) ApplyLivenessPenaltyToReputer(
ctx sdk.Context,
topic types.Topic,
nonceBlockHeight types.BlockHeight,
emaScore types.Score,
) (types.Score, error) {
return ApplyLivenessPenaltyToActor(
ctx,
CountReputerContiguousMissedEpochs,
func(topicId TopicId) (alloraMath.Dec, error) {
return k.GetTopicInitialReputerEmaScore(ctx, topicId)
},
func(topicId TopicId, score types.Score) error {
return k.SetReputerScoreEma(ctx, topicId, score.Address, score)
},
topic,
nonceBlockHeight,
emaScore,
)
}

func ApplyLivenessPenaltyToActor(
ctx sdk.Context,
missedEpochsFn func(topic types.Topic, lastSubmittedNonce int64) int64,
getAsymptoteFn func(topicId TopicId) (alloraMath.Dec, error),
setScoreFn func(topicId TopicId, score types.Score) error,
topic types.Topic,
nonceBlockHeight types.BlockHeight,
emaScore types.Score,
) (types.Score, error) {
missedEpochs := missedEpochsFn(topic, emaScore.BlockHeight)
// No missed epochs == no penalty
if missedEpochs == 0 {
return emaScore, nil
}

penalty, err := getAsymptoteFn(topic.Id)
if err != nil {
return types.Score{}, err
}
emaScore.BlockHeight = nonceBlockHeight

beforePenalty := emaScore
emaScore.Score, err = applyPenalty(topic, penalty, emaScore.Score, missedEpochs)
if err != nil {
return types.Score{}, err
}

ctx.Logger().Debug("apply liveness penalty on actor",
"nonce", nonceBlockHeight,
"penalty", penalty,
"before", beforePenalty,
"after", emaScore,
)

// Save the penalised EMA score
return emaScore, setScoreFn(topic.Id, emaScore)
}

// applyPenalty applies the penalty to the EMA score for the given number of missed epochs while staying above provided limit.
func applyPenalty(topic types.Topic, penalty, emaScore alloraMath.Dec, missedEpochs int64) (alloraMath.Dec, error) {
return alloraMath.NCalcEma(topic.MeritSortitionAlpha, penalty, emaScore, uint64(missedEpochs))
}

// CountWorkerContiguousMissedEpochs counts the number of contiguous missed epochs prior to the given nonce, given the
// actor last submission.
func CountWorkerContiguousMissedEpochs(topic types.Topic, lastSubmittedNonce int64) int64 {
prevEpochStart := topic.EpochLastEnded - topic.EpochLength
return countContiguousMissedEpochs(prevEpochStart, topic.EpochLength, lastSubmittedNonce)
}

// CountReputerContiguousMissedEpochs counts the number of contiguous missed epochs prior to the given nonce, given the
// actor last submission.
func CountReputerContiguousMissedEpochs(topic types.Topic, lastSubmittedNonce int64) int64 {
prevEpochStart := topic.EpochLastEnded - topic.EpochLength - topic.GroundTruthLag
return countContiguousMissedEpochs(prevEpochStart, topic.EpochLength, lastSubmittedNonce)
}

func countContiguousMissedEpochs(prevEpochStart, epochLength, lastSubmittedNonce int64) int64 {
lastSubmittedNonce = math.Max(lastSubmittedNonce, 0)
prevEpochStart = math.Max(prevEpochStart, 0)
epochLength = math.Max(epochLength, 0)

if lastSubmittedNonce >= prevEpochStart {
return 0
}

return (prevEpochStart-1-lastSubmittedNonce)/epochLength + 1
}
Loading
Loading