Store block first seen time in block audits
This commit is contained in:
		
							parent
							
								
									67295c1b9b
								
							
						
					
					
						commit
						1a75e3e317
					
				@ -46,7 +46,8 @@
 | 
			
		||||
    "PASSWORD": "__CORE_RPC_PASSWORD__",
 | 
			
		||||
    "TIMEOUT": 1000,
 | 
			
		||||
    "COOKIE": false,
 | 
			
		||||
    "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
 | 
			
		||||
    "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
 | 
			
		||||
    "DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
 | 
			
		||||
  },
 | 
			
		||||
  "ELECTRUM": {
 | 
			
		||||
    "HOST": "__ELECTRUM_HOST__",
 | 
			
		||||
 | 
			
		||||
@ -73,7 +73,8 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
        PASSWORD: 'mempool',
 | 
			
		||||
        TIMEOUT: 60000,
 | 
			
		||||
        COOKIE: false,
 | 
			
		||||
        COOKIE_PATH: '/bitcoin/.cookie'
 | 
			
		||||
        COOKIE_PATH: '/bitcoin/.cookie',
 | 
			
		||||
        DEBUG_LOG_PATH: '',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(config.SECOND_CORE_RPC).toStrictEqual({
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
 | 
			
		||||
@ -7,10 +8,10 @@ import transactionUtils from './transaction-utils';
 | 
			
		||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
 | 
			
		||||
 | 
			
		||||
class Audit {
 | 
			
		||||
  auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
 | 
			
		||||
   : { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
 | 
			
		||||
  auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, hash: string)
 | 
			
		||||
   : { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number, firstSeen: string | undefined } {
 | 
			
		||||
    if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
 | 
			
		||||
      return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
 | 
			
		||||
      return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1, firstSeen: undefined };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const matches: string[] = []; // present in both mined block and template
 | 
			
		||||
@ -176,6 +177,8 @@ class Audit {
 | 
			
		||||
    }
 | 
			
		||||
    const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
 | 
			
		||||
 | 
			
		||||
    const firstSeen = this.getFirstSeenFromLogs(hash);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      unseen,
 | 
			
		||||
      censored: Object.keys(isCensored),
 | 
			
		||||
@ -187,8 +190,39 @@ class Audit {
 | 
			
		||||
      accelerated,
 | 
			
		||||
      score,
 | 
			
		||||
      similarity,
 | 
			
		||||
      firstSeen
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getFirstSeenFromLogs(hash: string): string | undefined {
 | 
			
		||||
    const debugLogPath = config.CORE_RPC.DEBUG_LOG_PATH;
 | 
			
		||||
    if (debugLogPath) {
 | 
			
		||||
      try {
 | 
			
		||||
        const fileDescriptor = fs.openSync(debugLogPath, 'r');
 | 
			
		||||
        const bufferSize = 2048; // Read the last few lines of the file
 | 
			
		||||
        const buffer = Buffer.alloc(bufferSize);
 | 
			
		||||
        const fileSize = fs.statSync(debugLogPath).size;
 | 
			
		||||
        const chunkSize = Math.min(bufferSize, fileSize);
 | 
			
		||||
        fs.readSync(fileDescriptor, buffer, 0, chunkSize, fileSize - chunkSize);
 | 
			
		||||
        const lines = buffer.toString('utf8', 0, chunkSize).split('\n');
 | 
			
		||||
        fs.closeSync(fileDescriptor);
 | 
			
		||||
  
 | 
			
		||||
        for (let i = lines.length - 1; i >= 0; i--) {
 | 
			
		||||
          const line = lines[i];
 | 
			
		||||
          if (line && line.includes(`Saw new header hash=${hash}`)) {
 | 
			
		||||
            // Extract time from log: "2021-08-31T12:34:56Z" or "2021-08-31T12:34:56.123456Z" if logtimemicros=1
 | 
			
		||||
            const dateMatch = line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.?\d{6})?Z/);
 | 
			
		||||
            if (dateMatch) {
 | 
			
		||||
              return dateMatch[0].replace("T", " ").replace("Z", "");
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        logger.err(`Cannot parse block first seen time from Core logs. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return undefined;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Audit();
 | 
			
		||||
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 82;
 | 
			
		||||
  private static currentVersion = 83;
 | 
			
		||||
  private queryTimeout = 3600_000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
@ -705,6 +705,11 @@ class DatabaseMigration {
 | 
			
		||||
      await this.$fixBadV1AuditBlocks();
 | 
			
		||||
      await this.updateToSchemaVersion(82);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 83 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `blocks_audits` ADD first_seen timestamp(6) DEFAULT NULL');
 | 
			
		||||
      await this.updateToSchemaVersion(83);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -975,7 +975,7 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (Common.indexingEnabled()) {
 | 
			
		||||
        const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool);
 | 
			
		||||
        const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity, firstSeen } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool, block.id);
 | 
			
		||||
        const matchRate = Math.round(score * 100 * 100) / 100;
 | 
			
		||||
 | 
			
		||||
        const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
 | 
			
		||||
@ -1012,6 +1012,7 @@ class WebsocketHandler {
 | 
			
		||||
          matchRate: matchRate,
 | 
			
		||||
          expectedFees: totalFees,
 | 
			
		||||
          expectedWeight: totalWeight,
 | 
			
		||||
          firstSeen: firstSeen,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (block.extras) {
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,7 @@ export interface BlockAudit {
 | 
			
		||||
  expectedFees?: number,
 | 
			
		||||
  expectedWeight?: number,
 | 
			
		||||
  template?: any[];
 | 
			
		||||
  firstSeen?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionAudit {
 | 
			
		||||
@ -57,6 +58,7 @@ export interface TransactionAudit {
 | 
			
		||||
  conflict?: boolean;
 | 
			
		||||
  coinbase?: boolean;
 | 
			
		||||
  firstSeen?: number;
 | 
			
		||||
  blockFirstSeen?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuditScore {
 | 
			
		||||
 | 
			
		||||
@ -124,7 +124,8 @@ class AuditReplication {
 | 
			
		||||
      matchRate: auditSummary.matchRate,
 | 
			
		||||
      expectedFees: auditSummary.expectedFees,
 | 
			
		||||
      expectedWeight: auditSummary.expectedWeight,
 | 
			
		||||
    });
 | 
			
		||||
      firstSeen: auditSummary.firstSeen,
 | 
			
		||||
    }, true);
 | 
			
		||||
    // add missing data to cached blocks
 | 
			
		||||
    const cachedBlock = blocks.getBlocks().find(block => block.id === blockHash);
 | 
			
		||||
    if (cachedBlock) {
 | 
			
		||||
 | 
			
		||||
@ -15,11 +15,11 @@ interface MigrationAudit {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class BlocksAuditRepositories {
 | 
			
		||||
  public async $saveAudit(audit: BlockAudit): Promise<void> {
 | 
			
		||||
  public async $saveAudit(audit: BlockAudit, replication = false): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
 | 
			
		||||
        VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs),
 | 
			
		||||
          JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
 | 
			
		||||
      await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight, first_seen)
 | 
			
		||||
        VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${replication ? 'FROM_UNIXTIME(?)' : '?'})`, [audit.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs),
 | 
			
		||||
          JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight, audit.firstSeen]);
 | 
			
		||||
    } 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`);
 | 
			
		||||
@ -78,6 +78,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
          blocks_audits.height,
 | 
			
		||||
          blocks_audits.hash as id,
 | 
			
		||||
          UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
 | 
			
		||||
          UNIX_TIMESTAMP(blocks_audits.first_seen) as firstSeen,
 | 
			
		||||
          template,
 | 
			
		||||
          unseen_txs as unseenTxs,
 | 
			
		||||
          missing_txs as missingTxs,
 | 
			
		||||
@ -96,6 +97,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
      `, [hash]);
 | 
			
		||||
      
 | 
			
		||||
      if (rows.length) {
 | 
			
		||||
        console.log(rows[0].firstSeen);
 | 
			
		||||
        rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs);
 | 
			
		||||
        rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
 | 
			
		||||
        rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
 | 
			
		||||
@ -106,6 +108,10 @@ class BlocksAuditRepositories {
 | 
			
		||||
        rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
 | 
			
		||||
        rows[0].template = JSON.parse(rows[0].template);
 | 
			
		||||
 | 
			
		||||
        if (!rows[0].firstSeen) {
 | 
			
		||||
          delete rows[0].firstSeen;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return rows[0];
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
@ -124,6 +130,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
        const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
 | 
			
		||||
        const isAccelerated = blockAudit.acceleratedTxs.includes(txid);
 | 
			
		||||
        const isConflict = blockAudit.fullrbfTxs.includes(txid);
 | 
			
		||||
        const blockFirstSeen = blockAudit.firstSeen;
 | 
			
		||||
        let isExpected = false;
 | 
			
		||||
        let firstSeen = undefined;
 | 
			
		||||
        blockAudit.template?.forEach(tx => {
 | 
			
		||||
@ -142,6 +149,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
          conflict: isConflict,
 | 
			
		||||
          accelerated: isAccelerated,
 | 
			
		||||
          firstSeen,
 | 
			
		||||
          ...(blockFirstSeen) && { blockFirstSeen },
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user