From 81ec54fcb3eca267f4b6c05eeb676c8d16724e30 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 17 May 2023 11:46:50 -0400 Subject: [PATCH] Omit possible high-sigop txs from block health score --- backend/src/api/audit.ts | 16 ++++++++++++++-- backend/src/api/database-migration.ts | 7 ++++++- backend/src/api/websocket-handler.ts | 3 ++- backend/src/mempool.interfaces.ts | 1 + .../src/repositories/BlocksAuditsRepository.ts | 9 +++++---- .../components/block-overview-graph/tx-view.ts | 3 ++- .../block-overview-tooltip.component.html | 1 + .../src/app/components/block/block.component.ts | 6 +++++- .../src/app/interfaces/node-api.interface.ts | 2 +- .../src/app/interfaces/websocket.interface.ts | 2 +- 10 files changed, 38 insertions(+), 12 deletions(-) diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index 7435e3b99..1ec1ae65a 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -6,14 +6,15 @@ 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, similarity: number } { + : { censored: string[], added: string[], fresh: string[], sigop: string[], score: number, similarity: number } { if (!projectedBlocks?.[0]?.transactionIds || !mempool) { - return { censored: [], added: [], fresh: [], score: 0, similarity: 1 }; + return { censored: [], added: [], fresh: [], sigop: [], score: 0, similarity: 1 }; } const matches: string[] = []; // present in both mined block and template const added: string[] = []; // present in mined block, not in template const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN + const sigop: string[] = []; // missing, but possibly has an adjusted vsize due to high sigop count const isCensored = {}; // missing, without excuse const isDisplaced = {}; let displacedWeight = 0; @@ -37,6 +38,8 @@ class Audit { // tx is recent, may have reached the miner too late for inclusion if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) { fresh.push(txid); + } else if (this.isPossibleHighSigop(mempool[txid])) { + sigop.push(txid); } else { isCensored[txid] = true; } @@ -137,10 +140,19 @@ class Audit { censored: Object.keys(isCensored), added, fresh, + sigop, score, similarity, }; } + + // Detect transactions with a possibly adjusted vsize due to high sigop count + // very rough heuristic based on number of OP_CHECKMULTISIG outputs + // will miss cases with other sources of sigops + isPossibleHighSigop(tx: TransactionExtended): boolean { + const numBareMultisig = tx.vout.reduce((count, vout) => count + (vout.scriptpubkey_asm.includes('OP_CHECKMULTISIG') ? 1 : 0), 0); + return (numBareMultisig * 400) > tx.vsize; + } } export default new Audit(); \ No newline at end of file diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 11039815f..21c87f9e2 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 59; + private static currentVersion = 60; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -516,6 +516,11 @@ class DatabaseMigration { // https://github.com/mempool/mempool/issues/3360 await this.$executeQuery(`TRUNCATE prices`); } + + if (databaseSchemaVersion < 60 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"'); + await this.updateToSchemaVersion(60); + } } /** diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 3fa7006fb..8b6604522 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -557,7 +557,7 @@ class WebsocketHandler { } if (Common.indexingEnabled() && memPool.isInSync()) { - const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); + const { censored, added, fresh, sigop, 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) => { @@ -584,6 +584,7 @@ class WebsocketHandler { addedTxs: added, missingTxs: censored, freshTxs: fresh, + sigopTxs: sigop, matchRate: matchRate, }); diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 7204c174e..63f33d222 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -32,6 +32,7 @@ export interface BlockAudit { hash: string, missingTxs: string[], freshTxs: string[], + sigopTxs: string[], addedTxs: string[], matchRate: number, } diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts index 70565a1c8..33075f43c 100644 --- a/backend/src/repositories/BlocksAuditsRepository.ts +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces'; class BlocksAuditRepositories { public async $saveAudit(audit: BlockAudit): Promise { try { - await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, match_rate) - VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), - JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), audit.matchRate]); + await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, match_rate) + VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), + JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), audit.matchRate]); } catch (e: any) { if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`); @@ -52,7 +52,7 @@ class BlocksAuditRepositories { const [rows]: any[] = await DB.query( `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size, blocks.weight, blocks.tx_count, - transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, match_rate as matchRate + transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, sigop_txs as sigopTxs, match_rate as matchRate FROM blocks_audits JOIN blocks ON blocks.hash = blocks_audits.hash JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash @@ -63,6 +63,7 @@ class BlocksAuditRepositories { rows[0].missingTxs = JSON.parse(rows[0].missingTxs); rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].freshTxs = JSON.parse(rows[0].freshTxs); + rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs); rows[0].transactions = JSON.parse(rows[0].transactions); rows[0].template = JSON.parse(rows[0].template); diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index f2e67da5b..bb3d9563c 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -37,7 +37,7 @@ export default class TxView implements TransactionStripped { value: number; feerate: number; rate?: number; - status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; context?: 'projected' | 'actual'; scene?: BlockScene; @@ -171,6 +171,7 @@ export default class TxView implements TransactionStripped { case 'censored': return auditColors.censored; case 'missing': + case 'sigop': return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; case 'fresh': return auditColors.missing; diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html index 7e2de8d67..795958fe3 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html @@ -44,6 +44,7 @@ Match Removed Marginal fee rate + High sigop count Recently broadcasted Added Marginal fee rate diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index a11be9ad2..f5fe1a469 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -335,6 +335,7 @@ export class BlockComponent implements OnInit, OnDestroy { const isMissing = {}; const isSelected = {}; const isFresh = {}; + const isSigop = {}; this.numMissing = 0; this.numUnexpected = 0; @@ -354,6 +355,9 @@ export class BlockComponent implements OnInit, OnDestroy { for (const txid of blockAudit.freshTxs || []) { isFresh[txid] = true; } + for (const txid of blockAudit.sigopTxs || []) { + isSigop[txid] = true; + } // set transaction statuses for (const tx of blockAudit.template) { tx.context = 'projected'; @@ -362,7 +366,7 @@ export class BlockComponent implements OnInit, OnDestroy { } else if (inBlock[tx.txid]) { tx.status = 'found'; } else { - tx.status = isFresh[tx.txid] ? 'fresh' : 'missing'; + tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing'); isMissing[tx.txid] = true; this.numMissing++; } diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 8d5d71da7..321ee5841 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -155,7 +155,7 @@ export interface TransactionStripped { fee: number; vsize: number; value: number; - status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; } interface RbfTransaction extends TransactionStripped { diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index bcabf5a29..eaf937a5f 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -76,7 +76,7 @@ export interface TransactionStripped { vsize: number; value: number; rate?: number; // effective fee rate - status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; context?: 'projected' | 'actual'; }