From 38909cfc4253ca0d62e0c4b1a374e4b0769728d6 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 5 Aug 2023 16:08:54 +0900 Subject: [PATCH] use bulk /txs endpoint to check cached rbf tx status --- .../bitcoin/bitcoin-api-abstract-factory.ts | 1 + backend/src/api/bitcoin/bitcoin-api.ts | 4 ++ backend/src/api/bitcoin/esplora-api.ts | 4 ++ backend/src/api/rbf-cache.ts | 53 +++++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 9407a5441..a76b93e8d 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -3,6 +3,7 @@ import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { $getRawMempool(): Promise; $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise; + $getRawTransactions(txids: string[]): Promise; $getMempoolTransactions(txids: string[]): Promise; $getAllMempoolTransactions(lastTxid: string); $getTransactionHex(txId: string): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 807baae2e..1be7993b8 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -60,6 +60,10 @@ class BitcoinApi implements AbstractBitcoinApi { }); } + $getRawTransactions(txids: string[]): Promise { + throw new Error('Method getRawTransactions not supported by the Bitcoin RPC API.'); + } + $getMempoolTransactions(txids: string[]): Promise { throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.'); } diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index af021bf2e..1ebfef8c8 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -213,6 +213,10 @@ class ElectrsApi implements AbstractBitcoinApi { return this.failoverRouter.$get('/tx/' + txId); } + async $getRawTransactions(txids: string[]): Promise { + return this.$postWrapper(config.ESPLORA.REST_API_URL + '/txs', txids, 'json'); + } + async $getMempoolTransactions(txids: string[]): Promise { return this.failoverRouter.$post('/internal/mempool/txs', txids, 'json'); } diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index 6e1f37afb..7d95df27b 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -2,6 +2,7 @@ import config from "../config"; import logger from "../logger"; import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces"; import bitcoinApi from './bitcoin/bitcoin-api-factory'; +import { IEsploraApi } from "./bitcoin/esplora-api.interface"; import { Common } from "./common"; import redisCache from "./redis-cache"; @@ -383,6 +384,7 @@ class RbfCache { }); logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); this.staleCount = 0; + await this.checkTrees(); this.cleanup(); } catch (e) { logger.err('failed to restore RBF cache: ' + (e instanceof Error ? e.message : e)); @@ -481,6 +483,57 @@ class RbfCache { return tree; } + private async checkTrees(): Promise { + const found: { [txid: string]: boolean } = {}; + const txids = Array.from(this.txs.values()).map(tx => tx.txid).filter(txid => { + return !this.expiring.has(txid) && !this.getRbfTree(txid)?.mined; + }); + + const processTxs = (txs: IEsploraApi.Transaction[]): void => { + for (const tx of txs) { + found[tx.txid] = true; + if (tx.status?.confirmed) { + const tree = this.getRbfTree(tx.txid); + if (tree) { + this.setTreeMined(tree, tx.txid); + tree.mined = true; + this.evict(tx.txid, false); + } + } + } + }; + + if (config.MEMPOOL.BACKEND === 'esplora') { + const sliceLength = 10000; + for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) { + const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength); + try { + const txs = await bitcoinApi.$getRawTransactions(slice); + processTxs(txs); + } catch (err) { + logger.err('failed to fetch some cached rbf transactions'); + } + } + } else { + const txs: IEsploraApi.Transaction[] = []; + for (const txid of txids) { + try { + const tx = await bitcoinApi.$getRawTransaction(txid, true, false); + txs.push(tx); + } catch (err) { + // some 404s are expected, so continue quietly + } + } + processTxs(txs); + } + + for (const txid of txids) { + if (!found[txid]) { + this.evict(txid, false); + } + } + } + public getLatestRbfSummary(): ReplacementInfo[] { const rbfList = this.getRbfTrees(false); return rbfList.slice(0, 6).map(rbfTree => {