Feat/1541: Hide top third of validators to promote decentralisation (#1626)
* Feat/1541: Hide top third of validators to promote decentralisation * Feat/1541: Hide top third of validators * Feat/1541: Only top validators if 3 or more
This commit is contained in:
parent
e6c19cf9b1
commit
178597dc01
@ -1,4 +1,4 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import { NodeList, NODES_QUERY } from './node-list';
|
import { NodeList, NODES_QUERY } from './node-list';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
@ -15,7 +15,7 @@ jest.mock('../../components/epoch-countdown', () => ({
|
|||||||
const nodeFactory = (overrides?: PartialDeep<Nodes_nodes>) => {
|
const nodeFactory = (overrides?: PartialDeep<Nodes_nodes>) => {
|
||||||
const defaultNode = {
|
const defaultNode = {
|
||||||
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
||||||
name: 'Skynet',
|
name: 'high',
|
||||||
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
|
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
|
||||||
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
|
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
|
||||||
infoUrl: 'https://en.wikipedia.org/wiki/Skynet_(Terminator)',
|
infoUrl: 'https://en.wikipedia.org/wiki/Skynet_(Terminator)',
|
||||||
@ -32,7 +32,7 @@ const nodeFactory = (overrides?: PartialDeep<Nodes_nodes>) => {
|
|||||||
rankingScore: '0.67845061012234727427532760837568',
|
rankingScore: '0.67845061012234727427532760837568',
|
||||||
stakeScore: '0.3392701644525644',
|
stakeScore: '0.3392701644525644',
|
||||||
performanceScore: '0.9998677767864936',
|
performanceScore: '0.9998677767864936',
|
||||||
votingPower: '2407',
|
votingPower: '3500',
|
||||||
status: 'tendermint',
|
status: 'tendermint',
|
||||||
__typename: 'RankingScore',
|
__typename: 'RankingScore',
|
||||||
},
|
},
|
||||||
@ -46,7 +46,7 @@ const MOCK_NODES = {
|
|||||||
nodeFactory(),
|
nodeFactory(),
|
||||||
nodeFactory({
|
nodeFactory({
|
||||||
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
|
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
|
||||||
name: 'T-800 Terminator',
|
name: 'medium',
|
||||||
pubkey:
|
pubkey:
|
||||||
'ccc3b8362c25b09d20df8ea407b0a476d6b24a0e72bc063d0033c8841652ddd4',
|
'ccc3b8362c25b09d20df8ea407b0a476d6b24a0e72bc063d0033c8841652ddd4',
|
||||||
stakedTotal: '9618711883996159534058',
|
stakedTotal: '9618711883996159534058',
|
||||||
@ -62,7 +62,7 @@ const MOCK_NODES = {
|
|||||||
}),
|
}),
|
||||||
nodeFactory({
|
nodeFactory({
|
||||||
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
|
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
|
||||||
name: 'NCC-1701-E',
|
name: 'low',
|
||||||
pubkey:
|
pubkey:
|
||||||
'0931a8fd8cc935458f470e435a05414387cea6f329d648be894fcd44bd517a2b',
|
'0931a8fd8cc935458f470e435a05414387cea6f329d648be894fcd44bd517a2b',
|
||||||
stakedTotal: '4041343338923442976709',
|
stakedTotal: '4041343338923442976709',
|
||||||
@ -129,20 +129,30 @@ afterAll(() => {
|
|||||||
describe('Nodes list', () => {
|
describe('Nodes list', () => {
|
||||||
it('should render epoch info', async () => {
|
it('should render epoch info', async () => {
|
||||||
renderNodeList();
|
renderNodeList();
|
||||||
await waitFor(() => {
|
expect(await screen.findByTestId('epoch-info')).toBeInTheDocument();
|
||||||
expect(screen.getByText(MOCK_NODES.nodes[0].name)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
expect(screen.getByTestId('epoch-info')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a list of all nodes', async () => {
|
it('should initially render a list of nodes with the top third hidden', async () => {
|
||||||
renderNodeList();
|
renderNodeList();
|
||||||
|
|
||||||
await waitFor(() => {
|
expect(await screen.findByText('medium')).toBeInTheDocument();
|
||||||
expect(screen.getByText(MOCK_NODES.nodes[0].name)).toBeInTheDocument();
|
expect(screen.getByText('low')).toBeInTheDocument();
|
||||||
expect(screen.getByText(MOCK_NODES.nodes[1].name)).toBeInTheDocument();
|
expect(screen.queryByText('high')).toBeNull();
|
||||||
expect(screen.getByText(MOCK_NODES.nodes[2].name)).toBeInTheDocument();
|
});
|
||||||
|
|
||||||
|
it('should show all validators if button is pressed', async () => {
|
||||||
|
renderNodeList();
|
||||||
|
|
||||||
|
expect(await screen.findByText('medium')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('high')).toBeNull();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByTestId('show-all-validators'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('high')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('medium')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('low')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display the correctly formatted fields in the correct columns', async () => {
|
it('should display the correctly formatted fields in the correct columns', async () => {
|
||||||
@ -179,12 +189,15 @@ describe('Nodes list', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderNodeList(MOCK_NODE);
|
renderNodeList(MOCK_NODE);
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(MOCK_NODE.nodes[0].name)).toBeInTheDocument();
|
expect(await screen.findByTestId('validators-grid')).toBeInTheDocument();
|
||||||
});
|
|
||||||
|
|
||||||
const grid = screen.getByTestId('validators-grid');
|
const grid = screen.getByTestId('validators-grid');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByTestId('show-all-validators'));
|
||||||
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
grid.querySelector('[role="gridcell"][col-id="validator"]')
|
grid.querySelector('[role="gridcell"][col-id="validator"]')
|
||||||
).toHaveTextContent('T-800 Terminator');
|
).toHaveTextContent('T-800 Terminator');
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { gql, useQuery } from '@apollo/client';
|
import { gql, useQuery } from '@apollo/client';
|
||||||
import { forwardRef, useEffect, useMemo, useRef } from 'react';
|
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AgGridDynamic as AgGrid,
|
AgGridDynamic as AgGrid,
|
||||||
AsyncRenderer,
|
AsyncRenderer,
|
||||||
|
Button,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -60,6 +61,23 @@ interface ValidatorRendererProps {
|
|||||||
data: { validator: { avatarUrl: string; name: string } };
|
data: { validator: { avatarUrl: string; name: string } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CanonisedNodeProps {
|
||||||
|
id: string;
|
||||||
|
[VALIDATOR]: {
|
||||||
|
avatarUrl: string | null;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
[STATUS]: string;
|
||||||
|
[TOTAL_STAKE_THIS_EPOCH]: string;
|
||||||
|
[SHARE]: string;
|
||||||
|
[VALIDATOR_STAKE]: string;
|
||||||
|
[PENDING_STAKE]: string;
|
||||||
|
[RANKING_SCORE]: string;
|
||||||
|
[STAKE_SCORE]: string;
|
||||||
|
[PERFORMANCE_SCORE]: string;
|
||||||
|
[VOTING_POWER]: string;
|
||||||
|
}
|
||||||
|
|
||||||
const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
|
const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
|
||||||
const { avatarUrl, name } = data.validator;
|
const { avatarUrl, name } = data.validator;
|
||||||
return (
|
return (
|
||||||
@ -92,6 +110,7 @@ export const NodeList = ({ epoch }: NodeListProps) => {
|
|||||||
errorPolicy: 'ignore',
|
errorPolicy: 'ignore',
|
||||||
});
|
});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [hideTopThird, setHideTopThird] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const epochInterval = setInterval(() => {
|
const epochInterval = setInterval(() => {
|
||||||
@ -113,7 +132,7 @@ export const NodeList = ({ epoch }: NodeListProps) => {
|
|||||||
const nodes = useMemo(() => {
|
const nodes = useMemo(() => {
|
||||||
if (!data?.nodes) return [];
|
if (!data?.nodes) return [];
|
||||||
|
|
||||||
return data.nodes.map(
|
const canonisedNodes = data.nodes.map(
|
||||||
({
|
({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@ -166,7 +185,39 @@ export const NodeList = ({ epoch }: NodeListProps) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [data, t]);
|
|
||||||
|
if (canonisedNodes.length < 3 || !hideTopThird) {
|
||||||
|
return canonisedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedByVotingPower = canonisedNodes.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new BigNumber(b[VOTING_POWER]).toNumber() -
|
||||||
|
new BigNumber(a[VOTING_POWER]).toNumber()
|
||||||
|
);
|
||||||
|
|
||||||
|
// The point of identifying and hiding the group that could halt the network
|
||||||
|
// is that we assume the top 1/3 of stake is held by considerably less than
|
||||||
|
// 1/3 of the validators and we really want people not to stake any more to
|
||||||
|
// that group, because we want to make it require as many difference
|
||||||
|
// validators to collude as possible to halt the network, so we hide them.
|
||||||
|
const removeTopThirdOfStakeScores = sortedByVotingPower.reduce(
|
||||||
|
(acc, node) => {
|
||||||
|
if (acc.cumulativeScore < 3333) {
|
||||||
|
acc.cumulativeScore += Number(node[VOTING_POWER]);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
acc.remaining.push(node);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ remaining: [], cumulativeScore: 0 } as {
|
||||||
|
remaining: CanonisedNodeProps[];
|
||||||
|
cumulativeScore: number;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return removeTopThirdOfStakeScores.remaining;
|
||||||
|
}, [data, t, hideTopThird]);
|
||||||
|
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
|
||||||
@ -247,6 +298,30 @@ export const NodeList = ({ epoch }: NodeListProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="validators-grid">
|
<div data-testid="validators-grid">
|
||||||
|
{hideTopThird && (
|
||||||
|
<div className="mb-6 py-4 px-4 md:px-12 bg-neutral-900 text-sm text-center">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Button
|
||||||
|
data-testid="show-all-validators"
|
||||||
|
icon="list"
|
||||||
|
className="inline-flex items-center"
|
||||||
|
onClick={() => setHideTopThird(false)}
|
||||||
|
>
|
||||||
|
{t('Reveal top validators')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{t(
|
||||||
|
'Validators with too great a stake share will have the staking rewards for their delegators penalised.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="mb-0">
|
||||||
|
{t(
|
||||||
|
'To avoid penalties and increase decentralisation of the network, delegate to validators below.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<AgGrid
|
<AgGrid
|
||||||
domLayout="autoHeight"
|
domLayout="autoHeight"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
@ -43,7 +43,7 @@ export const Staking = ({ data }: { data?: StakingQueryResult }) => {
|
|||||||
</Callout>
|
</Callout>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl uppercase">{t('Nodes')}</h2>
|
<h2 className="text-2xl uppercase">{t('Validator nodes')}</h2>
|
||||||
<NodeList data-testid="node-list" epoch={data?.epoch} />
|
<NodeList data-testid="node-list" epoch={data?.epoch} />
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
Loading…
Reference in New Issue
Block a user