From 99ea1ad0a05531a8a87f4e36ca4b6ec365ffc755 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 10 Jul 2024 13:13:53 +0900 Subject: [PATCH] Avoid fetching full audit on tx page --- backend/src/api/bitcoin/bitcoin.routes.ts | 15 +++++ backend/src/api/blocks.ts | 10 ++- backend/src/mempool.interfaces.ts | 13 ++++ .../repositories/BlocksAuditsRepository.ts | 37 ++++++++++- .../app/components/block/block.component.ts | 1 + .../components/tracker/tracker.component.ts | 1 + .../transaction/transaction.component.ts | 64 +++++++++++-------- frontend/src/app/services/api.service.ts | 7 ++ frontend/src/app/services/cache.service.ts | 10 +++ 9 files changed, 128 insertions(+), 30 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index ac848d4a4..742ffe242 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -42,6 +42,7 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) @@ -361,6 +362,20 @@ class BitcoinRoutes { } } + private async $getBlockTxAuditSummary(req: Request, res: Response) { + try { + const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid); + if (auditSummary) { + res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); + res.json(auditSummary); + } else { + return res.status(404).send(`transaction audit not available`); + } + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async getBlocks(req: Request, res: Response) { try { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 97db07027..9cc9233d5 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -2,7 +2,7 @@ import config from '../config'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import logger from '../logger'; import memPool from './mempool'; -import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces'; +import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces'; import { Common } from './common'; import diskCache from './disk-cache'; import transactionUtils from './transaction-utils'; @@ -1359,6 +1359,14 @@ class Blocks { } } + public async $getBlockTxAuditSummary(hash: string, txid: string): Promise { + if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { + return BlocksAuditsRepository.$getBlockTxAudit(hash, txid); + } else { + return null; + } + } + public getLastDifficultyAdjustmentTime(): number { return this.lastDifficultyAdjustmentTime; } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 34375604e..5e8026d15 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -42,6 +42,19 @@ export interface BlockAudit { matchRate: number, expectedFees?: number, expectedWeight?: number, + template?: any[]; +} + +export interface TransactionAudit { + seen?: boolean; + expected?: boolean; + added?: boolean; + prioritized?: boolean; + delayed?: number; + accelerated?: boolean; + conflict?: boolean; + coinbase?: boolean; + firstSeen?: number; } export interface AuditScore { diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts index daf1ba52d..1e0d28689 100644 --- a/backend/src/repositories/BlocksAuditsRepository.ts +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -1,7 +1,7 @@ import blocks from '../api/blocks'; import DB from '../database'; import logger from '../logger'; -import { BlockAudit, AuditScore } from '../mempool.interfaces'; +import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces'; class BlocksAuditRepositories { public async $saveAudit(audit: BlockAudit): Promise { @@ -98,6 +98,41 @@ class BlocksAuditRepositories { } } + public async $getBlockTxAudit(hash: string, txid: string): Promise { + try { + const blockAudit = await this.$getBlockAudit(hash); + + if (blockAudit) { + const isAdded = blockAudit.addedTxs.includes(txid); + const isPrioritized = blockAudit.prioritizedTxs.includes(txid); + const isAccelerated = blockAudit.acceleratedTxs.includes(txid); + const isConflict = blockAudit.fullrbfTxs.includes(txid); + let isExpected = false; + let firstSeen = undefined; + blockAudit.template?.forEach(tx => { + if (tx.txid === txid) { + isExpected = true; + firstSeen = tx.time; + } + }); + + return { + seen: isExpected || isPrioritized || isAccelerated, + expected: isExpected, + added: isAdded, + prioritized: isPrioritized, + conflict: isConflict, + accelerated: isAccelerated, + firstSeen, + } + } + return null; + } catch (e: any) { + logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $getBlockAuditScore(hash: string): Promise { try { const [rows]: any[] = await DB.query( diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 01702487f..980921a3c 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -295,6 +295,7 @@ export class BlockComponent implements OnInit, OnDestroy { ), !this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id) .pipe( + tap(() => this.cacheService.setBlockAuditLoaded(block.id)), catchError((err) => { this.overviewError = err; return of(null); diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 698226d50..72640822d 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -322,6 +322,7 @@ export class TrackerComponent implements OnInit, OnDestroy { }) ), fetchAudit ? this.apiService.getBlockAudit$(hash).pipe( + tap((blockAudit) => this.cacheService.setBlockAuditLoaded(hash)), map(audit => { const isAdded = audit.addedTxs.includes(txid); const isPrioritized = audit.prioritizedTxs.includes(txid); diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index ee0980e7c..0b2a4c861 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -42,7 +42,7 @@ interface Pool { slug: string; } -interface AuditStatus { +export interface TxAuditStatus { seen?: boolean; expected?: boolean; added?: boolean; @@ -100,7 +100,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { sigops: number | null; adjustedVsize: number | null; pool: Pool | null; - auditStatus: AuditStatus | null; + auditStatus: TxAuditStatus | null; isAcceleration: boolean = false; filters: Filter[] = []; showCpfpDetails = false; @@ -364,33 +364,41 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { const auditAvailable = this.isAuditAvailable(height); const isCoinbase = this.tx.vin.some(v => v.is_coinbase); const fetchAudit = auditAvailable && !isCoinbase; - return fetchAudit ? this.apiService.getBlockAudit$(hash).pipe( - map(audit => { - const isAdded = audit.addedTxs.includes(txid); - const isPrioritized = audit.prioritizedTxs.includes(txid); - const isAccelerated = audit.acceleratedTxs.includes(txid); - const isConflict = audit.fullrbfTxs.includes(txid); - const isExpected = audit.template.some(tx => tx.txid === txid); - const firstSeen = audit.template.find(tx => tx.txid === txid)?.time; - return { - seen: isExpected || isPrioritized || isAccelerated, - expected: isExpected, - added: isAdded, - prioritized: isPrioritized, - conflict: isConflict, - accelerated: isAccelerated, - firstSeen, - }; - }), - retry({ count: 3, delay: 2000 }), - catchError(() => { - return of(null); - }) - ) : of(isCoinbase ? { coinbase: true } : null); + if (fetchAudit) { + // If block audit is already cached, use it to get transaction audit + const blockAuditLoaded = this.cacheService.getBlockAuditLoaded(hash); + if (blockAuditLoaded) { + return this.apiService.getBlockAudit$(hash).pipe( + map(audit => { + const isAdded = audit.addedTxs.includes(txid); + const isPrioritized = audit.prioritizedTxs.includes(txid); + const isAccelerated = audit.acceleratedTxs.includes(txid); + const isConflict = audit.fullrbfTxs.includes(txid); + const isExpected = audit.template.some(tx => tx.txid === txid); + const firstSeen = audit.template.find(tx => tx.txid === txid)?.time; + return { + seen: isExpected || isPrioritized || isAccelerated, + expected: isExpected, + added: isAdded, + prioritized: isPrioritized, + conflict: isConflict, + accelerated: isAccelerated, + firstSeen, + }; + }) + ) + } else { + return this.apiService.getBlockTxAudit$(hash, txid).pipe( + retry({ count: 3, delay: 2000 }), + catchError(() => { + return of(null); + }) + ) + } + } else { + return of(isCoinbase ? { coinbase: true } : null); + } }), - catchError((e) => { - return of(null); - }) ).subscribe(auditStatus => { this.auditStatus = auditStatus; if (this.auditStatus?.firstSeen) { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 6b0d60ccf..9e2c008e4 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -8,6 +8,7 @@ import { Transaction } from '../interfaces/electrs.interface'; import { Conversion } from './price.service'; import { StorageService } from './storage.service'; import { WebsocketResponse } from '../interfaces/websocket.interface'; +import { TxAuditStatus } from '../components/transaction/transaction.component'; @Injectable({ providedIn: 'root' @@ -374,6 +375,12 @@ export class ApiService { ); } + getBlockTxAudit$(hash: string, txid: string) : Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/tx/${txid}/audit` + ); + } + getBlockAuditScores$(from: number): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` + diff --git a/frontend/src/app/services/cache.service.ts b/frontend/src/app/services/cache.service.ts index 993fcdfc6..7a3d6f0a2 100644 --- a/frontend/src/app/services/cache.service.ts +++ b/frontend/src/app/services/cache.service.ts @@ -20,6 +20,7 @@ export class CacheService { network: string; blockHashCache: { [hash: string]: BlockExtended } = {}; blockCache: { [height: number]: BlockExtended } = {}; + blockAuditLoaded: { [hash: string]: boolean } = {}; blockLoading: { [height: number]: boolean } = {}; copiesInBlockQueue: { [height: number]: number } = {}; blockPriorities: number[] = []; @@ -97,6 +98,10 @@ export class CacheService { } } + async setBlockAuditLoaded(hash: string) { + this.blockAuditLoaded[hash] = true; + } + // increase the priority of a block, to delay removal bumpBlockPriority(height) { this.blockPriorities.push(height); @@ -124,6 +129,7 @@ export class CacheService { resetBlockCache() { this.blockHashCache = {}; this.blockCache = {}; + this.blockAuditLoaded = {}; this.blockLoading = {}; this.copiesInBlockQueue = {}; this.blockPriorities = []; @@ -132,4 +138,8 @@ export class CacheService { getCachedBlock(height) { return this.blockCache[height]; } + + getBlockAuditLoaded(hash) { + return this.blockAuditLoaded[hash]; + } } \ No newline at end of file