Merge pull request #3881 from mempool/mononaut/separate-audit-api

Separate summary and audit-summary API endpoints
This commit is contained in:
softsimon 2023-07-01 19:43:20 +02:00 committed by GitHub
commit 2bda12e5f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 129 additions and 161 deletions

View File

@ -399,9 +399,13 @@ class BitcoinRoutes {
private async getBlockAuditSummary(req: Request, res: Response) { private async getBlockAuditSummary(req: Request, res: Response) {
try { try {
const transactions = await blocks.$getBlockAuditSummary(req.params.hash); const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); if (auditSummary) {
res.json(transactions); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
return res.status(404).send(`audit not available`);
}
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }

View File

@ -1007,19 +1007,11 @@ class Blocks {
} }
public async $getBlockAuditSummary(hash: string): Promise<any> { public async $getBlockAuditSummary(hash: string): Promise<any> {
let summary;
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
summary = await BlocksAuditsRepository.$getBlockAudit(hash); return BlocksAuditsRepository.$getBlockAudit(hash);
} else {
return null;
} }
// fallback to non-audited transaction summary
if (!summary?.transactions?.length) {
const strippedTransactions = await this.$getStrippedBlockTransactions(hash);
summary = {
transactions: strippedTransactions
};
}
return summary;
} }
public getLastDifficultyAdjustmentTime(): number { public getLastDifficultyAdjustmentTime(): number {

View File

@ -64,7 +64,6 @@ class BlocksAuditRepositories {
const [rows]: any[] = await DB.query( const [rows]: any[] = await DB.query(
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size, `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
blocks.weight, blocks.tx_count, blocks.weight, blocks.tx_count,
transactions,
template, template,
missing_txs as missingTxs, missing_txs as missingTxs,
added_txs as addedTxs, added_txs as addedTxs,
@ -76,7 +75,6 @@ class BlocksAuditRepositories {
FROM blocks_audits FROM blocks_audits
JOIN blocks ON blocks.hash = blocks_audits.hash JOIN blocks ON blocks.hash = blocks_audits.hash
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
WHERE blocks_audits.hash = "${hash}" WHERE blocks_audits.hash = "${hash}"
`); `);
@ -85,12 +83,9 @@ 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].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template); rows[0].template = JSON.parse(rows[0].template);
if (rows[0].transactions.length) { return rows[0];
return rows[0];
}
} }
return null; return null;
} catch (e: any) { } catch (e: any) {

View File

@ -63,7 +63,7 @@
*ngIf="blockAudit?.matchRate != null; else nullHealth" *ngIf="blockAudit?.matchRate != null; else nullHealth"
>{{ blockAudit?.matchRate }}%</span> >{{ blockAudit?.matchRate }}%</span>
<ng-template #nullHealth> <ng-template #nullHealth>
<ng-container *ngIf="!isLoadingAudit; else loadingHealth"> <ng-container *ngIf="!isLoadingOverview; else loadingHealth">
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span> <span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-container> </ng-container>
</ng-template> </ng-template>

View File

@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators'; import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith } from 'rxjs/operators';
import { Transaction, Vout } from '../../interfaces/electrs.interface'; import { Transaction, Vout } from '../../interfaces/electrs.interface';
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs'; import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
@ -44,7 +44,6 @@ export class BlockComponent implements OnInit, OnDestroy {
strippedTransactions: TransactionStripped[]; strippedTransactions: TransactionStripped[];
overviewTransitionDirection: string; overviewTransitionDirection: string;
isLoadingOverview = true; isLoadingOverview = true;
isLoadingAudit = true;
error: any; error: any;
blockSubsidy: number; blockSubsidy: number;
fees: number; fees: number;
@ -281,143 +280,111 @@ export class BlockComponent implements OnInit, OnDestroy {
this.isLoadingOverview = false; this.isLoadingOverview = false;
}); });
if (!this.auditSupported) { this.overviewSubscription = block$.pipe(
this.overviewSubscription = block$.pipe( switchMap((block) => {
startWith(null), return forkJoin([
pairwise(), this.apiService.getStrippedBlockTransactions$(block.id)
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
return of([]);
}),
switchMap((transactions) => {
if (prevBlock) {
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
} else {
return of({ transactions, direction: 'down' });
}
})
)
),
)
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
this.strippedTransactions = transactions;
this.isLoadingOverview = false;
this.setupBlockGraphs();
},
(error) => {
this.error = error;
this.isLoadingOverview = false;
});
}
if (this.auditSupported) {
this.auditSubscription = block$.pipe(
startWith(null),
pairwise(),
switchMap(([prevBlock, block]) => {
this.isLoadingAudit = true;
this.blockAudit = null;
return this.apiService.getBlockAudit$(block.id)
.pipe( .pipe(
catchError((err) => { catchError((err) => {
this.overviewError = err; this.overviewError = err;
this.isLoadingAudit = false; return of(null);
return of([]);
}) })
); ),
} !this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id)
), .pipe(
filter((response) => response != null), catchError((err) => {
map((response) => { this.overviewError = err;
const blockAudit = response.body; return of(null);
const inTemplate = {}; })
const inBlock = {}; )
const isAdded = {}; ]);
const isCensored = {}; })
const isMissing = {}; )
const isSelected = {}; .subscribe(([transactions, blockAudit]) => {
const isFresh = {}; if (transactions) {
const isSigop = {}; this.strippedTransactions = transactions;
this.numMissing = 0; } else {
this.numUnexpected = 0; this.strippedTransactions = [];
}
if (blockAudit?.template) { this.blockAudit = null;
for (const tx of blockAudit.template) { if (transactions && blockAudit) {
inTemplate[tx.txid] = true; const inTemplate = {};
} const inBlock = {};
for (const tx of blockAudit.transactions) { const isAdded = {};
inBlock[tx.txid] = true; const isCensored = {};
} const isMissing = {};
for (const txid of blockAudit.addedTxs) { const isSelected = {};
isAdded[txid] = true; const isFresh = {};
} const isSigop = {};
for (const txid of blockAudit.missingTxs) { this.numMissing = 0;
isCensored[txid] = true; this.numUnexpected = 0;
}
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';
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing');
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of blockAudit.transactions.entries()) {
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0; if (blockAudit?.template) {
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0; for (const tx of blockAudit.template) {
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0; inTemplate[tx.txid] = true;
this.setAuditAvailable(true);
} else {
this.setAuditAvailable(false);
} }
return blockAudit; for (const tx of transactions) {
}), inBlock[tx.txid] = true;
catchError((err) => { }
console.log(err); for (const txid of blockAudit.addedTxs) {
this.error = err; isAdded[txid] = true;
this.isLoadingOverview = false; }
this.isLoadingAudit = false; for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
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';
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing');
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of transactions.entries()) {
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0;
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0;
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0;
this.blockAudit = blockAudit;
this.setAuditAvailable(true);
} else {
this.setAuditAvailable(false); this.setAuditAvailable(false);
return of(null); }
}), } else {
).subscribe((blockAudit) => { this.setAuditAvailable(false);
this.blockAudit = blockAudit; }
this.setupBlockGraphs();
this.isLoadingOverview = false; this.isLoadingOverview = false;
this.isLoadingAudit = false; this.setupBlockGraphs();
}); });
}
this.networkChangedSubscription = this.stateService.networkChanged$ this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network); .subscribe((network) => this.network = network);
@ -652,25 +619,32 @@ export class BlockComponent implements OnInit, OnDestroy {
} }
updateAuditAvailableFromBlockHeight(blockHeight: number): void { updateAuditAvailableFromBlockHeight(blockHeight: number): void {
if (!this.auditSupported) { if (!this.isAuditAvailableFromBlockHeight(blockHeight)) {
this.setAuditAvailable(false); this.setAuditAvailable(false);
} }
}
isAuditAvailableFromBlockHeight(blockHeight: number): boolean {
if (!this.auditSupported) {
return false;
}
switch (this.stateService.network) { switch (this.stateService.network) {
case 'testnet': case 'testnet':
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) { if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
this.setAuditAvailable(false); return false;
} }
break; break;
case 'signet': case 'signet':
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) { if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
this.setAuditAvailable(false); return false;
} }
break; break;
default: default:
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) { if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
this.setAuditAvailable(false); return false;
} }
} }
return true;
} }
getMinBlockFee(block: BlockExtended): number { getMinBlockFee(block: BlockExtended): number {

View File

@ -153,6 +153,8 @@ export interface BlockExtended extends Block {
export interface BlockAudit extends BlockExtended { export interface BlockAudit extends BlockExtended {
missingTxs: string[], missingTxs: string[],
addedTxs: string[], addedTxs: string[],
freshTxs: string[],
sigopTxs: string[],
matchRate: number, matchRate: number,
expectedFees: number, expectedFees: number,
expectedWeight: number, expectedWeight: number,
@ -169,6 +171,7 @@ export interface TransactionStripped {
vsize: number; vsize: number;
value: number; value: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual';
} }
interface RbfTransaction extends TransactionStripped { interface RbfTransaction extends TransactionStripped {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree } from '../interfaces/node-api.interface'; PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { WebsocketResponse } from '../interfaces/websocket.interface'; import { WebsocketResponse } from '../interfaces/websocket.interface';
@ -249,9 +249,9 @@ export class ApiService {
); );
} }
getBlockAudit$(hash: string) : Observable<any> { getBlockAudit$(hash: string) : Observable<BlockAudit> {
return this.httpClient.get<any>( return this.httpClient.get<BlockAudit>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`, { observe: 'response' } this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`
); );
} }