diff --git a/apps/governance/src/routes/staking/PreviousEpoch.graphql b/apps/governance/src/routes/staking/PreviousEpoch.graphql index 4581eca9f..1b6c42e03 100644 --- a/apps/governance/src/routes/staking/PreviousEpoch.graphql +++ b/apps/governance/src/routes/staking/PreviousEpoch.graphql @@ -5,12 +5,22 @@ query PreviousEpoch($epochId: ID) { edges { node { id + stakedTotal rewardScore { rawValidatorScore performanceScore + multisigScore + validatorScore + normalisedScore + validatorStatus } rankingScore { + status + previousStatus + rankingScore stakeScore + performanceScore + votingPower } } } diff --git a/apps/governance/src/routes/staking/__generated__/PreviousEpoch.ts b/apps/governance/src/routes/staking/__generated__/PreviousEpoch.ts index 3914a0c59..002e75b5c 100644 --- a/apps/governance/src/routes/staking/__generated__/PreviousEpoch.ts +++ b/apps/governance/src/routes/staking/__generated__/PreviousEpoch.ts @@ -8,7 +8,7 @@ export type PreviousEpochQueryVariables = Types.Exact<{ }>; -export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string } | null, rankingScore: { __typename?: 'RankingScore', stakeScore: string } } } | null> | null } | null } }; +export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, stakedTotal: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string, multisigScore: string, validatorScore: string, normalisedScore: string, validatorStatus: Types.ValidatorStatus } | null, rankingScore: { __typename?: 'RankingScore', status: Types.ValidatorStatus, previousStatus: Types.ValidatorStatus, rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string } } } | null> | null } | null } }; export const PreviousEpochDocument = gql` @@ -19,12 +19,22 @@ export const PreviousEpochDocument = gql` edges { node { id + stakedTotal rewardScore { rawValidatorScore performanceScore + multisigScore + validatorScore + normalisedScore + validatorStatus } rankingScore { + status + previousStatus + rankingScore stakeScore + performanceScore + votingPower } } } diff --git a/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.spec.tsx b/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.spec.tsx index f9e88224a..f65c25356 100644 --- a/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.spec.tsx +++ b/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.spec.tsx @@ -79,36 +79,72 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = { { node: { id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4', + stakedTotal: '14182454495731682635157', rewardScore: { rawValidatorScore: '0.25', performanceScore: '0.9998677767864936', + multisigScore: '', + validatorScore: '', + normalisedScore: '', + validatorStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, }, rankingScore: { stakeScore: '0.2499583402766206', + performanceScore: '0.9998677767864936', + status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + previousStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + rankingScore: '', + votingPower: '', }, }, }, { node: { id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99', + stakedTotal: '9618711883996159534058', rewardScore: { rawValidatorScore: '0.3', performanceScore: '1', + multisigScore: '', + validatorScore: '0.31067', + normalisedScore: '', + validatorStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, }, rankingScore: { stakeScore: '0.25', + performanceScore: '0.9998677767864936', + status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + previousStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + rankingScore: '', + votingPower: '', }, }, }, { node: { id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81', + stakedTotal: '4041343338923442976709', rewardScore: { rawValidatorScore: '0.35', performanceScore: '0.999629748500531', + multisigScore: '', + validatorScore: '', + normalisedScore: '', + validatorStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, }, rankingScore: { stakeScore: '0.2312', + performanceScore: '0.9998677767864936', + status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + previousStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + rankingScore: '', + votingPower: '', }, }, }, diff --git a/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.tsx b/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.tsx index c7e24613c..36b031e9e 100644 --- a/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.tsx +++ b/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.tsx @@ -7,12 +7,12 @@ import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { BigNumber } from '../../../../lib/bignumber'; import { + calculateOverallPenalty, + calculateOverstakedPenalty, + calculatesPerformancePenalty, getFormattedPerformanceScore, getLastEpochScoreAndPerformance, getNormalisedVotingPower, - getOverstakingPenalty, - getPerformancePenalty, - getTotalPenalties, getUnnormalisedVotingPower, } from '../../shared'; import { @@ -32,6 +32,7 @@ import type { ValidatorsTableProps } from './shared'; import { formatNumber, formatNumberPercentage, + removePaginationWrapper, toBigNum, } from '@vegaprotocol/utils'; import { VALIDATOR_LOGO_MAP } from './logo-map'; @@ -136,6 +137,10 @@ export const ConsensusValidatorsTable = ({ [totalStake] ); + const allNodesInPreviousEpoch = removePaginationWrapper( + previousEpochData?.epoch.validatorsConnection?.edges + ); + const nodes = useMemo(() => { if (!data) return []; let canonisedNodes = data @@ -160,7 +165,7 @@ export const ConsensusValidatorsTable = ({ stakedByDelegates, stakedByOperator, stakedTotal, - rankingScore: { stakeScore, votingPower }, + rankingScore: { stakeScore, votingPower, performanceScore }, pendingStake, stakedTotalRanking, stakedByUser, @@ -172,11 +177,8 @@ export const ConsensusValidatorsTable = ({ : avatarUrl ? avatarUrl : null; - const { - rawValidatorScore: previousEpochValidatorScore, - performanceScore: previousEpochPerformanceScore, - stakeScore: previousEpochStakeScore, - } = getLastEpochScoreAndPerformance(previousEpochData, id); + const { rawValidatorScore: previousEpochValidatorScore } = + getLastEpochScoreAndPerformance(previousEpochData, id); return { id, @@ -199,21 +201,19 @@ export const ConsensusValidatorsTable = ({ toBigNum(stakedByOperator, decimals), 2 ), - [ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore( - previousEpochPerformanceScore - ).toString(), - [ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty( - previousEpochPerformanceScore + [ValidatorFields.PERFORMANCE_SCORE]: + getFormattedPerformanceScore(performanceScore).toString(), + [ValidatorFields.PERFORMANCE_PENALTY]: formatNumberPercentage( + calculatesPerformancePenalty(performanceScore), + 2 ), - [ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty( - previousEpochValidatorScore, - previousEpochStakeScore + [ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage( + calculateOverstakedPenalty(id, allNodesInPreviousEpoch), + 2 ), - [ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties( - previousEpochValidatorScore, - previousEpochPerformanceScore, - stakedTotal, - totalStake + [ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage( + calculateOverallPenalty(id, allNodesInPreviousEpoch), + 2 ), [ValidatorFields.PENDING_STAKE]: pendingStake, [ValidatorFields.STAKED_BY_USER]: stakedByUser @@ -328,12 +328,12 @@ export const ConsensusValidatorsTable = ({ ...remaining, ]; }, [ + allNodesInPreviousEpoch, data, decimals, hideTopThird, previousEpochData, thirdOfTotalStake, - totalStake, validatorsView, ]); diff --git a/apps/governance/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx b/apps/governance/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx index 48a129b2d..9f12b7eda 100644 --- a/apps/governance/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx +++ b/apps/governance/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx @@ -5,15 +5,14 @@ import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { BigNumber } from '../../../../lib/bignumber'; import { + calculatesPerformancePenalty, + calculateOverallPenalty, + calculateOverstakedPenalty, getFormattedPerformanceScore, getLastEpochScoreAndPerformance, - getOverstakingPenalty, - getPerformancePenalty, - getTotalPenalties, } from '../../shared'; import { defaultColDef, - StakeNeededForPromotionRenderer, stakedTotalPercentage, ValidatorFields, ValidatorRenderer, @@ -28,6 +27,7 @@ import type { ValidatorsTableProps } from './shared'; import { formatNumber, formatNumberPercentage, + removePaginationWrapper, toBigNum, } from '@vegaprotocol/utils'; @@ -52,6 +52,10 @@ export const StandbyPendingValidatorsTable = ({ const gridRef = useRef(null); + const allNodesInPreviousEpoch = removePaginationWrapper( + previousEpochData?.epoch.validatorsConnection?.edges + ); + let nodes = useMemo(() => { if (!data) return []; @@ -77,18 +81,15 @@ export const StandbyPendingValidatorsTable = ({ stakedByDelegates, stakedByOperator, stakedTotal, - rankingScore: { stakeScore }, + rankingScore: { stakeScore, performanceScore }, pendingStake, stakedTotalRanking, stakedByUser, pendingUserStake, userStakeShare, }) => { - const { - rawValidatorScore: previousEpochValidatorScore, - performanceScore: previousEpochPerformanceScore, - stakeScore: previousEpochStakeScore, - } = getLastEpochScoreAndPerformance(previousEpochData, id); + const { performanceScore: previousEpochPerformanceScore } = + getLastEpochScoreAndPerformance(previousEpochData, id); let individualStakeNeededForPromotion, individualStakeNeededForPromotionDescription; @@ -144,21 +145,19 @@ export const StandbyPendingValidatorsTable = ({ toBigNum(stakedByOperator, decimals), 2 ), - [ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore( - previousEpochPerformanceScore - ).toString(), - [ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty( - previousEpochPerformanceScore + [ValidatorFields.PERFORMANCE_SCORE]: + getFormattedPerformanceScore(performanceScore).toString(), + [ValidatorFields.PERFORMANCE_PENALTY]: formatNumberPercentage( + calculatesPerformancePenalty(performanceScore), + 2 ), - [ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty( - previousEpochValidatorScore, - previousEpochStakeScore + [ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage( + calculateOverstakedPenalty(id, allNodesInPreviousEpoch), + 2 ), - [ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties( - previousEpochValidatorScore, - previousEpochPerformanceScore, - stakedTotal, - totalStake + [ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage( + calculateOverallPenalty(id, allNodesInPreviousEpoch), + 2 ), [ValidatorFields.PENDING_STAKE]: pendingStake, [ValidatorFields.STAKED_BY_USER]: stakedByUser @@ -172,13 +171,13 @@ export const StandbyPendingValidatorsTable = ({ } ); }, [ + allNodesInPreviousEpoch, data, decimals, previousEpochData, stakeNeededForPromotion, stakeNeededForPromotionDescription, t, - totalStake, ]); if (validatorsView === 'myStake') { @@ -226,21 +225,21 @@ export const StandbyPendingValidatorsTable = ({ cellRenderer: StakeShareRenderer, width: 100, }, - { - field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION, - headerName: t(ValidatorFields.STAKE_NEEDED_FOR_PROMOTION).toString(), - headerTooltip: t(stakeNeededForPromotionDescription, { - prefix: t('The'), - }), - cellRenderer: StakeNeededForPromotionRenderer, - width: 210, - }, + // { + // field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION, + // headerName: t(ValidatorFields.STAKE_NEEDED_FOR_PROMOTION).toString(), + // headerTooltip: t(stakeNeededForPromotionDescription, { + // prefix: t('The'), + // }), + // cellRenderer: StakeNeededForPromotionRenderer, + // width: 210, + // }, { field: ValidatorFields.TOTAL_PENALTIES, headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(), headerTooltip: t('TotalPenaltiesDescription').toString(), cellRenderer: TotalPenaltiesRenderer, - width: 120, + width: 120 + 210, }, ], [] diff --git a/apps/governance/src/routes/staking/node/validator-table.tsx b/apps/governance/src/routes/staking/node/validator-table.tsx index dc1427ff8..dcec7bb68 100644 --- a/apps/governance/src/routes/staking/node/validator-table.tsx +++ b/apps/governance/src/routes/staking/node/validator-table.tsx @@ -1,11 +1,15 @@ -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useEnvironment, DocsLinks, ExternalLinks, } from '@vegaprotocol/environment'; -import { toBigNum } from '@vegaprotocol/utils'; +import { + formatNumberPercentage, + removePaginationWrapper, + toBigNum, +} from '@vegaprotocol/utils'; import * as Schema from '@vegaprotocol/types'; import { Link as UTLink, @@ -24,11 +28,11 @@ import { SubHeading } from '../../../components/heading'; import { getLastEpochScoreAndPerformance, getNormalisedVotingPower, - getOverstakingPenalty, - getPerformancePenalty, - getTotalPenalties, getUnnormalisedVotingPower, getStakePercentage, + calculatesPerformancePenalty, + calculateOverstakedPenalty, + calculateOverallPenalty, } from '../shared'; import type { ReactNode } from 'react'; import type { StakingNodeFieldsFragment } from '../__generated__/Staking'; @@ -78,17 +82,27 @@ export const ValidatorTable = ({ const stakedOnNode = toBigNum(node.stakedTotal, decimals); - const { rawValidatorScore, performanceScore, stakeScore } = - getLastEpochScoreAndPerformance(previousEpochData, node.id); + const { rawValidatorScore } = getLastEpochScoreAndPerformance( + previousEpochData, + node.id + ); const stakePercentage = getStakePercentage(total, stakedOnNode); - const totalPenaltiesAmount = getTotalPenalties( - rawValidatorScore, - performanceScore, - stakedOnNode.toString(), - total.toString() - ); + const penalties = useMemo(() => { + const allNodesInPreviousEpoch = removePaginationWrapper( + previousEpochData?.epoch.validatorsConnection?.edges + ); + return { + // current epoch + performance: calculatesPerformancePenalty( + node.rankingScore.performanceScore + ), + // previous epoch + overstaked: calculateOverstakedPenalty(node.id, allNodesInPreviousEpoch), + overall: calculateOverallPenalty(node.id, allNodesInPreviousEpoch), + }; + }, [node, previousEpochData?.epoch.validatorsConnection?.edges]); return ( <> @@ -242,7 +256,7 @@ export const ValidatorTable = ({ - {getOverstakingPenalty(rawValidatorScore, stakeScore)} + {formatNumberPercentage(penalties.overstaked, 2)} @@ -251,7 +265,7 @@ export const ValidatorTable = ({ - {getPerformancePenalty(performanceScore)} + {formatNumberPercentage(penalties.performance, 2)} @@ -260,7 +274,7 @@ export const ValidatorTable = ({ {t('TOTAL PENALTIES')} - {totalPenaltiesAmount} + {formatNumberPercentage(penalties.overall, 2)} diff --git a/apps/governance/src/routes/staking/shared.spec.ts b/apps/governance/src/routes/staking/shared.spec.ts index c470b0461..135976e2b 100644 --- a/apps/governance/src/routes/staking/shared.spec.ts +++ b/apps/governance/src/routes/staking/shared.spec.ts @@ -9,6 +9,7 @@ import { getTotalPenalties, getStakePercentage, } from './shared'; +import * as Schema from '@vegaprotocol/types'; describe('getLastEpochScoreAndPerformance', () => { const mockPreviousEpochData = { @@ -19,24 +20,48 @@ describe('getLastEpochScoreAndPerformance', () => { { node: { id: '0x123', + stakedTotal: '', rewardScore: { rawValidatorScore: '0.25', performanceScore: '0.75', + multisigScore: '', + validatorScore: '', + normalisedScore: '', + validatorStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, }, rankingScore: { stakeScore: '0.25', + performanceScore: '0.75', + status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + previousStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + rankingScore: '', + votingPower: '', }, }, }, { node: { id: '0x234', + stakedTotal: '', rewardScore: { rawValidatorScore: '0.35', performanceScore: '0.85', + multisigScore: '', + validatorScore: '', + normalisedScore: '', + validatorStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, }, rankingScore: { stakeScore: '0.25', + performanceScore: '0.85', + status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + previousStatus: + Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT, + rankingScore: '', + votingPower: '', }, }, }, diff --git a/apps/governance/src/routes/staking/shared.ts b/apps/governance/src/routes/staking/shared.ts index 85a897a80..8e9ef9767 100644 --- a/apps/governance/src/routes/staking/shared.ts +++ b/apps/governance/src/routes/staking/shared.ts @@ -4,6 +4,94 @@ import { } from '@vegaprotocol/utils'; import type { PreviousEpochQuery } from './__generated__/PreviousEpoch'; import { BigNumber } from '../../lib/bignumber'; +import type { LastArrayElement } from 'type-fest'; + +type Node = NonNullable< + LastArrayElement< + NonNullable< + NonNullable['edges'] + > + > +>['node']; + +/** + * Calculates theoretical stake score for a given node + * @param nodeId Id of a node for which a score is calculated + * @param nodes A collection of all nodes + * @returns Theoretical stake score for given node based on the staked total + * of all node of the same type (status) + */ +const calculateTheoreticalStakeScore = (nodeId: string, nodes: Node[]) => { + const node = nodes.find((n) => n.id === nodeId); + if (!node) { + return new BigNumber(0); + } + const all = nodes + .filter((n) => n.rankingScore.status === node.rankingScore.status) + .map((n) => new BigNumber(n.stakedTotal)); + const sumOfSameType = all.reduce((acc, a) => acc.plus(a), new BigNumber(0)); + if (sumOfSameType.isZero()) { + return new BigNumber(0); + } + return new BigNumber(node.stakedTotal).dividedBy(sumOfSameType); +}; + +/** + * Calculates overall penalty for a given node + * @param nodeId Id of a node for which a penalty is calculated + * @param nodes A collection of all nodes - needed to calculate theoretical stake score + * @returns % + */ +export const calculateOverallPenalty = (nodeId: string, nodes: Node[]) => { + const node = nodes.find((n) => n.id === nodeId); + const tts = calculateTheoreticalStakeScore(nodeId, nodes); + if (!node || tts.isZero()) { + return new BigNumber(0); + } + const penalty = new BigNumber(1) + .minus(new BigNumber(node.rewardScore?.validatorScore || 0).dividedBy(tts)) + .times(100); + return penalty.isLessThan(0) ? new BigNumber(0) : penalty; +}; + +/** + * Calculates over-staked penalty for a given node + * @param nodeId Id of a node for which a penalty is calculated + * @param nodes A collection of all nodes - needed to calculate theoretical stake score + * @returns % + */ +export const calculateOverstakedPenalty = (nodeId: string, nodes: Node[]) => { + const node = nodes.find((n) => n.id === nodeId); + const tts = calculateTheoreticalStakeScore(nodeId, nodes); + if (!node || tts.isZero()) { + return new BigNumber(0); + } + const penalty = new BigNumber(1) + .minus( + new BigNumber(node.rewardScore?.rawValidatorScore || 0).dividedBy(tts) + ) + .times(100); + console.log( + nodeId, + new BigNumber(node.rewardScore?.rawValidatorScore || 0).toString(), + tts.toString(), + new BigNumber(node.rewardScore?.rawValidatorScore || 0) + .dividedBy(tts) + .toString() + ); + return penalty.isLessThan(0) ? new BigNumber(0) : penalty; +}; + +/** + * Calculates performance penalty based on the given performance score. + * @returns % + */ +export const calculatesPerformancePenalty = (performanceScore: string) => { + const penalty = new BigNumber(1) + .minus(new BigNumber(performanceScore)) + .times(100); + return penalty.isLessThan(0) ? new BigNumber(0) : penalty; +}; export const getLastEpochScoreAndPerformance = ( previousEpochData: PreviousEpochQuery | undefined, @@ -15,7 +103,7 @@ export const getLastEpochScoreAndPerformance = ( return { rawValidatorScore: validator?.rewardScore?.rawValidatorScore, - performanceScore: validator?.rewardScore?.performanceScore, + performanceScore: validator?.rankingScore?.performanceScore, stakeScore: validator?.rankingScore?.stakeScore, }; };