Omit possible high-sigop txs from block health score

This commit is contained in:
Mononaut 2023-05-17 11:46:50 -04:00
parent 64d6bda728
commit 81ec54fcb3
No known key found for this signature in database
GPG key ID: A3F058E41374C04E
10 changed files with 38 additions and 12 deletions

View file

@ -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();

View file

@ -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);
}
}
/**

View file

@ -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,
});

View file

@ -32,6 +32,7 @@ export interface BlockAudit {
hash: string,
missingTxs: string[],
freshTxs: string[],
sigopTxs: string[],
addedTxs: string[],
matchRate: number,
}

View file

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

View file

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

View file

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

View file

@ -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++;
}

View file

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

View file

@ -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';
}