include accelerated tx data in block audits

This commit is contained in:
Mononaut 2023-05-22 18:16:58 -04:00
parent a13c424869
commit e489f713eb
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
11 changed files with 42 additions and 12 deletions

View File

@ -6,16 +6,17 @@ 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: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }) auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, accelerations: { [txid: string]: number })
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } { : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], score: 0, similarity: 1 }; return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], 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 or lastBoosted within PROPAGATION_MARGIN const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse const isCensored = {}; // missing, without excuse
const isDisplaced = {}; const isDisplaced = {};
let displacedWeight = 0; let displacedWeight = 0;
@ -28,6 +29,9 @@ class Audit {
const now = Math.round((Date.now() / 1000)); const now = Math.round((Date.now() / 1000));
for (const tx of transactions) { for (const tx of transactions) {
inBlock[tx.txid] = tx; inBlock[tx.txid] = tx;
if (accelerations[tx.txid]) {
accelerated.push(tx.txid);
}
} }
// coinbase is always expected // coinbase is always expected
if (transactions[0]) { if (transactions[0]) {
@ -149,6 +153,7 @@ class Audit {
fresh, fresh,
sigop: [], sigop: [],
fullrbf: rbf, fullrbf: rbf,
accelerated,
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 = 64; private static currentVersion = 65;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -548,6 +548,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL'); await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
await this.updateToSchemaVersion(64); await this.updateToSchemaVersion(64);
} }
if (databaseSchemaVersion < 65 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(65);
}
} }
/** /**

View File

@ -206,7 +206,7 @@ class MempoolBlocks {
return mempoolBlockDeltas; return mempoolBlockDeltas;
} }
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> { public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, accelerations: { [txid: string]: number } = {}): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now(); const start = Date.now();
// reset mempool short ids // reset mempool short ids

View File

@ -666,7 +666,7 @@ class WebsocketHandler {
} }
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const { censored, added, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool, accelerations);
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 : [];
@ -695,6 +695,7 @@ class WebsocketHandler {
freshTxs: fresh, freshTxs: fresh,
sigopTxs: sigop, sigopTxs: sigop,
fullrbfTxs: fullrbf, fullrbfTxs: fullrbf,
acceleratedTxs: accelerated,
matchRate: matchRate, matchRate: matchRate,
expectedFees: totalFees, expectedFees: totalFees,
expectedWeight: totalWeight, expectedWeight: totalWeight,

View File

@ -36,6 +36,7 @@ export interface BlockAudit {
sigopTxs: string[], sigopTxs: string[],
fullrbfTxs: string[], fullrbfTxs: string[],
addedTxs: string[], addedTxs: string[],
acceleratedTxs: string[],
matchRate: number, matchRate: number,
expectedFees?: number, expectedFees?: number,
expectedWeight?: number, expectedWeight?: 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, fullrbf_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, accelerated_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), JSON.stringify(audit.fullrbfTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]); JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), 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`);
@ -69,6 +69,7 @@ class BlocksAuditRepositories {
fresh_txs as freshTxs, fresh_txs as freshTxs,
sigop_txs as sigopTxs, sigop_txs as sigopTxs,
fullrbf_txs as fullrbfTxs, fullrbf_txs as fullrbfTxs,
accelerated_txs as acceleratedTxs,
match_rate as matchRate, match_rate as matchRate,
expected_fees as expectedFees, expected_fees as expectedFees,
expected_weight as expectedWeight expected_weight as expectedWeight
@ -83,6 +84,8 @@ class BlocksAuditRepositories {
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].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
rows[0].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template); rows[0].template = JSON.parse(rows[0].template);
return rows[0]; return rows[0];

View File

@ -17,6 +17,7 @@ const auditColors = {
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'), added: hexToColor('0099ff'),
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
accelerated: hexToColor('8F5FF6'),
}; };
// convert from this class's update format to TxSprite's update format // convert from this class's update format to TxSprite's update format
@ -38,7 +39,7 @@ export default class TxView implements TransactionStripped {
value: number; value: number;
feerate: number; feerate: number;
rate?: number; rate?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
scene?: BlockScene; scene?: BlockScene;
@ -216,6 +217,8 @@ export default class TxView implements TransactionStripped {
return auditColors.added; return auditColors.added;
case 'selected': case 'selected':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'accelerated':
return auditColors.accelerated;
case 'found': case 'found':
if (this.context === 'projected') { if (this.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];

View File

@ -54,6 +54,7 @@
<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="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td> <td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
</ng-container> </ng-container>
</tr> </tr>
</tbody> </tbody>

View File

@ -340,6 +340,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isFresh = {}; const isFresh = {};
const isSigop = {}; const isSigop = {};
const isRbf = {}; const isRbf = {};
const isAccelerated = {};
this.numMissing = 0; this.numMissing = 0;
this.numUnexpected = 0; this.numUnexpected = 0;
@ -365,6 +366,9 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.fullrbfTxs || []) { for (const txid of blockAudit.fullrbfTxs || []) {
isRbf[txid] = true; isRbf[txid] = true;
} }
for (const txid of blockAudit.acceleratedTxs || []) {
isAccelerated[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';
@ -389,6 +393,9 @@ export class BlockComponent implements OnInit, OnDestroy {
isMissing[tx.txid] = true; isMissing[tx.txid] = true;
this.numMissing++; this.numMissing++;
} }
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
} }
for (const [index, tx] of transactions.entries()) { for (const [index, tx] of transactions.entries()) {
tx.context = 'actual'; tx.context = 'actual';
@ -405,6 +412,9 @@ export class BlockComponent implements OnInit, OnDestroy {
isSelected[tx.txid] = true; isSelected[tx.txid] = true;
this.numUnexpected++; this.numUnexpected++;
} }
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
} }
for (const tx of transactions) { for (const tx of transactions) {
inBlock[tx.txid] = true; inBlock[tx.txid] = true;

View File

@ -158,6 +158,7 @@ export interface BlockAudit extends BlockExtended {
freshTxs: string[], freshTxs: string[],
sigopTxs: string[], sigopTxs: string[],
fullrbfTxs: string[], fullrbfTxs: string[],
acceleratedTxs: string[],
matchRate: number, matchRate: number,
expectedFees: number, expectedFees: number,
expectedWeight: number, expectedWeight: number,
@ -174,7 +175,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' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }

View File

@ -89,7 +89,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' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }