-
Notifications
You must be signed in to change notification settings - Fork 42
/
rewards_calc.go
465 lines (427 loc) · 15.8 KB
/
rewards_calc.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
package module
import (
"fmt"
"math/big"
cosmosMath "cosmossdk.io/math"
params "github.com/allora-network/allora-chain/app/params"
state "github.com/allora-network/allora-chain/x/emissions"
"github.com/allora-network/allora-chain/x/emissions/keeper"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type Uint = cosmosMath.Uint
type Float = big.Float
type Number interface {
*Float | *cosmosMath.Uint
}
// ********************************************************
// * PUBLIC EXPORTED READ-ONLY FUNCTIONS *
// ********************************************************
// For a given topic:
// given the sum total of all stake in that topic,
// given the amount of new tokens scheduled to be emitted this epoch,
// given the total amount of stake in the network,
// return the amount of new tokens to be emitted to each partipicant in that topic
func GetParticipantEmissionsForTopic(
ctx sdk.Context,
am AppModule,
topicId keeper.TOPIC_ID,
topicStake *Uint,
cumulativeEmission *Uint,
accumulatedMetDemand *Uint,
totalStake *Uint) (rewards map[string]*Uint, err error) {
// get total emission for topic
topicEmissionXStake := cumulativeEmission.Mul(*topicStake)
topicEmissions := topicEmissionXStake.Quo(*totalStake).Add(*accumulatedMetDemand)
// get all reputers in that topic
// get all normalized stakes of those reputers
topicStakeFloat := big.NewFloat(0).SetInt(topicStake.BigInt())
reputerStakeNorm, err := am.keeper.GetReputerNormalizedStake(ctx, topicId, topicStakeFloat)
if err != nil {
fmt.Println("Error getting reputer normalized stake: ", err)
return nil, err
}
// Get Weights between nodes in topic
// Weight_ij = reputer i -> worker j -> weight val
topicWeights, err := am.keeper.GetWeightsFromTopic(ctx, topicId)
if err != nil {
fmt.Println("Error getting weights from topic: ", err)
return nil, err
}
// Mask inferences if workers admit insufficient liveness
maskedTopicWeights, err := MaskWeightsIfInsufficientLiveness(ctx, am, topicId, topicWeights)
if err != nil {
fmt.Println("Error masking weights if insufficient liveness: ", err)
return nil, err
}
// Ranks = matmul Weights * NormalizedStake
// for i rows and j columns
// i.e. rank[j] = sum(j) + weight_ij * normalizedStake_i
ranks := matmul(maskedTopicWeights, reputerStakeNorm)
// Incentive = normalize(Ranks)
incentive, err := normalize(ranks)
if err != nil {
// if error is ErrDivideMapValuesByZero (e.g. because no workers were live => all weights masked) then return empty rewards
if err == state.ErrDivideMapValuesByZero {
return make(map[string]*Uint), nil
}
fmt.Println("Error normalizing ranks: ", err)
return nil, err
}
// BondDeltas using elementwise multiplication of the same vector for all rows of Weight matrix.
// i.e. For each row i: BondDelta_ij = Weights_ij x Stake_j
bondDeltas := elementWiseProduct(maskedTopicWeights, reputerStakeNorm)
// Row-wise normalize BondDeltas
bondDeltasNorm, err := normalizeBondDeltas(bondDeltas)
if err != nil {
fmt.Println("Error normalizing bond deltas: ", err)
return nil, err
}
// Dividends = normalize(BondDeltas matmul Incentive)
dividends := matmul(bondDeltasNorm, incentive)
dividendsNorm, err := normalize(dividends)
if err != nil {
fmt.Println("Error normalizing dividends: ", err)
return nil, err
}
// EmissionSum = sum(Dividends) + sum(Incentives)
dividendsSum := sumMapValues(dividendsNorm)
incentivesSum := sumMapValues(incentive)
emissionSum := big.NewFloat(0).Add(÷ndsSum, &incentivesSum)
topicEmissionsFloat := big.NewFloat(0).SetInt(topicEmissions.BigInt())
if big.NewFloat(0).SetInt64(0).Cmp(emissionSum) == 0 {
// If EmissionSum == 0 then set NormalizedReputerEmissions to normalized stake
reputerEmissionsNorm := reputerStakeNorm
// ValidatorEmissions = scalar multiply topicEmissionsTotal x NormalizedReputerEmissions
rewards, err = scalarMultiply(reputerEmissionsNorm, topicEmissionsFloat)
if err != nil {
fmt.Println("Error scalar multiplying reputer emissions: ", err)
return nil, err
}
} else {
// NormalizedServerEmissions = Incentives scalar divide EmmissionSum
normalizedWorkerEmissions, err := divideMapValues(incentive, emissionSum)
if err != nil {
fmt.Println("Error dividing incentives by emission sum: ", err)
return nil, err
}
// NormalizedValidatorEmissions = Dividends scalar divide EmmissionSum
normalizedReputerEmissions, err := divideMapValues(dividends, emissionSum)
if err != nil {
fmt.Println("Error dividing dividends by emission sum: ", err)
return nil, err
}
// ServerEmissions = scalar multiply topicEmissionsTotal x NormalizedServerEmissions
workerEmissions, err := scalarMultiply(normalizedWorkerEmissions, topicEmissionsFloat)
if err != nil {
fmt.Println("Error scalar multiplying worker emissions: ", err)
return nil, err
}
// ValidatorEmissions = scalar multiply topicEmissionsTotal x NormalizedValidatorEmissions
reputerEmissions, err := scalarMultiply(normalizedReputerEmissions, topicEmissionsFloat)
if err != nil {
fmt.Println("Error scalar multiplying reputer emissions: ", err)
return nil, err
}
rewards = mapAdd(reputerEmissions, workerEmissions)
}
return rewards, nil
}
// This function checks topic weights and then masks them if not enough inferences were collected in that timestep
// It should:
//
// mask the inputted weights above with GetNumInferencesInRewardEpoch(),
// checking inference cadence by reward epoch length / topic inference cadence
// simple forgiveness check (only if many inferences are missing, don't reward)
func MaskWeightsIfInsufficientLiveness(
ctx sdk.Context,
am AppModule,
topicId keeper.TOPIC_ID,
weights map[string]map[string]*Uint) (map[string]map[string]*Uint, error) {
maskedWeights := make(map[string]map[string]*Uint)
for reputer, workerWeights := range weights {
if maskedWeights[reputer] == nil {
maskedWeights[reputer] = make(map[string]*Uint)
}
for worker, workerWeight := range workerWeights {
// Get the topic => its inference cadence
topic, err := am.keeper.GetTopic(ctx, topicId)
if err != nil {
return nil, err
}
// Get the number of inferences in the reward epoch
workerAddress, err := sdk.AccAddressFromBech32(worker)
if err != nil {
return nil, err
}
numInferencesInRewardEpoch, err := am.keeper.GetNumInferencesInRewardEpoch(ctx, topicId, workerAddress)
if err != nil {
return nil, err
}
// If number of inferences in the reward epoch < amount that should be there by too much, then mask the weight
epochLength, err := am.keeper.GetParamsEpochLength(ctx)
if err != nil {
return nil, err
}
maxPossibleInferencesInRewardEpoch := uint64(epochLength) / topic.InferenceCadence
// Allow for for 10% of inferences to be missing. Percent directly encoded as cosmosMath.LegacyDec
maxAllowableMissingInferencePercent, err := am.keeper.GetParamsMaxMissingInferencePercent(ctx)
if err != nil {
return nil, err
}
expectedNumInferencesInRewardEpoch := cosmosMath.LegacyOneDec().Sub(maxAllowableMissingInferencePercent).MulInt(
cosmosMath.NewIntFromUint64(maxPossibleInferencesInRewardEpoch)).TruncateInt()
if numInferencesInRewardEpoch.LT(cosmosMath.NewUintFromBigInt(expectedNumInferencesInRewardEpoch.BigInt())) {
maskedVal := cosmosMath.ZeroUint()
maskedWeights[reputer][worker] = &maskedVal
} else {
maskedWeights[reputer][worker] = workerWeight
}
}
}
return maskedWeights, nil
}
// ********************************************************
// * PRIVATE STATE CHANGING FUNCTIONS *
// ********************************************************
// The function that performs the emission of new tokens
func emitRewards(ctx sdk.Context, am AppModule) error {
// get total stake in network
totalStake, err := am.keeper.GetTotalStake(ctx)
if err != nil {
fmt.Println("Error getting total stake: ", err)
return err
}
// if no stake, no rewards to give away, do nothing
if totalStake.Equal(cosmosMath.ZeroUint()) {
err = am.keeper.SetLastRewardsUpdate(ctx, ctx.BlockHeight())
if err != nil {
fmt.Println("Error setting last rewards update: ", err)
return err
}
return nil
}
emissionsAddress := am.keeper.AccountKeeper().GetModuleAddress(state.AlloraRewardsAccountName)
emissionsBalance := am.keeper.BankKeeper().GetBalance(ctx, emissionsAddress, params.DefaultBondDenom)
cumulativeEmission := cosmosMath.NewUintFromBigInt(emissionsBalance.Amount.BigInt())
// Save/set the above emissions to actually pay participants.
// Do this by increasing the stake of each worker by their due ServerEmission + ValidatorEmission
err = am.keeper.SetLastRewardsUpdate(ctx, ctx.BlockHeight())
if err != nil {
fmt.Println("Error setting last rewards update: ", err)
return err
}
// use anonymous function to iterate through each (topic, sumStakeForTopic)
funcEachTopic := func(topicId keeper.TOPIC_ID, topicStake Uint) (bool, error) {
accumulatedMetDemand, err := am.keeper.GetTopicAccumulatedMetDemand(ctx, topicId)
if err != nil {
fmt.Println("Error getting accumulated met demand: ", err)
return true, err
}
// for each topic get percentage of total emissions
// then get each participant's percentage of that percentage
rewards, err := GetParticipantEmissionsForTopic(
ctx,
am,
topicId,
&topicStake,
&cumulativeEmission,
&accumulatedMetDemand,
&totalStake)
if err != nil {
fmt.Println("Error getting participant emissions for topic: ", err)
return true, err
}
// if no rewards to give, do nothing
if len(rewards) == 0 {
fmt.Printf(" No rewards to emit for Topic %v \n", topicId)
return false, nil
}
// Mint new tokens to the participants of that topic
emitRewardsToTopicParticipants(ctx, am, topicId, rewards)
am.keeper.SetTopicAccumulatedMetDemand(ctx, topicId, cosmosMath.ZeroUint())
return false, nil
}
// Iterate through each (topic, sumStakeForTopic) and run funcEachTopic for each topic
err = am.keeper.WalkAllTopicStake(ctx, funcEachTopic)
if err != nil {
fmt.Println("Error walking all topic stake: ", err)
return err
}
return am.keeper.ResetNumInferencesInRewardEpoch(ctx)
}
// this function addStake to each participant of a topic according
// to how much stake the reputer/workerEmissions maps say to add
func emitRewardsToTopicParticipants(
ctx sdk.Context,
am AppModule,
topic keeper.TOPIC_ID,
rewards map[string]*Uint) {
// by default emissions are restaked, upon the person themselves.
fmt.Printf("\n---------------- Rewards for Topic %v ----------------\n", topic)
for participant, reward := range rewards {
fmt.Printf(" Emitting %suallo to %s \n", reward.String(), participant)
am.keeper.AddStake(ctx, []uint64{topic}, participant, participant, *reward)
rewardCoins := sdk.NewCoins(sdk.NewCoin(params.DefaultBondDenom, cosmosMath.NewIntFromBigInt(reward.BigInt())))
am.keeper.BankKeeper().SendCoinsFromModuleToModule(ctx, state.AlloraRewardsAccountName, state.AlloraStakingAccountName, rewardCoins)
}
if len(rewards) == 0 {
fmt.Printf(" No rewards to emit for Topic %v \n", topic)
}
fmt.Println("\n-----------------------------------------")
}
// ********************************************************
// * PRIVATE HELPER FUNCTIONS *
// ********************************************************
// matmul multiplies a matrix by a vector where both are stored in golang maps
// the index to the map is considered the row or column
// 0 values are taken to be not found in the map and so skipped during addition
// for matrix * vector, iterating through rows i and columns j,
// result_j = result_j + matrix_ij * vector_i
//
// EXAMPLE:
// vector = { 1, 2 }
// matrix = { { 1, 2, 3 }, { 4, 5, 6 } }
// output = { 1*1 + 2*4, 1*2 + 2*5, 1*3 + 6*2}
// output = { 9, 12, 15 }
// or represented as a map:
// vector = { "a": 1, "b": 2 }
// matrix = { "a": { "c": 1, "d": 2, "e": 3 }, "b": { "c": 4, "d": 5, "e": 6 } }
// output = { "c": 1*1 + 2*4, "d": 1*2 + 2*5, "e": 1*3 + 6*2}
// output = { "c": 9, "d": 12, "e": 15 }
func matmul[N Number](
matrix map[string]map[string]N,
vector map[string]*Float) (result map[string]*Float) {
result = make(map[string]*Float)
for i, rowMap := range matrix {
vec_i := vector[i]
if vec_i == nil {
continue
}
for j, matrix_ij := range rowMap {
priorResult := big.NewFloat(0)
if result[j] != nil {
priorResult = result[j]
}
deltaResult := big.NewFloat(0)
switch m_ij := any(matrix_ij).(type) {
case *cosmosMath.Uint:
f := big.NewFloat(0)
f.SetInt(m_ij.BigInt())
deltaResult.Mul(f, vec_i)
case *Float:
deltaResult.Mul(m_ij, vec_i)
default:
panic("matmul: unknown input type")
}
deltaResult.Add(deltaResult, priorResult)
result[j] = deltaResult
}
}
return result
}
// normalize divides every value in a map by the sum of all values in the map
func normalize(a map[string]*Float) (map[string]*Float, error) {
if len(a) == 0 {
return a, nil
}
sum := big.NewFloat(0)
for _, val := range a {
sum.Add(sum, val)
}
return divideMapValues(a, sum)
}
// divideMapValues divides every value in a map by the divisor provided
func divideMapValues(
a map[string]*Float,
divisor *Float) (map[string]*Float, error) {
if divisor.Cmp(big.NewFloat(0)) == 0 {
return nil, state.ErrDivideMapValuesByZero
}
ret := make(map[keeper.ACC_ADDRESS]*Float)
for key, val := range a {
ret[key] = big.NewFloat(0).Quo(val, divisor)
}
return ret, nil
}
// Element Wise Product takes a matrix and a vector and multiplies
// each element of the matrix by the corresponding element of the vector
// this can sometimes be called the Hadamard product
// note that we use maps to represent the matrix and vector
// so values of zero are simply not stored in the map.
// for matrix * vector, iterating through rows i and columns j,
// result_ij = matrix_ij * vector_i
func elementWiseProduct(
matrix map[string]map[string]*Uint,
vector map[string]*Float) (result map[string]map[string]*Float) {
result = make(map[string]map[string]*Float)
for i, rowMap := range matrix {
result[i] = make(map[string]*Float)
vec_i := vector[i]
if vec_i == nil {
continue
}
for j, matrix_ij := range rowMap {
matrix_ijFloat := big.NewFloat(0).SetInt(matrix_ij.BigInt())
result[i][j] = big.NewFloat(0).Mul(matrix_ijFloat, vec_i)
}
}
return result
}
// Row-wise normalizes BondDeltas. For each row, normalizes the values in that row relative to the row
func normalizeBondDeltas(bondDeltas map[keeper.REPUTERS]map[keeper.WORKERS]*Float) (result map[keeper.REPUTERS]map[keeper.WORKERS]*Float, err error) {
result = make(map[keeper.REPUTERS]map[keeper.WORKERS]*Float)
for reputer, workerWeights := range bondDeltas {
result[reputer], err = normalize(workerWeights)
if err != nil {
return nil, err
}
}
return result, nil
}
// sumMapValues adds all values in a map together and returns the result
func sumMapValues(a map[string]*Float) Float {
ret := big.NewFloat(0)
for _, val := range a {
ret.Add(ret, val)
}
return *ret
}
// scalarMultiply multiplies a matrix by a scalar
// every value in the matrix individually is multiplied by the scalar
// for this case we then cast the Float back to a Uint
func scalarMultiply(
matrix map[string]*Float,
scalar *Float) (result map[string]*Uint, err error) {
result = make(map[string]*Uint)
err = nil
for key, val := range matrix {
val := big.NewFloat(0).Mul(val, scalar)
if val.Sign() == -1 {
return nil, state.ErrScalarMultiplyNegative
}
valBigInt, _ := val.Int(nil)
valUint := cosmosMath.NewUintFromBigInt(valBigInt)
result[key] = &valUint
}
return result, err
}
// mapAdd adds two maps together, summing the values of the same keys
func mapAdd(a map[string]*Uint, b map[string]*Uint) (result map[string]*Uint) {
result = make(map[string]*Uint)
for key, val := range a {
val2, ok := b[key]
if ok {
sum := val.Add(*val2)
result[key] = &sum
} else {
result[key] = val
}
}
for key, val := range b {
_, ok := a[key]
if !ok {
result[key] = val
}
}
return result
}