Merge pull request #3879 from mempool/mononaut/audit-exclude-fullrbf

exclude fullrbf txs from audit and label in visualization
This commit is contained in:
wiz 2023-07-11 15:29:32 +09:00 committed by GitHub
commit 795e6753eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 62 additions and 17 deletions

View File

@ -1,19 +1,21 @@
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import rbfCache from './rbf-cache';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners 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 { class Audit {
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }) auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
: { censored: string[], added: string[], fresh: string[], sigop: string[], score: number, similarity: number } { : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], score: 0, similarity: 1 }; return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], score: 0, similarity: 1 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template const added: string[] = []; // present in mined block, not in template
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement
const isCensored = {}; // missing, without excuse const isCensored = {}; // missing, without excuse
const isDisplaced = {}; const isDisplaced = {};
let displacedWeight = 0; let displacedWeight = 0;
@ -35,7 +37,9 @@ class Audit {
for (const txid of projectedBlocks[0].transactionIds) { for (const txid of projectedBlocks[0].transactionIds) {
if (!inBlock[txid]) { if (!inBlock[txid]) {
// tx is recent, may have reached the miner too late for inclusion // tx is recent, may have reached the miner too late for inclusion
if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) { if (rbfCache.isFullRbf(txid)) {
fullrbf.push(txid);
} else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
fresh.push(txid); fresh.push(txid);
} else { } else {
isCensored[txid] = true; isCensored[txid] = true;
@ -91,7 +95,9 @@ class Audit {
if (inTemplate[tx.txid]) { if (inTemplate[tx.txid]) {
matches.push(tx.txid); matches.push(tx.txid);
} else { } else {
if (!isDisplaced[tx.txid]) { if (rbfCache.isFullRbf(tx.txid)) {
fullrbf.push(tx.txid);
} else if (!isDisplaced[tx.txid]) {
added.push(tx.txid); added.push(tx.txid);
} }
overflowWeight += tx.weight; overflowWeight += tx.weight;
@ -138,6 +144,7 @@ class Audit {
added, added,
fresh, fresh,
sigop: [], sigop: [],
fullrbf,
score, score,
similarity, similarity,
}; };

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 62; private static currentVersion = 63;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -539,6 +539,10 @@ class DatabaseMigration {
await this.updateToSchemaVersion(62); await this.updateToSchemaVersion(62);
} }
if (databaseSchemaVersion < 63 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(63);
}
} }
/** /**

View File

@ -169,6 +169,19 @@ class RbfCache {
} }
} }
// is the transaction involved in a full rbf replacement?
public isFullRbf(txid: string): boolean {
const treeId = this.treeMap.get(txid);
if (!treeId) {
return false;
}
const tree = this.rbfTrees.get(treeId);
if (!tree) {
return false;
}
return tree?.fullRbf;
}
private cleanup(): void { private cleanup(): void {
const now = Date.now(); const now = Date.now();
for (const txid of this.expiring.keys()) { for (const txid of this.expiring.keys()) {

View File

@ -583,6 +583,10 @@ class WebsocketHandler {
const _memPool = memPool.getMempool(); const _memPool = memPool.getMempool();
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT) { if (config.MEMPOOL.AUDIT) {
let projectedBlocks; let projectedBlocks;
let auditMempool = _memPool; let auditMempool = _memPool;
@ -605,7 +609,7 @@ class WebsocketHandler {
} }
if (Common.indexingEnabled() && memPool.isInSync()) { if (Common.indexingEnabled() && memPool.isInSync()) {
const { censored, added, fresh, sigop, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100; const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : []; const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@ -633,6 +637,7 @@ class WebsocketHandler {
missingTxs: censored, missingTxs: censored,
freshTxs: fresh, freshTxs: fresh,
sigopTxs: sigop, sigopTxs: sigop,
fullrbfTxs: fullrbf,
matchRate: matchRate, matchRate: matchRate,
expectedFees: totalFees, expectedFees: totalFees,
expectedWeight: totalWeight, expectedWeight: totalWeight,
@ -652,10 +657,6 @@ class WebsocketHandler {
} }
} }
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
// Update mempool to remove transactions included in the new block // Update mempool to remove transactions included in the new block
for (const txId of txIds) { for (const txId of txIds) {
delete _memPool[txId]; delete _memPool[txId];

View File

@ -34,6 +34,7 @@ export interface BlockAudit {
missingTxs: string[], missingTxs: string[],
freshTxs: string[], freshTxs: string[],
sigopTxs: string[], sigopTxs: string[],
fullrbfTxs: string[],
addedTxs: string[], addedTxs: string[],
matchRate: number, matchRate: number,
expectedFees?: number, expectedFees?: number,

View File

@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
class BlocksAuditRepositories { class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> { public async $saveAudit(audit: BlockAudit): Promise<void> {
try { try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, match_rate, expected_fees, expected_weight) await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, match_rate, expected_fees, expected_weight)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), 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, audit.expectedFees, audit.expectedWeight]); JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart 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`); logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
@ -68,6 +68,7 @@ class BlocksAuditRepositories {
added_txs as addedTxs, added_txs as addedTxs,
fresh_txs as freshTxs, fresh_txs as freshTxs,
sigop_txs as sigopTxs, sigop_txs as sigopTxs,
fullrbf_txs as fullrbfTxs,
match_rate as matchRate, match_rate as matchRate,
expected_fees as expectedFees, expected_fees as expectedFees,
expected_weight as expectedWeight expected_weight as expectedWeight
@ -81,6 +82,7 @@ class BlocksAuditRepositories {
rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].freshTxs = JSON.parse(rows[0].freshTxs); rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs); rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
rows[0].template = JSON.parse(rows[0].template); rows[0].template = JSON.parse(rows[0].template);
return rows[0]; return rows[0];

View File

@ -37,7 +37,7 @@ export default class TxView implements TransactionStripped {
value: number; value: number;
feerate: number; feerate: number;
rate?: number; rate?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
scene?: BlockScene; scene?: BlockScene;
@ -172,6 +172,7 @@ export default class TxView implements TransactionStripped {
return auditColors.censored; return auditColors.censored;
case 'missing': case 'missing':
case 'sigop': case 'sigop':
case 'fullrbf':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'fresh': case 'fresh':
return auditColors.missing; return auditColors.missing;

View File

@ -52,6 +52,7 @@
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</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="'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> <td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
</ng-container> </ng-container>
</tr> </tr>
</tbody> </tbody>

View File

@ -317,6 +317,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isSelected = {}; const isSelected = {};
const isFresh = {}; const isFresh = {};
const isSigop = {}; const isSigop = {};
const isFullRbf = {};
this.numMissing = 0; this.numMissing = 0;
this.numUnexpected = 0; this.numUnexpected = 0;
@ -339,6 +340,9 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.sigopTxs || []) { for (const txid of blockAudit.sigopTxs || []) {
isSigop[txid] = true; isSigop[txid] = true;
} }
for (const txid of blockAudit.fullrbfTxs || []) {
isFullRbf[txid] = true;
}
// set transaction statuses // set transaction statuses
for (const tx of blockAudit.template) { for (const tx of blockAudit.template) {
tx.context = 'projected'; tx.context = 'projected';
@ -347,7 +351,15 @@ export class BlockComponent implements OnInit, OnDestroy {
} else if (inBlock[tx.txid]) { } else if (inBlock[tx.txid]) {
tx.status = 'found'; tx.status = 'found';
} else { } else {
tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing'); if (isFresh[tx.txid]) {
tx.status = 'fresh';
} else if (isSigop[tx.txid]) {
tx.status = 'sigop';
} else if (isFullRbf[tx.txid]) {
tx.status = 'fullrbf';
} else {
tx.status = 'missing';
}
isMissing[tx.txid] = true; isMissing[tx.txid] = true;
this.numMissing++; this.numMissing++;
} }
@ -360,6 +372,8 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = 'added'; tx.status = 'added';
} else if (inTemplate[tx.txid]) { } else if (inTemplate[tx.txid]) {
tx.status = 'found'; tx.status = 'found';
} else if (isFullRbf[tx.txid]) {
tx.status = 'fullrbf';
} else { } else {
tx.status = 'selected'; tx.status = 'selected';
isSelected[tx.txid] = true; isSelected[tx.txid] = true;

View File

@ -156,6 +156,7 @@ export interface BlockAudit extends BlockExtended {
addedTxs: string[], addedTxs: string[],
freshTxs: string[], freshTxs: string[],
sigopTxs: string[], sigopTxs: string[],
fullrbfTxs: string[],
matchRate: number, matchRate: number,
expectedFees: number, expectedFees: number,
expectedWeight: number, expectedWeight: number,
@ -171,7 +172,7 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }

View File

@ -77,7 +77,7 @@ export interface TransactionStripped {
vsize: number; vsize: number;
value: number; value: number;
rate?: number; // effective fee rate rate?: number; // effective fee rate
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }