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 @@