Skip to content

Commit

Permalink
A0-4613: Support for Finality bans (#141)
Browse files Browse the repository at this point in the history
Those are the changes required to support aleph-node v15 version:
* `BanConfig` was renamed to `ProductionBanConfig`
* There's new entry `FinalityBanConfig`
* v14 event reason was renamed from `InsuficientUptime` to
`InsufficientProduction`
* there's new event in v15 which is called `InsufficentFinalization`
* there's small bug in which already lifted ban was shown as current
ban, classic `>` required but was `>=`

This change is not backwards compatible.
  • Loading branch information
Marcin-Radecki authored Feb 12, 2025
2 parents a4e66e7 + 56fd69a commit 2c003b9
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 46 deletions.
8 changes: 5 additions & 3 deletions packages/page-staking/src/Query/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright 2017-2025 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { SessionIndex } from '@polkadot/types/interfaces';
import type { INumber } from '@polkadot/types/types';
import type { u32 } from '@polkadot/types-codec';
import type { FutureCommittee } from '../Performance/useFutureSessionCommittee.js';

import React, { useCallback, useMemo, useRef, useState } from 'react';
Expand Down Expand Up @@ -35,7 +35,7 @@ function Query ({ className }: Props): React.ReactElement<Props> {
const { api } = useApi();
const { value } = useParams<{ value: string }>();
const [validatorId, setValidatorId] = useState<string | null>(value || null);
const underperformedValidatorSessionCount = useCall<u32>(
const underperformedValidatorSessionCount = useCall<SessionIndex>(
getCommitteeManagement(api).query.underperformedValidatorSessionCount,
[value]
);
Expand Down Expand Up @@ -181,7 +181,9 @@ function Query ({ className }: Props): React.ReactElement<Props> {
}
{value && !!isAlephChain && groupIndex === 0 &&
<SummaryBox className={className}>
<CardSummary label={t('Underperformed Session Count')}>
<CardSummary
label={t('Underperformed Production Session Count')}
>
{underperformedValidatorSessionCount?.toString()}
</CardSummary>
</SummaryBox>
Expand Down
2 changes: 1 addition & 1 deletion packages/page-staking/src/Suspensions/CurrentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function CurrentList ({ suspensions }: Props): React.ReactElement<Props> {
return suspensions?.filter(
({ address, suspensionLiftsInEra }) =>
!reservedValidators.find((value) => value === address) &&
(!activeOnly || sessionInfo === undefined || suspensionLiftsInEra >= sessionInfo.currentEra)
(!activeOnly || sessionInfo === undefined || suspensionLiftsInEra > sessionInfo.currentEra)
);
}, [suspensions, sessionInfo, reservedValidators, activeOnly]);

Expand Down
103 changes: 61 additions & 42 deletions packages/page-staking/src/Suspensions/Suspensions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright 2017-2025 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { u64, Vec } from '@polkadot/types';
import type { EventRecord, Hash } from '@polkadot/types/interfaces';
import type { u16, Vec } from '@polkadot/types';
import type { EraIndex, EventRecord, Hash, SessionIndex } from '@polkadot/types/interfaces';
import type { Perbill } from '@polkadot/types/interfaces/runtime';
import type { Codec } from '@polkadot/types/types';
import type { u32 } from '@polkadot/types-codec';
import type { SuspensionEvent } from './index.js';

import { useEffect, useMemo, useState } from 'react';
Expand All @@ -14,15 +14,27 @@ import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';

import useErasStartSessionIndexLookup from '../Performance/useErasStartSessionIndexLookup.js';

type SuspensionReasons = [string, string, number][];
interface ProductionBanConfig {
minimalExpectedPerformance: Perbill,
underperformedSessionCountThreshold: SessionIndex,
cleanSessionCounterDelay: SessionIndex,
banPeriod: EraIndex,
}

interface FinalityBanConfig {
minimalExpectedPerformance: u16,
underperformedSessionCountThreshold: SessionIndex,
cleanSessionCounterDelay: SessionIndex,
banPeriod: EraIndex,
}

function parseEvents (events: EventRecord[]): SuspensionReasons {
function parseEvents (events: EventRecord[], productionBanConfigPeriod: number, finalizationBanConfigPeriod: number): SuspensionEvent[] {
return events.filter(({ event }) => COMMITTEE_MANAGEMENT_NAMES.includes(event.section) && event.method === 'BanValidators')
.map(({ event }) => {
const raw = event.data[0] as unknown as Codec[][];

const reasons: SuspensionReasons = raw.map((value) => {
const account = value[0].toString();
return raw.map((value) => {
const address = value[0].toString();
const reasonAndEra = value[1].toHuman() as unknown as Record<string, Codec>;

const reasonTypeAndValue = reasonAndEra.reason as unknown as Record<string, string>;
Expand All @@ -31,39 +43,51 @@ function parseEvents (events: EventRecord[]): SuspensionReasons {
const era = Number(reasonAndEra.start.toString());

if (reasonType === 'OtherReason') {
return [account, reasonValue, era];
} else if (reasonType === 'InsufficientUptime') {
return [account, 'Insufficient uptime in at least ' + reasonValue + ' sessions', era];
return {
address,
era,
suspensionLiftsInEra: era + productionBanConfigPeriod,
suspensionReason: reasonValue
};
} else if (reasonType === 'InsufficientProduction') {
return {
address,
era,
suspensionLiftsInEra: era + productionBanConfigPeriod,
suspensionReason: `Insufficient block production in at least ${reasonValue} sessions`
};
} else if (reasonType === 'InsufficientFinalization') {
return {
address,
era,
suspensionLiftsInEra: era + finalizationBanConfigPeriod,
suspensionReason: `Insufficient finalization in at least ${reasonValue} sessions`
};
} else {
return [account, reasonType + ': ' + reasonValue, era];
return {
address,
era,
suspensionLiftsInEra: era + productionBanConfigPeriod,
suspensionReason: `${reasonType} : ${reasonValue}`
};
}
});

return reasons;
}).flat();
}

interface BanConfig {
minimalExpectedPerformance: u64,
underperformedSessionCountThreshold: u32,
cleanSessionCounterDelay: u32,
banPeriod: u32,
}

function useSuspensions (): SuspensionEvent[] | undefined {
const { api } = useApi();
// below logic is not able to detect kicks in blocks in which elections has failed,
// as staking.erasStartSessionIndex is not populated (new era does not start)
const erasStartSessionIndexLookup = useErasStartSessionIndexLookup();
const [electionBlockHashes, setElectionBlockHashes] = useState<Hash[] | undefined>(undefined);
const [eventsInBlocks, setEventsInBlocks] = useState<SuspensionReasons | undefined>(undefined);
const [eventsInBlocks, setEventsInBlocks] = useState<SuspensionEvent[] | undefined>(undefined);
const [suspensionEvents, setSuspensionEvents] = useState<SuspensionEvent[] | undefined>(undefined);
const banConfig = useCall<BanConfig>(getCommitteeManagement(api).query.banConfig);
const currentBanPeriod = useMemo(() => {
return banConfig?.banPeriod;
},
[banConfig]
);
const productionBanConfig = useCall<ProductionBanConfig>(getCommitteeManagement(api).query.productionBanConfig);

const currentProductionBanPeriod = productionBanConfig?.banPeriod;
const finalityBanConfig = useCall<FinalityBanConfig>(getCommitteeManagement(api).query.finalityBanConfig);
const currentFinalityBanPeriod = finalityBanConfig?.banPeriod;

const erasElectionsSessionIndexLookup = useMemo((): [number, number][] => {
return erasStartSessionIndexLookup
Expand Down Expand Up @@ -92,7 +116,7 @@ function useSuspensions (): SuspensionEvent[] | undefined {
);

useEffect(() => {
if (electionBlockHashes === undefined) {
if (electionBlockHashes === undefined || currentProductionBanPeriod === undefined || currentFinalityBanPeriod === undefined) {
return;
}

Expand All @@ -103,34 +127,29 @@ function useSuspensions (): SuspensionEvent[] | undefined {

Promise.all(promisesSystemEvents)
.then((events: Vec<EventRecord>[]) => {
const parsedEvents = parseEvents(events.map((vecOfEvents) => vecOfEvents.toArray()).flat());
const parsedEvents = parseEvents(
events.map((vecOfEvents) => vecOfEvents.toArray()).flat(),
currentProductionBanPeriod.toNumber(),
currentFinalityBanPeriod.toNumber()
);

setEventsInBlocks(parsedEvents);
}).catch(console.error);
}).catch(console.error);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[api, JSON.stringify(electionBlockHashes)]
[api, JSON.stringify(electionBlockHashes), currentProductionBanPeriod, currentFinalityBanPeriod]
);

useEffect(() => {
if (!currentBanPeriod) {
if (!currentProductionBanPeriod || !currentFinalityBanPeriod || !eventsInBlocks) {
return;
}

const events = eventsInBlocks?.map(([address, suspensionReason, era]) => {
return {
address,
era,
suspensionLiftsInEra: era + currentBanPeriod.toNumber(),
suspensionReason
};
}).reverse();

setSuspensionEvents(events);
setSuspensionEvents(eventsInBlocks.reverse());
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[api, JSON.stringify(eventsInBlocks), currentBanPeriod]
[api, JSON.stringify(eventsInBlocks), currentProductionBanPeriod]
);

return suspensionEvents;
Expand Down

0 comments on commit 2c003b9

Please sign in to comment.