Fetch missing block audit scores
This commit is contained in:
parent
1b3bc0ef4e
commit
5b6f713ef3
@ -1,5 +1,10 @@
|
||||
import config from '../config';
|
||||
import { BlockExtended, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import { Common } from './common';
|
||||
import { TransactionExtended, MempoolBlockWithTransactions, AuditScore } from '../mempool.interfaces';
|
||||
import blocksRepository from '../repositories/BlocksRepository';
|
||||
import blocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||
import blocks from '../api/blocks';
|
||||
|
||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||
|
||||
@ -81,7 +86,7 @@ class Audit {
|
||||
}
|
||||
overflowWeight += tx.weight;
|
||||
}
|
||||
totalWeight += tx.weight
|
||||
totalWeight += tx.weight;
|
||||
}
|
||||
|
||||
// transactions missing from near the end of our template are probably not being censored
|
||||
@ -97,7 +102,7 @@ class Audit {
|
||||
}
|
||||
if (mempool[txid].effectiveFeePerVsize > maxOverflowRate) {
|
||||
maxOverflowRate = mempool[txid].effectiveFeePerVsize;
|
||||
rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005
|
||||
rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005;
|
||||
}
|
||||
} else if (mempool[txid].effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding
|
||||
if (isCensored[txid]) {
|
||||
@ -117,6 +122,45 @@ class Audit {
|
||||
score
|
||||
};
|
||||
}
|
||||
|
||||
public async $getBlockAuditScores(fromHeight?: number, limit: number = 15): Promise<AuditScore[]> {
|
||||
let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
|
||||
const returnScores: AuditScore[] = [];
|
||||
|
||||
if (currentHeight < 0) {
|
||||
return returnScores;
|
||||
}
|
||||
|
||||
for (let i = 0; i < limit && currentHeight >= 0; i++) {
|
||||
const block = blocks.getBlocks().find((b) => b.height === currentHeight);
|
||||
if (block?.extras?.matchRate != null) {
|
||||
returnScores.push({
|
||||
hash: block.id,
|
||||
matchRate: block.extras.matchRate
|
||||
});
|
||||
} else {
|
||||
let currentHash;
|
||||
if (!currentHash && Common.indexingEnabled()) {
|
||||
const dbBlock = await blocksRepository.$getBlockByHeight(currentHeight);
|
||||
if (dbBlock && dbBlock['id']) {
|
||||
currentHash = dbBlock['id'];
|
||||
}
|
||||
}
|
||||
if (!currentHash) {
|
||||
currentHash = await bitcoinApi.$getBlockHash(currentHeight);
|
||||
}
|
||||
if (currentHash) {
|
||||
const auditScore = await blocksAuditsRepository.$getBlockAuditScore(currentHash);
|
||||
returnScores.push({
|
||||
hash: currentHash,
|
||||
matchRate: auditScore?.matchRate
|
||||
});
|
||||
}
|
||||
}
|
||||
currentHeight--;
|
||||
}
|
||||
return returnScores;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Audit();
|
@ -195,9 +195,9 @@ class Blocks {
|
||||
};
|
||||
}
|
||||
|
||||
const auditSummary = await BlocksAuditsRepository.$getShortBlockAudit(block.id);
|
||||
if (auditSummary) {
|
||||
blockExtended.extras.matchRate = auditSummary.matchRate;
|
||||
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
|
||||
if (auditScore != null) {
|
||||
blockExtended.extras.matchRate = auditScore.matchRate;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Application, Request, Response } from 'express';
|
||||
import config from "../../config";
|
||||
import logger from '../../logger';
|
||||
import audits from '../audit';
|
||||
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
||||
import BlocksRepository from '../../repositories/BlocksRepository';
|
||||
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
|
||||
@ -26,6 +27,9 @@ class MiningRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp)
|
||||
;
|
||||
@ -276,6 +280,29 @@ class MiningRoutes {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getBlockAuditScores(req: Request, res: Response) {
|
||||
try {
|
||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(await audits.$getBlockAuditScores(height, 15));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlockAuditScore(req: Request, res: Response) {
|
||||
try {
|
||||
const audit = await BlocksAuditsRepository.$getBlockAuditScore(req.params.hash);
|
||||
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||
res.json(audit || 'null');
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MiningRoutes();
|
||||
|
@ -32,6 +32,11 @@ export interface BlockAudit {
|
||||
matchRate: number,
|
||||
}
|
||||
|
||||
export interface AuditScore {
|
||||
hash: string,
|
||||
matchRate?: number,
|
||||
}
|
||||
|
||||
export interface MempoolBlock {
|
||||
blockSize: number;
|
||||
blockVSize: number;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import { BlockAudit } from '../mempool.interfaces';
|
||||
import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
||||
|
||||
class BlocksAuditRepositories {
|
||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||
@ -72,10 +72,10 @@ class BlocksAuditRepositories {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getShortBlockAudit(hash: string): Promise<any> {
|
||||
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`SELECT hash as id, match_rate as matchRate
|
||||
`SELECT hash, match_rate as matchRate
|
||||
FROM blocks_audits
|
||||
WHERE blocks_audits.hash = "${hash}"
|
||||
`);
|
||||
|
@ -114,7 +114,7 @@
|
||||
<td i18n="block.health">Block health</td>
|
||||
<td>
|
||||
<a *ngIf="block.extras?.matchRate != null" [routerLink]="['/block-audit/' | relativeUrl, blockHash]">{{ block.extras.matchRate }}%</a>
|
||||
<span *ngIf="block.extras?.matchRate == null" i18n="unknown">Unknown</span>
|
||||
<span *ngIf="block.extras?.matchRate === null" i18n="unknown">Unknown</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
@ -4,7 +4,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
|
||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Observable, of, Subscription, asyncScheduler, EMPTY } from 'rxjs';
|
||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, Subject } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
@ -60,6 +60,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
nextBlockTxListSubscription: Subscription = undefined;
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean;
|
||||
fetchAuditScore$ = new Subject<string>();
|
||||
fetchAuditScoreSubscription: Subscription;
|
||||
|
||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||
|
||||
@ -105,12 +107,30 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (block.id === this.blockHash) {
|
||||
this.block = block;
|
||||
if (this.block.id && this.block?.extras?.matchRate == null) {
|
||||
this.fetchAuditScore$.next(this.block.id);
|
||||
}
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.indexingAvailable) {
|
||||
this.fetchAuditScoreSubscription = this.fetchAuditScore$
|
||||
.pipe(
|
||||
switchMap((hash) => this.apiService.getBlockAuditScore$(hash)),
|
||||
catchError(() => EMPTY),
|
||||
)
|
||||
.subscribe((score) => {
|
||||
if (score && score.hash === this.block.id) {
|
||||
this.block.extras.matchRate = score.matchRate || null;
|
||||
} else {
|
||||
this.block.extras.matchRate = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const block$ = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const blockHash: string = params.get('id') || '';
|
||||
@ -209,6 +229,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
|
||||
if (this.block.id && this.block?.extras?.matchRate == null) {
|
||||
this.fetchAuditScore$.next(this.block.id);
|
||||
}
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
this.transactionsError = null;
|
||||
@ -311,6 +334,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.networkChangedSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.fetchAuditScoreSubscription?.unsubscribe();
|
||||
this.unsubscribeNextBlockSubscriptions();
|
||||
}
|
||||
|
||||
|
@ -46,22 +46,17 @@
|
||||
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<a *ngIf="block.extras?.matchRate != null" class="clear-link" [routerLink]="['/block-audit/' | relativeUrl, block.id]">
|
||||
<a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block-audit/' | relativeUrl, block.id] : null">
|
||||
<div class="progress progress-health">
|
||||
<div class="progress-bar progress-bar-health" role="progressbar"
|
||||
[ngStyle]="{'width': (100 - (block.extras?.matchRate || 0)) + '%' }"></div>
|
||||
[ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div>
|
||||
<div class="progress-text">
|
||||
<span>{{ block.extras.matchRate }}%</span>
|
||||
<span *ngIf="auditScores[block.id] != null;">{{ auditScores[block.id] }}%</span>
|
||||
<span *ngIf="auditScores[block.id] === undefined" class="skeleton-loader"></span>
|
||||
<span *ngIf="auditScores[block.id] === null">~</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div *ngIf="block.extras?.matchRate == null" class="progress progress-health">
|
||||
<div class="progress-bar progress-bar-health" role="progressbar"
|
||||
[ngStyle]="{'width': '100%' }"></div>
|
||||
<div class="progress-text">
|
||||
<span>~</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
|
||||
|
@ -196,6 +196,10 @@ tr, td, th {
|
||||
@media (max-width: 950px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-text .skeleton-loader {
|
||||
top: -8.5px;
|
||||
}
|
||||
}
|
||||
.health.widget {
|
||||
width: 25%;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs';
|
||||
import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, concat, Observable, timer, EMPTY, Subscription, of } from 'rxjs';
|
||||
import { catchError, delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@ -12,10 +12,14 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
styleUrls: ['./blocks-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlocksList implements OnInit {
|
||||
export class BlocksList implements OnInit, OnDestroy {
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
blocks$: Observable<BlockExtended[]> = undefined;
|
||||
auditScores: { [hash: string]: number | void } = {};
|
||||
|
||||
auditScoreSubscription: Subscription;
|
||||
latestScoreSubscription: Subscription;
|
||||
|
||||
indexingAvailable = false;
|
||||
isLoading = true;
|
||||
@ -105,6 +109,53 @@ export class BlocksList implements OnInit {
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
|
||||
if (this.indexingAvailable) {
|
||||
this.auditScoreSubscription = this.fromHeightSubject.pipe(
|
||||
switchMap((fromBlockHeight) => {
|
||||
return this.apiService.getBlockAuditScores$(this.page === 1 ? undefined : fromBlockHeight)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
})
|
||||
).subscribe((scores) => {
|
||||
Object.values(scores).forEach(score => {
|
||||
this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
|
||||
});
|
||||
});
|
||||
|
||||
this.latestScoreSubscription = this.stateService.blocks$.pipe(
|
||||
switchMap((block) => {
|
||||
if (block[0]?.extras?.matchRate != null) {
|
||||
return of({
|
||||
hash: block[0].id,
|
||||
matchRate: block[0]?.extras?.matchRate,
|
||||
});
|
||||
}
|
||||
else if (block[0]?.id && this.auditScores[block[0].id] === undefined) {
|
||||
return this.apiService.getBlockAuditScore$(block[0].id)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
).subscribe((score) => {
|
||||
if (score && score.hash) {
|
||||
this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.auditScoreSubscription?.unsubscribe();
|
||||
this.latestScoreSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
pageChange(page: number) {
|
||||
|
@ -152,6 +152,11 @@ export interface RewardStats {
|
||||
totalTx: number;
|
||||
}
|
||||
|
||||
export interface AuditScore {
|
||||
hash: string;
|
||||
matchRate?: number;
|
||||
}
|
||||
|
||||
export interface ITopNodesPerChannels {
|
||||
publicKey: string,
|
||||
alias: string,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats } from '../interfaces/node-api.interface';
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore } from '../interfaces/node-api.interface';
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
@ -234,6 +234,19 @@ export class ApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getBlockAuditScores$(from: number): Observable<AuditScore[]> {
|
||||
return this.httpClient.get<AuditScore[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` +
|
||||
(from !== undefined ? `/${from}` : ``)
|
||||
);
|
||||
}
|
||||
|
||||
getBlockAuditScore$(hash: string) : Observable<any> {
|
||||
return this.httpClient.get<any>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/score/` + hash
|
||||
);
|
||||
}
|
||||
|
||||
getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
|
||||
return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user