code.vegaprotocol.io/vega@v0.79.0/core/validators/validator_score.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package validators 17 18 import ( 19 "context" 20 "math/rand" 21 "sort" 22 23 "code.vegaprotocol.io/vega/core/events" 24 "code.vegaprotocol.io/vega/core/types" 25 "code.vegaprotocol.io/vega/libs/num" 26 "code.vegaprotocol.io/vega/logging" 27 ) 28 29 type valScore struct { 30 ID string 31 score num.Decimal 32 } 33 34 // getStakeScore returns a score for the validator based on their relative score of the total score. 35 // No anti-whaling is applied. 36 func getStakeScore(delegationState []*types.ValidatorData) map[string]num.Decimal { 37 totalStake := num.UintZero() 38 for _, ds := range delegationState { 39 totalStake.AddSum(num.Sum(ds.SelfStake, ds.StakeByDelegators)) 40 } 41 42 totalStakeD := totalStake.ToDecimal() 43 scores := make(map[string]num.Decimal, len(delegationState)) 44 for _, ds := range delegationState { 45 if totalStakeD.IsPositive() { 46 scores[ds.NodeID] = num.Sum(ds.SelfStake, ds.StakeByDelegators).ToDecimal().Div(totalStakeD) 47 } else { 48 scores[ds.NodeID] = num.DecimalZero() 49 } 50 } 51 return scores 52 } 53 54 // getPerformanceScore returns the performance score of the validators. 55 // if the node has been a tendermint validator for the epoch it returns its tendermint performance score (as the ratio between the blocks proposed 56 // and the number of times it was expected to propose) 57 // if the node has less than the minimum stake they get 0 performance score 58 // if the node is ersatz or waiting list validators and has not yet forwarded or voted first - their score is 0 59 // if the node is not tm node - their score is based on the number of times out of the last 10 that they signed every 1000 blocks. 60 func (t *Topology) getPerformanceScore(delegationState []*types.ValidatorData) map[string]num.Decimal { 61 scores := make(map[string]num.Decimal, len(delegationState)) 62 63 totalTmPower := int64(0) 64 for _, vs := range t.validators { 65 totalTmPower += vs.validatorPower 66 } 67 68 for _, ds := range delegationState { 69 vd := t.validators[ds.NodeID] 70 performanceScore := num.DecimalZero() 71 if ds.SelfStake.LT(t.minimumStake) { 72 scores[ds.NodeID] = performanceScore 73 continue 74 } 75 if vd.status == ValidatorStatusTendermint { 76 scores[ds.NodeID] = t.validatorPerformance.ValidatorPerformanceScore(vd.data.TmPubKey, vd.validatorPower, totalTmPower, t.performanceScalingFactor) 77 continue 78 } 79 80 if vd.numberOfEthereumEventsForwarded < t.minimumEthereumEventsForNewValidator { 81 scores[ds.NodeID] = performanceScore 82 continue 83 } 84 for _, v := range t.validators[ds.NodeID].heartbeatTracker.blockSigs { 85 if v { 86 performanceScore = performanceScore.Add(PerformanceIncrement) 87 } 88 } 89 scores[ds.NodeID] = performanceScore 90 } 91 92 return scores 93 } 94 95 // getRankingScore returns the score for ranking as stake_score x performance_score. 96 // for validators in ersatz or tendermint it is scaled by 1+incumbent factor. 97 func (t *Topology) getRankingScore(delegationState []*types.ValidatorData) (map[string]num.Decimal, map[string]num.Decimal, map[string]num.Decimal) { 98 stakeScores := getStakeScore(delegationState) 99 performanceScores := t.getPerformanceScore(delegationState) 100 rankingScores := t.getRankingScoreInternal(stakeScores, performanceScores) 101 return stakeScores, performanceScores, rankingScores 102 } 103 104 func (t *Topology) getRankingScoreInternal(stakeScores, perfScores map[string]num.Decimal) map[string]num.Decimal { 105 if len(stakeScores) != len(perfScores) { 106 t.log.Panic("incompatible slice length for stakeScores and perfScores") 107 } 108 rankingScore := make(map[string]num.Decimal, len(stakeScores)) 109 for nodeID := range stakeScores { 110 vd := t.validators[nodeID] 111 ranking := stakeScores[nodeID].Mul(perfScores[nodeID]) 112 if vd.status == ValidatorStatusTendermint || vd.status == ValidatorStatusErsatz { 113 ranking = ranking.Mul(t.validatorIncumbentBonusFactor) 114 } 115 rankingScore[nodeID] = ranking 116 } 117 return rankingScore 118 } 119 120 // normaliseScores normalises the given scores with respect to their sum, making sure they don't go above 1. 121 func normaliseScores(scores map[string]num.Decimal, rng *rand.Rand) map[string]num.Decimal { 122 totalScore := num.DecimalZero() 123 for _, v := range scores { 124 totalScore = totalScore.Add(v) 125 } 126 127 normScores := make(map[string]num.Decimal, len(scores)) 128 if totalScore.IsZero() { 129 for k := range scores { 130 normScores[k] = num.DecimalZero() 131 } 132 return normScores 133 } 134 135 scoreSum := num.DecimalZero() 136 for n, s := range scores { 137 normScores[n] = s.Div(totalScore) 138 scoreSum = scoreSum.Add(normScores[n]) 139 } 140 if scoreSum.LessThanOrEqual(DecimalOne) { 141 return normScores 142 } 143 keys := make([]string, 0, len(normScores)) 144 for k := range normScores { 145 keys = append(keys, k) 146 } 147 sort.Strings(keys) 148 precisionError := scoreSum.Sub(num.DecimalFromInt64(1)) 149 unluckyValidator := rng.Intn(len(keys)) 150 normScores[keys[unluckyValidator]] = num.MaxD(normScores[keys[unluckyValidator]].Sub(precisionError), num.DecimalZero()) 151 return normScores 152 } 153 154 // calcValidatorScore calculates the stake based raw validator score with anti whaling. 155 func CalcValidatorScore(valStake, totalStake, optStake num.Decimal, stakeScoreParams types.StakeScoreParams) num.Decimal { 156 if totalStake.IsZero() { 157 return num.DecimalZero() 158 } 159 return antiwhale(valStake, totalStake, optStake, stakeScoreParams) 160 } 161 162 func antiwhale(valStake, totalStake, optStake num.Decimal, stakeScoreParams types.StakeScoreParams) num.Decimal { 163 penaltyFlatAmt := num.MaxD(num.DecimalZero(), valStake.Sub(optStake)) 164 penaltyDownAmt := num.MaxD(num.DecimalZero(), valStake.Sub(stakeScoreParams.OptimalStakeMultiplier.Mul(optStake))) 165 linearScore := valStake.Sub(penaltyFlatAmt).Sub(penaltyDownAmt).Div(totalStake) // totalStake guaranteed to be non zero at this point 166 linearScore = num.MinD(num.DecimalOne(), num.MaxD(num.DecimalZero(), linearScore)) 167 return linearScore 168 } 169 170 // getValScore returns the multiplications of the corresponding score for each validator. 171 func getValScore(inScores ...map[string]num.Decimal) map[string]num.Decimal { 172 if len(inScores) == 0 { 173 return map[string]num.Decimal{} 174 } 175 scores := make(map[string]num.Decimal, len(inScores[0])) 176 for k := range inScores[0] { 177 s := num.DecimalFromFloat(1) 178 for _, v := range inScores { 179 s = s.Mul(v[k]) 180 } 181 scores[k] = s 182 } 183 return scores 184 } 185 186 // getMultisigScore (applies to tm validators only) returns multisigScore as: 187 // if the val_score = raw_score x performance_score is in the top <numberEthMultisigSigners> and the validator is on the multisig contract => 1 188 // else 0 189 // that means a validator in tendermint set only gets a reward if it is in the top <numberEthMultisigSigners> and their registered with the multisig contract. 190 func getMultisigScore( 191 log *logging.Logger, 192 status ValidatorStatus, 193 rawScores map[string]num.Decimal, 194 perfScore map[string]num.Decimal, 195 primaryMultisig MultiSigTopology, 196 secondaryMultisig MultiSigTopology, 197 numberEthMultisigSigners int, 198 nodeIDToEthAddress map[string]string, 199 ) map[string]num.Decimal { 200 if status == ValidatorStatusErsatz { 201 scores := make(map[string]num.Decimal, len(rawScores)) 202 for k := range rawScores { 203 scores[k] = decimalOne 204 } 205 return scores 206 } 207 208 ethAddresses := make([]string, 0, len(rawScores)) 209 for k := range rawScores { 210 if eth, ok := nodeIDToEthAddress[k]; !ok { 211 log.Panic("missing eth address in mapping", logging.String("node-id", k)) 212 } else { 213 ethAddresses = append(ethAddresses, eth) 214 } 215 } 216 sort.Strings(ethAddresses) 217 218 if primaryMultisig.ExcessSigners(ethAddresses) || secondaryMultisig.ExcessSigners(ethAddresses) { 219 res := make(map[string]num.Decimal, len(rawScores)) 220 for rs := range rawScores { 221 res[rs] = num.DecimalZero() 222 } 223 return res 224 } 225 226 valScores := make([]valScore, 0, len(rawScores)) 227 for k, d := range rawScores { 228 valScores = append(valScores, valScore{ID: k, score: d.Mul(perfScore[k])}) 229 } 230 231 sort.SliceStable(valScores, func(i, j int) bool { 232 if valScores[i].score.Equal(valScores[j].score) { 233 return valScores[i].ID < valScores[j].ID 234 } 235 return valScores[i].score.GreaterThan(valScores[j].score) 236 }) 237 238 res := make(map[string]num.Decimal, len(valScores)) 239 for i, vs := range valScores { 240 if i < numberEthMultisigSigners { 241 if eth, ok := nodeIDToEthAddress[vs.ID]; !ok { 242 log.Panic("missing eth address in mapping", logging.String("node-id", vs.ID)) 243 } else { 244 if primaryMultisig.IsSigner(eth) && secondaryMultisig.IsSigner(eth) { 245 res[vs.ID] = decimalOne 246 } 247 } 248 continue 249 } 250 // everyone else is a 1 251 res[vs.ID] = decimalOne 252 } 253 for k := range rawScores { 254 if _, ok := res[k]; !ok { 255 res[k] = num.DecimalZero() 256 } 257 } 258 259 return res 260 } 261 262 // GetRewardsScores returns the reward scores (raw, performance, multisig, validator_score, and normalised) for tm and ersatz validaor sets. 263 func (t *Topology) GetRewardsScores(ctx context.Context, epochSeq string, delegationState []*types.ValidatorData, stakeScoreParams types.StakeScoreParams) (*types.ScoreData, *types.ScoreData) { 264 t.mu.RLock() 265 defer t.mu.RUnlock() 266 tmScores, optStake := t.calculateScores(delegationState, ValidatorStatusTendermint, stakeScoreParams, nil) 267 ezScores, _ := t.calculateScores(delegationState, ValidatorStatusErsatz, stakeScoreParams, &optStake) 268 269 evts := make([]events.Event, 0, len(tmScores.NodeIDSlice)+len(ezScores.NodeIDSlice)) 270 for _, nodeID := range tmScores.NodeIDSlice { 271 evts = append(evts, events.NewValidatorScore(ctx, nodeID, epochSeq, tmScores.ValScores[nodeID], tmScores.NormalisedScores[nodeID], tmScores.RawValScores[nodeID], tmScores.PerformanceScores[nodeID], tmScores.MultisigScores[nodeID], "tendermint")) 272 } 273 for _, nodeID := range ezScores.NodeIDSlice { 274 evts = append(evts, events.NewValidatorScore(ctx, nodeID, epochSeq, ezScores.ValScores[nodeID], ezScores.NormalisedScores[nodeID], ezScores.RawValScores[nodeID], ezScores.PerformanceScores[nodeID], decimalOne, "ersatz")) 275 } 276 t.broker.SendBatch(evts) 277 return tmScores, ezScores 278 } 279 280 func (t *Topology) calculateScores(delegationState []*types.ValidatorData, validatorStatus ValidatorStatus, stakeScoreParams types.StakeScoreParams, optStake *num.Decimal) (*types.ScoreData, num.Decimal) { 281 scores := &types.ScoreData{} 282 283 // identify validators for the status for the epoch 284 validatorsForStatus := map[string]struct{}{} 285 nodeIDToEthAddress := map[string]string{} 286 for k, d := range t.validators { 287 if d.status == validatorStatus { 288 validatorsForStatus[k] = struct{}{} 289 } 290 nodeIDToEthAddress[d.data.ID] = d.data.EthereumAddress 291 } 292 293 // calculate the delegation and anti-whaling score for the validators with the given status 294 delegationForStatus, totalDelegationForStatus := CalcDelegation(validatorsForStatus, delegationState) 295 if optStake == nil { 296 optimalkStake := GetOptimalStake(totalDelegationForStatus, len(delegationForStatus), stakeScoreParams) 297 optStake = &optimalkStake 298 } 299 300 scores.RawValScores = CalcAntiWhalingScore(delegationForStatus, totalDelegationForStatus, *optStake, stakeScoreParams) 301 302 // calculate performance score based on performance of the validators with the given status 303 scores.PerformanceScores = t.getPerformanceScore(delegationForStatus) 304 305 // calculate multisig score for the validators 306 scores.MultisigScores = getMultisigScore(t.log, validatorStatus, scores.RawValScores, scores.PerformanceScores, t.primaryMultisig, t.secondaryMultisig, t.numberEthMultisigSigners, nodeIDToEthAddress) 307 308 // calculate the final score 309 scores.ValScores = getValScore(scores.RawValScores, scores.PerformanceScores, scores.MultisigScores) 310 311 // normalise the scores 312 scores.NormalisedScores = normaliseScores(scores.ValScores, t.rng) 313 314 // sort the list of tm validators 315 tmNodeIDs := make([]string, 0, len(delegationForStatus)) 316 for k := range scores.RawValScores { 317 tmNodeIDs = append(tmNodeIDs, k) 318 } 319 320 sort.Strings(tmNodeIDs) 321 scores.NodeIDSlice = tmNodeIDs 322 323 for _, k := range tmNodeIDs { 324 t.log.Info("reward scores for", logging.String("node-id", k), logging.String("stake-score", scores.RawValScores[k].String()), logging.String("performance-score", scores.PerformanceScores[k].String()), logging.String("multisig-score", scores.MultisigScores[k].String()), logging.String("validator-score", scores.ValScores[k].String()), logging.String("normalised-score", scores.NormalisedScores[k].String())) 325 } 326 327 return scores, *optStake 328 } 329 330 // CalcDelegation extracts the delegation of the validator set from the delegation state slice and returns the total delegation. 331 func CalcDelegation(validators map[string]struct{}, delegationState []*types.ValidatorData) ([]*types.ValidatorData, num.Decimal) { 332 tv := map[string]num.Decimal{} 333 tvTotal := num.UintZero() 334 tvDelegation := []*types.ValidatorData{} 335 336 // split the delegation into tendermint and ersatz and count their respective totals 337 for _, ds := range delegationState { 338 if _, ok := validators[ds.NodeID]; ok { 339 tv[ds.NodeID] = num.DecimalZero() 340 stake := num.Sum(ds.SelfStake, ds.StakeByDelegators) 341 tvTotal.AddSum(stake) 342 tvDelegation = append(tvDelegation, ds) 343 } 344 } 345 tvTotalD := tvTotal.ToDecimal() 346 return tvDelegation, tvTotalD 347 } 348 349 func GetOptimalStake(tmTotalDelegation num.Decimal, numValidators int, params types.StakeScoreParams) num.Decimal { 350 if tmTotalDelegation.IsPositive() { 351 numVal := num.DecimalFromInt64(int64(numValidators)) 352 return tmTotalDelegation.Div(num.MaxD(params.MinVal, numVal.Div(params.CompLevel))) 353 } 354 return num.DecimalZero() 355 } 356 357 // CalcAntiWhalingScore calculates the anti-whaling stake score for the validators represented in the given delegation set. 358 func CalcAntiWhalingScore(delegationState []*types.ValidatorData, totalStakeD, optStake num.Decimal, stakeScoreParams types.StakeScoreParams) map[string]num.Decimal { 359 stakeScore := make(map[string]num.Decimal, len(delegationState)) 360 for _, ds := range delegationState { 361 if totalStakeD.IsPositive() { 362 stakeScore[ds.NodeID] = CalcValidatorScore(num.Sum(ds.SelfStake, ds.StakeByDelegators).ToDecimal(), totalStakeD, optStake, stakeScoreParams) 363 } else { 364 stakeScore[ds.NodeID] = num.DecimalZero() 365 } 366 } 367 return stakeScore 368 }