Merge pull request #3846 from mempool/mononaut/audit-details
Add expected vs actual audit details comparison table
This commit is contained in:
@@ -282,10 +282,14 @@ class Blocks {
|
||||
}
|
||||
|
||||
extras.matchRate = null;
|
||||
extras.expectedFees = null;
|
||||
extras.expectedWeight = null;
|
||||
if (config.MEMPOOL.AUDIT) {
|
||||
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
|
||||
if (auditScore != null) {
|
||||
extras.matchRate = auditScore.matchRate;
|
||||
extras.expectedFees = auditScore.expectedFees;
|
||||
extras.expectedWeight = auditScore.expectedWeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,6 +459,46 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [INDEXING] Index expected fees & weight for all audited blocks
|
||||
*/
|
||||
public async $generateAuditStats(): Promise<void> {
|
||||
const blockIds = await BlocksAuditsRepository.$getBlocksWithoutSummaries();
|
||||
if (!blockIds?.length) {
|
||||
return;
|
||||
}
|
||||
let timer = Date.now();
|
||||
let indexedThisRun = 0;
|
||||
let indexedTotal = 0;
|
||||
logger.debug(`Indexing ${blockIds.length} block audit details`);
|
||||
for (const hash of blockIds) {
|
||||
const summary = await BlocksSummariesRepository.$getTemplate(hash);
|
||||
let totalFees = 0;
|
||||
let totalWeight = 0;
|
||||
for (const tx of summary?.transactions || []) {
|
||||
totalFees += tx.fee;
|
||||
totalWeight += (tx.vsize * 4);
|
||||
}
|
||||
await BlocksAuditsRepository.$setSummary(hash, totalFees, totalWeight);
|
||||
const cachedBlock = this.blocks.find(block => block.id === hash);
|
||||
if (cachedBlock) {
|
||||
cachedBlock.extras.expectedFees = totalFees;
|
||||
cachedBlock.extras.expectedWeight = totalWeight;
|
||||
}
|
||||
|
||||
indexedThisRun++;
|
||||
indexedTotal++;
|
||||
const elapsedSeconds = (Date.now() - timer) / 1000;
|
||||
if (elapsedSeconds > 5) {
|
||||
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||
logger.debug(`Indexed ${indexedTotal} / ${blockIds.length} block audit details (${blockPerSeconds.toFixed(1)}/s)`);
|
||||
timer = Date.now();
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
}
|
||||
logger.debug(`Indexing block audit details completed`);
|
||||
}
|
||||
|
||||
/**
|
||||
* [INDEXING] Index all blocks metadata for the mining dashboard
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 61;
|
||||
private static currentVersion = 62;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -533,6 +533,12 @@ class DatabaseMigration {
|
||||
await this.updateToSchemaVersion(61);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 62 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
|
||||
await this.updateToSchemaVersion(62);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -571,11 +571,18 @@ class WebsocketHandler {
|
||||
};
|
||||
}) : [];
|
||||
|
||||
let totalFees = 0;
|
||||
let totalWeight = 0;
|
||||
for (const tx of stripped) {
|
||||
totalFees += tx.fee;
|
||||
totalWeight += (tx.vsize * 4);
|
||||
}
|
||||
|
||||
BlocksSummariesRepository.$saveTemplate({
|
||||
height: block.height,
|
||||
template: {
|
||||
id: block.id,
|
||||
transactions: stripped
|
||||
transactions: stripped,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -588,10 +595,14 @@ class WebsocketHandler {
|
||||
freshTxs: fresh,
|
||||
sigopTxs: sigop,
|
||||
matchRate: matchRate,
|
||||
expectedFees: totalFees,
|
||||
expectedWeight: totalWeight,
|
||||
});
|
||||
|
||||
if (block.extras) {
|
||||
block.extras.matchRate = matchRate;
|
||||
block.extras.expectedFees = totalFees;
|
||||
block.extras.expectedWeight = totalWeight;
|
||||
block.extras.similarity = similarity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ class Indexer {
|
||||
await mining.$generatePoolHashrateHistory();
|
||||
await blocks.$generateBlocksSummariesDatabase();
|
||||
await blocks.$generateCPFPDatabase();
|
||||
await blocks.$generateAuditStats();
|
||||
} catch (e) {
|
||||
this.indexerRunning = false;
|
||||
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
|
||||
@@ -35,11 +35,15 @@ export interface BlockAudit {
|
||||
sigopTxs: string[],
|
||||
addedTxs: string[],
|
||||
matchRate: number,
|
||||
expectedFees?: number,
|
||||
expectedWeight?: number,
|
||||
}
|
||||
|
||||
export interface AuditScore {
|
||||
hash: string,
|
||||
matchRate?: number,
|
||||
expectedFees?: number
|
||||
expectedWeight?: number
|
||||
}
|
||||
|
||||
export interface MempoolBlock {
|
||||
@@ -182,6 +186,8 @@ export interface BlockExtension {
|
||||
feeRange: number[]; // fee rate percentiles
|
||||
reward: number;
|
||||
matchRate: number | null;
|
||||
expectedFees: number | null;
|
||||
expectedWeight: number | null;
|
||||
similarity?: number;
|
||||
pool: {
|
||||
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
||||
|
||||
@@ -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, 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]);
|
||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, match_rate, expected_fees, expected_weight)
|
||||
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]);
|
||||
} 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`);
|
||||
@@ -18,6 +18,19 @@ class BlocksAuditRepositories {
|
||||
}
|
||||
}
|
||||
|
||||
public async $setSummary(hash: string, expectedFees: number, expectedWeight: number) {
|
||||
try {
|
||||
await DB.query(`
|
||||
UPDATE blocks_audits SET
|
||||
expected_fees = ?,
|
||||
expected_weight = ?
|
||||
WHERE hash = ?
|
||||
`, [expectedFees, expectedWeight, hash]);
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot update block audit in db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlocksHealthHistory(div: number, interval: string | null): Promise<any> {
|
||||
try {
|
||||
let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`;
|
||||
@@ -51,7 +64,15 @@ 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, sigop_txs as sigopTxs, 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,
|
||||
expected_fees as expectedFees,
|
||||
expected_weight as expectedWeight
|
||||
FROM blocks_audits
|
||||
JOIN blocks ON blocks.hash = blocks_audits.hash
|
||||
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||
@@ -81,7 +102,7 @@ class BlocksAuditRepositories {
|
||||
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`SELECT hash, match_rate as matchRate
|
||||
`SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
|
||||
FROM blocks_audits
|
||||
WHERE blocks_audits.hash = "${hash}"
|
||||
`);
|
||||
@@ -95,7 +116,7 @@ class BlocksAuditRepositories {
|
||||
public async $getBlockAuditScores(maxHeight: number, minHeight: number): Promise<AuditScore[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`SELECT hash, match_rate as matchRate
|
||||
`SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
|
||||
FROM blocks_audits
|
||||
WHERE blocks_audits.height BETWEEN ? AND ?
|
||||
`, [minHeight, maxHeight]);
|
||||
@@ -105,6 +126,32 @@ class BlocksAuditRepositories {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlocksWithoutSummaries(): Promise<string[]> {
|
||||
try {
|
||||
const [fromRows]: any[] = await DB.query(`
|
||||
SELECT height
|
||||
FROM blocks_audits
|
||||
WHERE expected_fees IS NULL
|
||||
ORDER BY height DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
if (!fromRows?.length) {
|
||||
return [];
|
||||
}
|
||||
const fromHeight = fromRows[0].height;
|
||||
const [idRows]: any[] = await DB.query(`
|
||||
SELECT hash
|
||||
FROM blocks_audits
|
||||
WHERE height <= ?
|
||||
ORDER BY height DESC
|
||||
`, [fromHeight]);
|
||||
return idRows.map(row => row.hash);
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new BlocksAuditRepositories();
|
||||
|
||||
@@ -1018,10 +1018,14 @@ class BlocksRepository {
|
||||
|
||||
// Match rate is not part of the blocks table, but it is part of APIs so we must include it
|
||||
extras.matchRate = null;
|
||||
extras.expectedFees = null;
|
||||
extras.expectedWeight = null;
|
||||
if (config.MEMPOOL.AUDIT) {
|
||||
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(dbBlk.id);
|
||||
if (auditScore != null) {
|
||||
extras.matchRate = auditScore.matchRate;
|
||||
extras.expectedFees = auditScore.expectedFees;
|
||||
extras.expectedWeight = auditScore.expectedWeight;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,21 @@ class BlocksSummariesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getTemplate(id: string): Promise<BlockSummary | undefined> {
|
||||
try {
|
||||
const [templates]: any[] = await DB.query(`SELECT * from blocks_templates WHERE id = ?`, [id]);
|
||||
if (templates.length > 0) {
|
||||
return {
|
||||
id: templates[0].id,
|
||||
transactions: JSON.parse(templates[0].template),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get block template for block id ${id}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async $getIndexedSummariesId(): Promise<string[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
|
||||
|
||||
Reference in New Issue
Block a user