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  }