From 9c1c4ee96f27a5af6e3a8e6277b43f2e80064533 Mon Sep 17 00:00:00 2001 From: Arjun Porwal <65214523+ap211unitech@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:55:47 +0530 Subject: [PATCH] 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 --- packages/page-explorer/src/BlockHeader.tsx | 101 ++++++++++++++---- packages/page-explorer/src/BlockHeaders.tsx | 40 ++++--- .../page-explorer/src/Latency/useLatency.ts | 44 +++++++- packages/page-explorer/src/Main.tsx | 10 +- packages/page-explorer/src/Summary.tsx | 10 +- packages/react-hooks/src/ctx/BlockAuthors.tsx | 13 ++- packages/react-hooks/src/ctx/types.ts | 12 ++- packages/react-query/src/TimeNow.tsx | 5 +- 8 files changed, 181 insertions(+), 54 deletions(-) diff --git a/packages/page-explorer/src/BlockHeader.tsx b/packages/page-explorer/src/BlockHeader.tsx index 5a37a05996..7818292ed8 100644 --- a/packages/page-explorer/src/BlockHeader.tsx +++ b/packages/page-explorer/src/BlockHeader.tsx @@ -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 | 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} {type} ago + : <>{pre} {type} ago; +} +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 | null { return ( - - -

- {formatNumber(value.number)} -

- - {hashHex} - - {value.author && ( - - )} - - + <> + {headers.map((value, index) => { + const hashHex = value.hash.toHex(); + + return ( + + +

+ {formatNumber(value.number)} +

+ + {hashHex} + + {value.author && ( + + )} + + + {getDisplayValue((Date.now() - value.timestamp.toNumber()) / 1000)} + +
+ ); + })} + ); } +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); diff --git a/packages/page-explorer/src/BlockHeaders.tsx b/packages/page-explorer/src/BlockHeaders.tsx index f637b7501a..a0e1dd2bc6 100644 --- a/packages/page-explorer/src/BlockHeaders.tsx +++ b/packages/page-explorer/src/BlockHeaders.tsx @@ -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 { 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); + }, [headers]); + return ( - {headers - .filter((header) => !!header) - .map((header): React.ReactNode => ( - - ))} + {Object + .entries(groupedByTimestamp) + .map(([timestamp, headers]): React.ReactNode => { + return ( + !!e)} + key={timestamp} + /> + ); + })}
); } diff --git a/packages/page-explorer/src/Latency/useLatency.ts b/packages/page-explorer/src/Latency/useLatency.ts index b74af6837a..c9e749ecfc 100644 --- a/packages/page-explorer/src/Latency/useLatency.ts +++ b/packages/page-explorer/src/Latency/useLatency.ts @@ -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; } } diff --git a/packages/page-explorer/src/Main.tsx b/packages/page-explorer/src/Main.tsx index 628a206fff..c50fedcab2 100644 --- a/packages/page-explorer/src/Main.tsx +++ b/packages/page-explorer/src/Main.tsx @@ -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 { return ( <> - + diff --git a/packages/page-explorer/src/Summary.tsx b/packages/page-explorer/src/Summary.tsx index d9bf57aec7..2a32ed5fa3 100644 --- a/packages/page-explorer/src/Summary.tsx +++ b/packages/page-explorer/src/Summary.tsx @@ -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 && ( <> - + {/* Restart timer on key change */} + diff --git a/packages/react-hooks/src/ctx/BlockAuthors.tsx b/packages/react-hooks/src/ctx/BlockAuthors.tsx index c466b6ad17..eb40df38d5 100644 --- a/packages/react-hooks/src/ctx/BlockAuthors.tsx +++ b/packages/react-hooks/src/ctx/BlockAuthors.tsx @@ -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 { 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 => { + 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 index < MAX_HEADERS && old.number.unwrap().lt(blockNumber)) - .reduce((next, header): HeaderExtended[] => { + .reduce((next, header): AugmentedBlockHeader[] => { next.push(header); return next; diff --git a/packages/react-hooks/src/ctx/types.ts b/packages/react-hooks/src/ctx/types.ts index 10e936d3f9..e0eae90fea 100644 --- a/packages/react-hooks/src/ctx/types.ts +++ b/packages/react-hooks/src/ctx/types.ts @@ -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; eraPoints: Record; lastBlockAuthors: string[]; lastBlockNumber?: string; - lastHeader?: HeaderExtended; - lastHeaders: HeaderExtended[]; + lastHeader?: AugmentedBlockHeader; + lastHeaders: AugmentedBlockHeader[]; } export interface BlockEvents { diff --git a/packages/react-query/src/TimeNow.tsx b/packages/react-query/src/TimeNow.tsx index dd2a257700..61c5e2229d 100644 --- a/packages/react-query/src/TimeNow.tsx +++ b/packages/react-query/src/TimeNow.tsx @@ -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 { - const { api } = useApi(); - const timestamp = useCall(!value && api.query.timestamp?.now); + const timestamp = Date.now(); const [now, hasValue] = useMemo( () => [value || timestamp, !!(value || timestamp)],