diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index 5b67dc965..d0d677740 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -5,9 +5,9 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first class Audit { auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }) - : { censored: string[], added: string[], fresh: string[], score: number } { + : { censored: string[], added: string[], fresh: string[], score: number, similarity: number } { if (!projectedBlocks?.[0]?.transactionIds || !mempool) { - return { censored: [], added: [], fresh: [], score: 0 }; + return { censored: [], added: [], fresh: [], score: 0, similarity: 1 }; } const matches: string[] = []; // present in both mined block and template @@ -16,6 +16,8 @@ class Audit { const isCensored = {}; // missing, without excuse const isDisplaced = {}; let displacedWeight = 0; + let matchedWeight = 0; + let projectedWeight = 0; const inBlock = {}; const inTemplate = {}; @@ -38,11 +40,16 @@ class Audit { isCensored[txid] = true; } displacedWeight += mempool[txid].weight; + } else { + matchedWeight += mempool[txid].weight; } + projectedWeight += mempool[txid].weight; inTemplate[txid] = true; } displacedWeight += (4000 - transactions[0].weight); + projectedWeight += transactions[0].weight; + matchedWeight += transactions[0].weight; // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs // these displaced transactions should occupy the first N weight units of the next projected block @@ -121,12 +128,14 @@ class Audit { const numCensored = Object.keys(isCensored).length; const numMatches = matches.length - 1; // adjust for coinbase tx const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0; + const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; return { censored: Object.keys(isCensored), added, fresh, - score + score, + similarity, }; } } diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index f762cfc2c..df97c0292 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,4 +1,4 @@ -import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; +import { CpfpInfo, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; import config from '../config'; import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { isIP } from 'net'; @@ -164,6 +164,30 @@ export class Common { return parents; } + // calculates the ratio of matched transactions to projected transactions by weight + static getSimilarity(projectedBlock: MempoolBlockWithTransactions, transactions: TransactionExtended[]): number { + let matchedWeight = 0; + let projectedWeight = 0; + const inBlock = {}; + + for (const tx of transactions) { + inBlock[tx.txid] = tx; + } + + // look for transactions that were expected in the template, but missing from the mined block + for (const tx of projectedBlock.transactions) { + if (inBlock[tx.txid]) { + matchedWeight += tx.vsize * 4; + } + projectedWeight += tx.vsize * 4; + } + + projectedWeight += transactions[0].weight; + matchedWeight += transactions[0].weight; + + return projectedWeight ? matchedWeight / projectedWeight : 1; + } + static getSqlInterval(interval: string | null): string | null { switch (interval) { case '24h': return '1 DAY'; diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index a96264825..c89179ce7 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -432,7 +432,7 @@ class WebsocketHandler { } if (Common.indexingEnabled() && memPool.isInSync()) { - const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); + const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const matchRate = Math.round(score * 100 * 100) / 100; const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { @@ -464,8 +464,14 @@ class WebsocketHandler { if (block.extras) { block.extras.matchRate = matchRate; + block.extras.similarity = similarity; } } + } else if (block.extras) { + const mBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); + if (mBlocks?.length && mBlocks[0].transactions) { + block.extras.similarity = Common.getSimilarity(mBlocks[0], transactions); + } } const removed: string[] = []; diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 8662770bc..9961632c3 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -153,6 +153,7 @@ export interface BlockExtension { feeRange: number[]; // fee rate percentiles reward: number; matchRate: number | null; + similarity?: number; pool: { id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id` name: string; diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html index 692f7d863..58d555657 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html @@ -2,7 +2,7 @@
-
+
 
diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 57fba7966..d48d1f299 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core'; -import { Subscription, Observable, fromEvent, merge, of, combineLatest, timer } from 'rxjs'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs'; import { MempoolBlock } from '../../interfaces/websocket.interface'; import { StateService } from '../../services/state.service'; import { Router } from '@angular/router'; @@ -9,11 +9,18 @@ import { specialBlocks } from '../../app.constants'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { Location } from '@angular/common'; import { DifficultyAdjustment } from '../../interfaces/node-api.interface'; +import { animate, style, transition, trigger } from '@angular/animations'; @Component({ selector: 'app-mempool-blocks', templateUrl: './mempool-blocks.component.html', styleUrls: ['./mempool-blocks.component.scss'], + animations: [trigger('blockEntryTrigger', [ + transition(':enter', [ + style({ transform: 'translateX(-155px)' }), + animate('2s 0s ease', style({ transform: '' })), + ]), + ])], changeDetection: ChangeDetectionStrategy.OnPush, }) export class MempoolBlocksComponent implements OnInit, OnDestroy { @@ -32,12 +39,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { isLoadingWebsocketSubscription: Subscription; blockSubscription: Subscription; networkSubscription: Subscription; + chainTipSubscription: Subscription; network = ''; now = new Date().getTime(); timeOffset = 0; showMiningInfo = false; timeLtrSubscription: Subscription; timeLtr: boolean; + animateEntry: boolean = false; blockWidth = 125; blockPadding = 30; @@ -53,6 +62,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { resetTransitionTimeout: number; + chainTip: number = -1; blockIndex = 1; constructor( @@ -69,6 +79,8 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { } ngOnInit() { + this.chainTip = this.stateService.latestBlockHeight; + if (['', 'testnet', 'signet'].includes(this.stateService.network)) { this.enabledMiningInfoIfNeeded(this.location.path()); this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url)); @@ -153,11 +165,24 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { this.blockSubscription = this.stateService.blocks$ .subscribe(([block]) => { - if (block?.extras?.matchRate >= 66 && !this.tabHidden) { + if (this.chainTip === -1) { + this.animateEntry = block.height === this.stateService.latestBlockHeight; + } else { + this.animateEntry = block.height > this.chainTip; + } + + this.chainTip = this.stateService.latestBlockHeight; + if ((block?.extras?.similarity == null || block?.extras?.similarity > 0.5) && !this.tabHidden) { this.blockIndex++; } }); + this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => { + if (this.chainTip === -1) { + this.chainTip = height; + } + }); + this.networkSubscription = this.stateService.networkChanged$ .subscribe((network) => this.network = network); @@ -193,11 +218,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { this.blockSubscription.unsubscribe(); this.networkSubscription.unsubscribe(); this.timeLtrSubscription.unsubscribe(); + this.chainTipSubscription.unsubscribe(); clearTimeout(this.resetTransitionTimeout); } trackByFn(index: number, block: MempoolBlock) { - return block.index; + return (block.isStack) ? 'stack' : block.index; } reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { @@ -214,6 +240,9 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { lastBlock.medianFee = this.median(lastBlock.feeRange); lastBlock.totalFees += block.totalFees; } + if (blocks.length) { + blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize; + } return blocks; } @@ -332,4 +361,4 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { } return emptyBlocks; } -} +} \ No newline at end of file diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index cad623f9f..024376ca6 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -118,6 +118,7 @@ export interface BlockExtension { reward?: number; coinbaseRaw?: string; matchRate?: number; + similarity?: number; pool?: { id: number; name: string; diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 96f7530c9..46416857e 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -43,6 +43,7 @@ export interface MempoolBlock { totalFees: number; feeRange: number[]; index: number; + isStack?: boolean; } export interface MempoolBlockWithTransactions extends MempoolBlock {