Improve block time handling for chains with elastic scaling (#11876)

* fix: last block timer

* refactor: update BlockHeader and BlockHeaders to use AugmentedBlockHeader type and add blockTime calculation

* refactor: remove isLast prop from BlockHeader and update BlockHeaders mapping
fix: update label in Summary component from 'target' to 'slot'
fix: improve block time calculation in BlockAuthors context

* refactor: update BlockHeader to accept multiple headers and group by timestamp in BlockHeaders

* refactor: update BlockHeader time formatting and adjust display value units

* refactor: simplify BlockAuthorsCtxRoot by removing blockTime calculation and parent timestamp retrieval

* feat: enhance calcDelay function to handle elastic scaling and distribute delays across blocks
This commit is contained in:
Arjun Porwal 2025-10-06 09:55:47 +05:30 committed by GitHub
parent bf15606cf9
commit 9c1c4ee96f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 181 additions and 54 deletions

View File

@ -1,40 +1,99 @@
// Copyright 2017-2025 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HeaderExtended } from '@polkadot/api-derive/types';
import type { AugmentedBlockHeader } from '@polkadot/react-hooks/ctx/types';
import React from 'react';
import { Link } from 'react-router-dom';
import { AddressSmall } from '@polkadot/react-components';
import { AddressSmall, styled } from '@polkadot/react-components';
import { formatNumber } from '@polkadot/util';
interface Props {
value: HeaderExtended;
headers: AugmentedBlockHeader[];
}
function BlockHeader ({ value }: Props): React.ReactElement<Props> | null {
if (!value) {
return null;
}
function formatValue (value: number, type = 's', withDecimal = false): React.ReactNode {
const [pre, post] = value.toFixed().split('.');
const hashHex = value.hash.toHex();
return withDecimal && post?.trim()?.length > 0
? <>{pre}.{post} <span className='timeUnit'>{type} ago</span></>
: <>{pre} <span className='timeUnit'>{type} ago</span></>;
}
function getDisplayValue (elapsed: number): React.ReactNode {
return (elapsed < 60)
? formatValue(elapsed, 'secs', elapsed < 15)
: (elapsed < 3600)
? formatValue(elapsed / 60, 'mins')
: formatValue(elapsed / 3600, 'hrs');
}
function BlockHeader ({ headers }: Props): React.ReactElement<Props> | null {
return (
<tr>
<td className='number'>
<h4 className='--digits'>
<Link to={`/explorer/query/${hashHex}`}>{formatNumber(value.number)}</Link>
</h4>
</td>
<td className='all hash overflow'>{hashHex}</td>
<td className='address'>
{value.author && (
<AddressSmall value={value.author} />
)}
</td>
</tr>
<>
{headers.map((value, index) => {
const hashHex = value.hash.toHex();
return (
<StyledTr
className='isExpanded'
isFirstItem={index === 0}
isLastItem={index === headers.length - 1}
key={value.number.toString()}
>
<td className='number'>
<h4 className='--digits'>
<Link to={`/explorer/query/${hashHex}`}>{formatNumber(value.number)}</Link>
</h4>
</td>
<td className='all hash overflow'>{hashHex}</td>
<td className='address'>
{value.author && (
<AddressSmall value={value.author} />
)}
</td>
<td
className='all --digits blockTime'
key={Date.now()}
>
{getDisplayValue((Date.now() - value.timestamp.toNumber()) / 1000)}
</td>
</StyledTr>
);
})}
</>
);
}
const BASE_BORDER = 0.125;
const BORDER_TOP = `${BASE_BORDER * 3}rem solid var(--bg-page)`;
const BORDER_RADIUS = `${BASE_BORDER * 4}rem`;
const StyledTr = styled.tr<{isFirstItem: boolean; isLastItem: boolean}>`
.blockTime {
text-align: right;
font-style: italic;
}
td {
border-top: ${(props) => props.isFirstItem && BORDER_TOP};
border-radius: 0rem !important;
&:first-child {
border-top-left-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important;
border-bottom-left-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important;
}
&:last-child {
border-top-right-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important;
border-bottom-right-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important;
}
.timeUnit {
font-size: var(--font-percent-small);
}
}
`;
export default React.memo(BlockHeader);

View File

@ -1,9 +1,9 @@
// Copyright 2017-2025 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HeaderExtended } from '@polkadot/api-derive/types';
import type { AugmentedBlockHeader } from '@polkadot/react-hooks/ctx/types';
import React, { useRef } from 'react';
import React, { useMemo, useRef } from 'react';
import { Table } from '@polkadot/react-components';
@ -11,29 +11,45 @@ import BlockHeader from './BlockHeader.js';
import { useTranslation } from './translate.js';
interface Props {
headers: HeaderExtended[];
headers: AugmentedBlockHeader[];
}
function BlockHeaders ({ headers }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t('recent blocks'), 'start', 3]
[t('recent blocks'), 'start', 4]
]);
const groupedByTimestamp = useMemo(() => {
return headers.reduce((acc, product) => {
const timestamp = product.timestamp.toString();
if (!acc[timestamp]) {
acc[timestamp] = [];
}
acc[timestamp].push(product);
return acc;
}, {} as Record<string, AugmentedBlockHeader[]>);
}, [headers]);
return (
<Table
empty={t('No blocks available')}
header={headerRef.current}
>
{headers
.filter((header) => !!header)
.map((header): React.ReactNode => (
<BlockHeader
key={header.number.toString()}
value={header}
/>
))}
{Object
.entries(groupedByTimestamp)
.map(([timestamp, headers]): React.ReactNode => {
return (
<BlockHeader
headers={headers.filter((e) => !!e)}
key={timestamp}
/>
);
})}
</Table>
);
}

View File

@ -30,6 +30,10 @@ function getSetter ({ extrinsics }: Block): GenericExtrinsic | undefined {
);
}
/**
* Calculates the delay for each block, correctly handling elastic scaling
* by distributing the time difference across all blocks produced between two differing slots.
*/
function calcDelay (details: Detail[]): Detail[] {
const filtered = details
.sort((a, b) => a.block.number - b.block.number)
@ -42,8 +46,44 @@ function calcDelay (details: Detail[]): Detail[] {
const a = filtered[i];
const b = filtered[i + 1];
if ((b.block.number - a.block.number) === 1 && b.delay === 0) {
b.delay = b.now - a.now;
// Check if the current block 'a' and the next block 'b' share the same timestamp.
// If they do, 'a' cannot be the slot anchor for 'b', so we skip 'a' and let the loop continue
// to find the true anchor later when 'b' or a subsequent block is processed.
if (a.now === b.now) {
continue;
}
// At this point, 'a' is the last block of the previous slot
// and 'b' is the first block of the new slot.
const delta = b.now - a.now;
if (delta < 0) {
b.delay = 0;
continue;
}
let k = i + 1;
// Find the end of the current slot
while (k < filtered.length && filtered[k].now === b.now) {
k++;
}
const lastBlockInSlot = filtered[k - 1];
const blocksInWindow = lastBlockInSlot.block.number - a.block.number;
if (blocksInWindow <= 0) {
b.delay = delta;
continue;
}
// Calculate the distributed delay (Delta t / N)
const distributedDelay = delta / blocksInWindow;
// Apply the distributed delay to ALL blocks in the current slot/window
for (let m = i + 1; m < k; m++) {
filtered[m].delay = distributedDelay;
}
}

View File

@ -1,8 +1,7 @@
// Copyright 2017-2025 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HeaderExtended } from '@polkadot/api-derive/types';
import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import type { AugmentedBlockHeader, KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import React from 'react';
@ -16,14 +15,17 @@ import Summary from './Summary.js';
interface Props {
eventCount: number;
events: KeyedEvent[];
headers: HeaderExtended[];
headers: AugmentedBlockHeader[];
}
function Main ({ eventCount, events, headers }: Props): React.ReactElement<Props> {
return (
<>
<Query />
<Summary eventCount={eventCount} />
<Summary
eventCount={eventCount}
headers={headers}
/>
<Columar>
<Columar.Column>
<BlockHeaders headers={headers} />

View File

@ -1,6 +1,8 @@
// Copyright 2017-2025 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AugmentedBlockHeader } from '@polkadot/react-hooks/ctx/types';
import React from 'react';
import { CardSummary, SummaryBox } from '@polkadot/react-components';
@ -13,9 +15,10 @@ import { useTranslation } from './translate.js';
interface Props {
eventCount: number;
headers: AugmentedBlockHeader[];
}
function Summary ({ eventCount }: Props): React.ReactElement {
function Summary ({ eventCount, headers }: Props): React.ReactElement {
const { t } = useTranslation();
const { api } = useApi();
@ -25,11 +28,12 @@ function Summary ({ eventCount }: Props): React.ReactElement {
{api.query.timestamp && (
<>
<CardSummary label={t('last block')}>
<TimeNow />
{/* Restart timer on key change */}
<TimeNow key={headers.at(0)?.hash.toHex()} />
</CardSummary>
<CardSummary
className='media--800'
label={t('target')}
label={t('slot')}
>
<BlockToTime value={BN_ONE} />
</CardSummary>

View File

@ -3,7 +3,7 @@
import type { HeaderExtended } from '@polkadot/api-derive/types';
import type { EraRewardPoints } from '@polkadot/types/interfaces';
import type { BlockAuthors } from './types.js';
import type { AugmentedBlockHeader, BlockAuthors } from './types.js';
import React, { useEffect, useState } from 'react';
@ -31,13 +31,16 @@ export function BlockAuthorsCtxRoot ({ children }: Props): React.ReactElement<Pr
// No unsub, global context - destroyed on app close
useEffect((): void => {
api.isReady.then((): void => {
let lastHeaders: HeaderExtended[] = [];
let lastHeaders: AugmentedBlockHeader[] = [];
let lastBlockAuthors: string[] = [];
let lastBlockNumber = '';
// subscribe to new headers
api.derive.chain.subscribeNewHeads((lastHeader: HeaderExtended): void => {
if (lastHeader?.number) {
api.derive.chain.subscribeNewHeads(async (header: HeaderExtended): Promise<void> => {
if (header?.number) {
const timestamp = await ((await api.at(header.hash)).query.timestamp.now());
const lastHeader = Object.assign(header, { timestamp }) as AugmentedBlockHeader;
const blockNumber = lastHeader.number.unwrap();
let thisBlockAuthor = '';
@ -60,7 +63,7 @@ export function BlockAuthorsCtxRoot ({ children }: Props): React.ReactElement<Pr
lastHeaders = lastHeaders
.filter((old, index) => index < MAX_HEADERS && old.number.unwrap().lt(blockNumber))
.reduce((next, header): HeaderExtended[] => {
.reduce((next, header): AugmentedBlockHeader[] => {
next.push(header);
return next;

View File

@ -8,7 +8,7 @@ import type { HeaderExtended } from '@polkadot/api-derive/types';
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import type { InjectedExtension } from '@polkadot/extension-inject/types';
import type { ProviderStats } from '@polkadot/rpc-provider/types';
import type { BlockNumber, EventRecord } from '@polkadot/types/interfaces';
import type { BlockNumber, EventRecord, Moment } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';
import type { AssetInfoComplete } from '../types.js';
@ -75,13 +75,19 @@ export interface PayWithAsset {
selectedFeeAsset: AssetInfoComplete | null;
}
interface BlockTime {
readonly timestamp: Moment;
}
export type AugmentedBlockHeader = HeaderExtended & BlockTime;
export interface BlockAuthors {
byAuthor: Record<string, string>;
eraPoints: Record<string, string>;
lastBlockAuthors: string[];
lastBlockNumber?: string;
lastHeader?: HeaderExtended;
lastHeaders: HeaderExtended[];
lastHeader?: AugmentedBlockHeader;
lastHeaders: AugmentedBlockHeader[];
}
export interface BlockEvents {

View File

@ -5,8 +5,6 @@ import type { Moment } from '@polkadot/types/interfaces';
import React, { useMemo } from 'react';
import { useApi, useCall } from '@polkadot/react-hooks';
import Elapsed from './Elapsed.js';
interface Props {
@ -17,8 +15,7 @@ interface Props {
}
function TimeNow ({ children, className = '', label, value }: Props): React.ReactElement<Props> {
const { api } = useApi();
const timestamp = useCall<Moment>(!value && api.query.timestamp?.now);
const timestamp = Date.now();
const [now, hasValue] = useMemo(
() => [value || timestamp, !!(value || timestamp)],