diff --git a/packages/backend/src/Block.ts b/packages/backend/src/Block.ts new file mode 100644 index 0000000..3152a3b --- /dev/null +++ b/packages/backend/src/Block.ts @@ -0,0 +1,21 @@ +import { Types } from '@dotstats/common'; + +export default class Block { + public static readonly ZERO = new Block(0 as Types.BlockNumber, '' as Types.BlockHash); + + public readonly number: Types.BlockNumber; + public readonly hash: Types.BlockHash; + + constructor(number: Types.BlockNumber, hash: Types.BlockHash) { + this.number = number; + this.hash = hash; + } + + gt(other: Block): boolean { + return this.number > other.number; + } + + eq(other: Block): boolean { + return this.number === other.number && this.hash === other.hash; + } +} diff --git a/packages/backend/src/Chain.ts b/packages/backend/src/Chain.ts index d423636..d46c0d3 100644 --- a/packages/backend/src/Chain.ts +++ b/packages/backend/src/Chain.ts @@ -2,6 +2,7 @@ import * as EventEmitter from 'events'; import Node from './Node'; import Feed from './Feed'; import FeedSet from './FeedSet'; +import Block from './Block'; import { Maybe, Types, FeedMessage, NumStats } from '@dotstats/common'; const BLOCK_TIME_HISTORY = 10; @@ -14,6 +15,7 @@ export default class Chain { public readonly label: Types.ChainLabel; public height = 0 as Types.BlockNumber; + public finalized = Block.ZERO; public blockTimestamp = 0 as Types.Timestamp; private blockTimes = new NumStats(BLOCK_TIME_HISTORY); @@ -43,11 +45,13 @@ export default class Chain { }); node.events.on('block', () => this.updateBlock(node)); + node.events.on('finalized', () => this.updateFinalized(node)); node.events.on('stats', () => this.feeds.broadcast(Feed.stats(node))); node.events.on('hardware', () => this.feeds.broadcast(Feed.hardware(node))); node.events.on('location', (location) => this.feeds.broadcast(Feed.locatedNode(node, location))); this.updateBlock(node); + this.updateFinalized(node); } public addFeed(feed: Feed) { @@ -58,9 +62,11 @@ export default class Chain { feed.sendMessage(Feed.timeSync()); feed.sendMessage(Feed.bestBlock(this.height, this.blockTimestamp, this.averageBlockTime)); + feed.sendMessage(Feed.bestFinalizedBlock(this.finalized)); for (const node of this.nodes.values()) { feed.sendMessage(Feed.addedNode(node)); + feed.sendMessage(Feed.finalized(node)); } } @@ -81,9 +87,11 @@ export default class Chain { } private updateBlock(node: Node) { - if (node.height > this.height) { + const height = node.best.number; + + if (height > this.height) { // New best block - const { height, blockTimestamp } = node; + const { blockTimestamp } = node; if (this.blockTimestamp) { this.updateAverageBlockTime(height, blockTimestamp); @@ -100,14 +108,24 @@ export default class Chain { this.feeds.broadcast(Feed.bestBlock(this.height, this.blockTimestamp, this.averageBlockTime)); console.log(`[${this.label}] New block ${this.height}`); - } else if (node.height === this.height) { + } else if (height === this.height) { // Caught up to best block node.propagationTime = (node.blockTimestamp - this.blockTimestamp) as Types.PropagationTime; } this.feeds.broadcast(Feed.imported(node)); - console.log(`[${this.label}] ${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`); + console.log(`[${this.label}] ${node.name} imported ${height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`); + } + + private updateFinalized(node: Node) { + if (node.finalized.gt(this.finalized)) { + this.finalized = node.finalized; + + this.feeds.broadcast(Feed.bestFinalizedBlock(this.finalized)); + } + + this.feeds.broadcast(Feed.finalized(node)); } private updateAverageBlockTime(height: Types.BlockNumber, now: Types.Timestamp) { diff --git a/packages/backend/src/Feed.ts b/packages/backend/src/Feed.ts index ffb053c..15d09a1 100644 --- a/packages/backend/src/Feed.ts +++ b/packages/backend/src/Feed.ts @@ -2,6 +2,7 @@ import * as WebSocket from 'ws'; import * as EventEmitter from 'events'; import Node from './Node'; import Chain from './Chain'; +import Block from './Block'; import { VERSION, timestamp, Maybe, FeedMessage, Types, idGenerator } from '@dotstats/common'; import { Location } from './location'; @@ -42,6 +43,13 @@ export default class Feed { }; } + public static bestFinalizedBlock(block: Block): FeedMessage.Message { + return { + action: Actions.BestFinalized, + payload: [block.number, block.hash] + }; + } + public static addedNode(node: Node): FeedMessage.Message { return { action: Actions.AddedNode, @@ -70,6 +78,13 @@ export default class Feed { }; } + public static finalized(node: Node): FeedMessage.Message { + return { + action: Actions.FinalizedBlock, + payload: [node.id, node.finalized.number, node.finalized.hash] + }; + } + public static stats(node: Node): FeedMessage.Message { return { action: Actions.NodeStats, diff --git a/packages/backend/src/MeanList.ts b/packages/backend/src/MeanList.ts index 50f0c9d..504965c 100644 --- a/packages/backend/src/MeanList.ts +++ b/packages/backend/src/MeanList.ts @@ -1,6 +1,6 @@ import { Maybe, Types, timestamp } from '@dotstats/common'; -export class MeanList { +export default class MeanList { private periodCount = 0; private periodSum = 0; private meanIndex = 0; diff --git a/packages/backend/src/Node.ts b/packages/backend/src/Node.ts index 9ea7caf..5f957d4 100644 --- a/packages/backend/src/Node.ts +++ b/packages/backend/src/Node.ts @@ -4,7 +4,8 @@ import * as EventEmitter from 'events'; import { noop, timestamp, idGenerator, Maybe, Types, NumStats } from '@dotstats/common'; import { parseMessage, getBestBlock, Message, BestBlock, SystemInterval } from './message'; import { locate, Location } from './location'; -import { MeanList } from './MeanList'; +import MeanList from './MeanList'; +import Block from './Block'; const BLOCK_TIME_HISTORY = 10; const MEMORY_RECORDS = 20; @@ -32,8 +33,8 @@ export default class Node { public location: Maybe = null; public lastMessage: Types.Timestamp; public config: string; - public best = '' as Types.BlockHash; - public height = 0 as Types.BlockNumber; + public best = Block.ZERO; + public finalized = Block.ZERO; public latency = 0 as Types.Milliseconds; public blockTime = 0 as Types.Milliseconds; public blockTimestamp = 0 as Types.Timestamp; @@ -190,7 +191,7 @@ export default class Node { } public blockDetails(): Types.BlockDetails { - return [this.height, this.best, this.blockTime, this.blockTimestamp, this.propagationTime]; + return [this.best.number, this.best.hash, this.blockTime, this.blockTimestamp, this.propagationTime]; } public nodeLocation(): Maybe { @@ -234,7 +235,16 @@ export default class Node { } private onSystemInterval(message: SystemInterval) { - const { peers, txcount, cpu, memory, bandwidth_download: download, bandwidth_upload: upload } = message; + const { + peers, + txcount, + cpu, + memory, + bandwidth_download: download, + bandwidth_upload: upload, + finalized_height: finalized, + finalized_hash: finalizedHash + } = message; if (this.peers !== peers || this.txcount !== txcount) { this.peers = peers; @@ -243,6 +253,12 @@ export default class Node { this.events.emit('stats'); } + if (finalized != null && finalizedHash != null && finalized > this.finalized.number) { + this.finalized = new Block(finalized, finalizedHash); + + this.events.emit('finalized'); + } + if (cpu != null && memory != null) { const cpuChange = this.cpu.push(cpu); const memChange = this.memory.push(memory); @@ -279,11 +295,10 @@ export default class Node { private updateBestBlock(update: BestBlock) { const { height, ts: time, best } = update; - if (this.best !== best && this.height <= height) { + if (this.best.hash !== best && this.best.number <= height) { const blockTime = this.getBlockTime(time); - this.best = best; - this.height = height; + this.best = new Block(height, best); this.blockTimestamp = timestamp(); this.lastBlockAt = time; this.blockTimes.push(blockTime); diff --git a/packages/backend/src/message.ts b/packages/backend/src/message.ts index 2b27066..6621728 100644 --- a/packages/backend/src/message.ts +++ b/packages/backend/src/message.ts @@ -59,6 +59,8 @@ export interface SystemInterval extends BestBlock { status: 'Idle' | string; // TODO: 'Idle' | ...? bandwidth_upload: Maybe; bandwidth_download: Maybe; + finalized_height: Maybe; + finalized_hash: Maybe; } export interface NodeStart extends BestBlock { diff --git a/packages/common/src/SortedCollection.ts b/packages/common/src/SortedCollection.ts index d929c0a..b62a2a3 100644 --- a/packages/common/src/SortedCollection.ts +++ b/packages/common/src/SortedCollection.ts @@ -50,7 +50,7 @@ export function sortedInsert(item: T, into: Array, compare: Compare): n * * @return {number} index of the element, `-1` if not found */ -export function sortedIndexOf(item:T, within: Array, compare: Compare): number { +export function sortedIndexOf(item: T, within: Array, compare: Compare): number { if (within.length === 0) { return -1; } diff --git a/packages/common/src/feed.ts b/packages/common/src/feed.ts index b45f1e8..0a675b5 100644 --- a/packages/common/src/feed.ts +++ b/packages/common/src/feed.ts @@ -13,6 +13,7 @@ import { NodeHardware, NodeLocation, BlockNumber, + BlockHash, BlockDetails, Timestamp, Milliseconds, @@ -22,18 +23,20 @@ import { export const Actions = { FeedVersion : 0x00 as 0x00, BestBlock : 0x01 as 0x01, - AddedNode : 0x02 as 0x02, - RemovedNode : 0x03 as 0x03, - LocatedNode : 0x04 as 0x04, - ImportedBlock : 0x05 as 0x05, - NodeStats : 0x06 as 0x06, - NodeHardware : 0x07 as 0x07, - TimeSync : 0x08 as 0x08, - AddedChain : 0x09 as 0x09, - RemovedChain : 0x0A as 0x0A, - SubscribedTo : 0x0B as 0x0B, - UnsubscribedFrom : 0x0C as 0x0C, - Pong : 0x0D as 0x0D, + BestFinalized : 0x02 as 0x02, + AddedNode : 0x03 as 0x03, + RemovedNode : 0x04 as 0x04, + LocatedNode : 0x05 as 0x05, + ImportedBlock : 0x06 as 0x06, + FinalizedBlock : 0x07 as 0x07, + NodeStats : 0x08 as 0x08, + NodeHardware : 0x09 as 0x09, + TimeSync : 0x0A as 0x0A, + AddedChain : 0x0B as 0x0B, + RemovedChain : 0x0C as 0x0C, + SubscribedTo : 0x0D as 0x0D, + UnsubscribedFrom : 0x0E as 0x0E, + Pong : 0x0F as 0x0F, }; export type Action = typeof Actions[keyof typeof Actions]; @@ -54,6 +57,11 @@ export namespace Variants { payload: [BlockNumber, Timestamp, Maybe]; } + export interface BestFinalizedBlockMessage extends MessageBase { + action: typeof Actions.BestFinalized; + payload: [BlockNumber, BlockHash]; + } + export interface AddedNodeMessage extends MessageBase { action: typeof Actions.AddedNode; payload: [NodeId, NodeDetails, NodeStats, NodeHardware, BlockDetails, Maybe]; @@ -74,6 +82,11 @@ export namespace Variants { payload: [NodeId, BlockDetails]; } + export interface FinalizedBlockMessage extends MessageBase { + action: typeof Actions.FinalizedBlock; + payload: [NodeId, BlockNumber, BlockHash]; + } + export interface NodeStatsMessage extends MessageBase { action: typeof Actions.NodeStats; payload: [NodeId, NodeStats]; @@ -118,10 +131,12 @@ export namespace Variants { export type Message = | Variants.FeedVersionMessage | Variants.BestBlockMessage + | Variants.BestFinalizedBlockMessage | Variants.AddedNodeMessage | Variants.RemovedNodeMessage | Variants.LocatedNodeMessage | Variants.ImportedBlockMessage + | Variants.FinalizedBlockMessage | Variants.NodeStatsMessage | Variants.NodeHardwareMessage | Variants.TimeSyncMessage diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 96e7148..11bfd15 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -9,4 +9,4 @@ import * as FeedMessage from './feed'; export { Types, FeedMessage }; // Increment this if breaking changes were made to types in `feed.ts` -export const VERSION: Types.FeedVersion = 20 as Types.FeedVersion; +export const VERSION: Types.FeedVersion = 21 as Types.FeedVersion; diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index f42cfc4..d2ca238 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -31,6 +31,8 @@ export default class App extends React.Component<{}, State> { blocknumber: true, blockhash: true, blocktime: true, + finalized: false, + finalizedhash: false, blockpropagation: true, blocklasttime: false }, @@ -48,6 +50,7 @@ export default class App extends React.Component<{}, State> { this.state = { status: 'offline', best: 0 as Types.BlockNumber, + finalized: 0 as Types.BlockNumber, blockTimestamp: 0 as Types.Timestamp, blockAverage: null, timeDiff: 0 as Types.Milliseconds, diff --git a/packages/frontend/src/Connection.ts b/packages/frontend/src/Connection.ts index 5643ee5..d13904c 100644 --- a/packages/frontend/src/Connection.ts +++ b/packages/frontend/src/Connection.ts @@ -110,6 +110,14 @@ export class Connection { break; } + case Actions.BestFinalized: { + const [finalized /*, hash */] = message.payload; + + this.state = this.update({ finalized }); + + break; + } + case Actions.AddedNode: { const [id, nodeDetails, nodeStats, nodeHardware, blockDetails, location] = message.payload; const pinned = this.pins.has(nodeDetails[0]); @@ -144,6 +152,14 @@ export class Connection { break; } + case Actions.FinalizedBlock: { + const [id, height, hash] = message.payload; + + nodes.mut(id, (node) => node.updateFinalized(height, hash)); + + break; + } + case Actions.NodeStats: { const [id, nodeStats] = message.payload; diff --git a/packages/frontend/src/components/Chain/Chain.tsx b/packages/frontend/src/components/Chain/Chain.tsx index 1f1a7cb..af38968 100644 --- a/packages/frontend/src/components/Chain/Chain.tsx +++ b/packages/frontend/src/components/Chain/Chain.tsx @@ -7,6 +7,7 @@ import { Tile, Ago, List, Map, Settings } from '../'; import { PersistentObject, PersistentSet } from '../../persist'; import blockIcon from '../../icons/package.svg'; +import finalizedIcon from '../../icons/milestone.svg'; import blockTimeIcon from '../../icons/history.svg'; import lastTimeIcon from '../../icons/watch.svg'; import listIcon from '../../icons/list-alt-regular.svg'; @@ -51,13 +52,14 @@ export class Chain extends React.Component { public render() { const { appState } = this.props; - const { best, blockTimestamp, blockAverage } = appState; + const { best, finalized, blockTimestamp, blockAverage } = appState; const { display: currentTab } = this.state; return (
#{formatNumber(best)} + #{formatNumber(finalized)} { blockAverage == null ? '-' : secondsWithPrecision(blockAverage / 1000) }
diff --git a/packages/frontend/src/components/List/Row.tsx b/packages/frontend/src/components/List/Row.tsx index b197ec7..c35baf3 100644 --- a/packages/frontend/src/components/List/Row.tsx +++ b/packages/frontend/src/components/List/Row.tsx @@ -14,6 +14,7 @@ import nodeTypeIcon from '../../icons/terminal.svg'; import peersIcon from '../../icons/broadcast.svg'; import transactionsIcon from '../../icons/inbox.svg'; import blockIcon from '../../icons/package.svg'; +import finalizedIcon from '../../icons/milestone.svg'; import blockHashIcon from '../../icons/file-binary.svg'; import blockTimeIcon from '../../icons/history.svg'; import propagationTimeIcon from '../../icons/dashboard.svg'; @@ -233,6 +234,20 @@ export class Row extends React.Component { setting: 'blockhash', render: ({ hash }) => }, + { + label: 'Finalized Block', + icon: finalizedIcon, + width: 88, + setting: 'finalized', + render: ({ finalized }) => `#${formatNumber(finalized)}` + }, + { + label: 'Finalized Block Hash', + icon: blockHashIcon, + width: 154, + setting: 'finalizedhash', + render: ({ finalizedHash }) => + }, { label: 'Block Time', icon: blockTimeIcon, diff --git a/packages/frontend/src/components/List/Truncate.tsx b/packages/frontend/src/components/List/Truncate.tsx index fc44dd4..908ce17 100644 --- a/packages/frontend/src/components/List/Truncate.tsx +++ b/packages/frontend/src/components/List/Truncate.tsx @@ -13,6 +13,10 @@ export class Truncate extends React.Component { public render() { const { text, position, copy } = this.props; + if (!text) { + return '-'; + } + return (
{text}
diff --git a/packages/frontend/src/state.ts b/packages/frontend/src/state.ts index fcf7470..28c42ce 100644 --- a/packages/frontend/src/state.ts +++ b/packages/frontend/src/state.ts @@ -39,6 +39,9 @@ export class Node { public blockTimestamp: Types.Timestamp; public propagationTime: Maybe; + public finalized = 0 as Types.BlockNumber; + public finalizedHash = '' as Types.BlockHash; + public lat: Maybe; public lon: Maybe; public city: Maybe; @@ -106,6 +109,11 @@ export class Node { this.trigger(); } + public updateFinalized(height: Types.BlockNumber, hash: Types.BlockHash) { + this.finalized = height; + this.finalizedHash = hash; + } + public updateLocation(location: Types.NodeLocation) { const [lat, lon, city] = location; @@ -158,6 +166,8 @@ export namespace State { download: boolean; blocknumber: boolean; blockhash: boolean; + finalized: boolean; + finalizedhash: boolean; blocktime: boolean; blockpropagation: boolean; blocklasttime: boolean; @@ -167,6 +177,7 @@ export namespace State { export interface State { status: 'online' | 'offline' | 'upgrade-requested'; best: Types.BlockNumber; + finalized: Types.BlockNumber; blockTimestamp: Types.Timestamp; blockAverage: Maybe; timeDiff: Types.Milliseconds;