Omit possible high-sigop txs from block health score
This commit is contained in:
		
							parent
							
								
									12639ade6c
								
							
						
					
					
						commit
						bdb44c4609
					
				@ -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();
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ export interface BlockAudit {
 | 
			
		||||
  hash: string,
 | 
			
		||||
  missingTxs: string[],
 | 
			
		||||
  freshTxs: string[],
 | 
			
		||||
  sigopTxs: string[],
 | 
			
		||||
  addedTxs: string[],
 | 
			
		||||
  matchRate: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
 | 
			
		||||
class BlocksAuditRepositories {
 | 
			
		||||
  public async $saveAudit(audit: BlockAudit): Promise<void> {
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,7 @@
 | 
			
		||||
          <td *ngSwitchCase="'found'"><span class="badge badge-success" i18n="transaction.audit.match">Match</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'censored'"><span class="badge badge-danger" i18n="transaction.audit.removed">Removed</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'missing'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'sigop'"><span class="badge badge-warning" i18n="transaction.audit.sigop">High sigop count</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
 | 
			
		||||
 | 
			
		||||
@ -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++;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user