From 38909cfc4253ca0d62e0c4b1a374e4b0769728d6 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 5 Aug 2023 16:08:54 +0900 Subject: [PATCH 1/6] 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 => { From 156b5d0b3c33e775eeb158f16a4d4d4be48ed710 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 16 Aug 2023 18:30:18 +0900 Subject: [PATCH 2/6] Update bulk /txs to use new failover router, internal-api path --- backend/src/api/bitcoin/esplora-api.ts | 2 +- backend/src/api/rbf-cache.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 1ebfef8c8..3264e8725 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -214,7 +214,7 @@ class ElectrsApi implements AbstractBitcoinApi { } async $getRawTransactions(txids: string[]): Promise { - return this.$postWrapper(config.ESPLORA.REST_API_URL + '/txs', txids, 'json'); + return this.failoverRouter.$post('/internal-api/txs', txids, 'json'); } async $getMempoolTransactions(txids: string[]): Promise { diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index 7d95df27b..b7d6f11a6 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -505,10 +505,13 @@ class RbfCache { if (config.MEMPOOL.BACKEND === 'esplora') { const sliceLength = 10000; + let count = 0; 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); + count += txs.length; + logger.info(`Fetched ${count} of ${txids.length} unknown-status RBF transactions from esplora`); processTxs(txs); } catch (err) { logger.err('failed to fetch some cached rbf transactions'); From 031e14f30277ca74f65abec1bd81bc56b60aea81 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 28 Aug 2023 16:55:50 +0900 Subject: [PATCH 3/6] Update internal getRawTransactions to use new prefix --- backend/src/api/bitcoin/esplora-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 3264e8725..8f47921c2 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -214,7 +214,7 @@ class ElectrsApi implements AbstractBitcoinApi { } async $getRawTransactions(txids: string[]): Promise { - return this.failoverRouter.$post('/internal-api/txs', txids, 'json'); + return this.failoverRouter.$post('/internal/txs', txids, 'json'); } async $getMempoolTransactions(txids: string[]): Promise { From 9d60c39aebf638f04b5efa84dce9ea92acd0ebb2 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 12 Nov 2023 06:19:46 +0000 Subject: [PATCH 4/6] Resolve rbf cache merge conflicts --- backend/src/api/rbf-cache.ts | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index b7d6f11a6..950e3a4e5 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -382,10 +382,11 @@ class RbfCache { this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime()); } }); - 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(); + logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); this.cleanup(); + } catch (e) { logger.err('failed to restore RBF cache: ' + (e instanceof Error ? e.message : e)); } @@ -423,31 +424,6 @@ class RbfCache { return; } - // check if any transactions in this tree have already been confirmed - mined = mined || treeInfo.mined; - let exists = mined; - if (!mined) { - try { - const apiTx = await bitcoinApi.$getRawTransaction(txid); - if (apiTx) { - exists = true; - } - if (apiTx?.status?.confirmed) { - mined = true; - treeInfo.txMined = true; - this.evict(txid, true); - } - } catch (e) { - // most transactions only exist in our cache - } - } - - // if the root tx is not in the mempool or the blockchain - // evict this tree as soon as possible - if (root === txid && !exists) { - this.evict(txid, true); - } - // recursively reconstruct child trees for (const childId of treeInfo.replaces) { const replaced = await this.importTree(root, childId, deflated, txs, mined); @@ -505,13 +481,10 @@ class RbfCache { if (config.MEMPOOL.BACKEND === 'esplora') { const sliceLength = 10000; - let count = 0; 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); - count += txs.length; - logger.info(`Fetched ${count} of ${txids.length} unknown-status RBF transactions from esplora`); processTxs(txs); } catch (err) { logger.err('failed to fetch some cached rbf transactions'); From 5998b54fec492da40d85779bd790b656890f03a9 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 12 Nov 2023 09:23:37 +0000 Subject: [PATCH 5/6] more logs, reduce request chunk sizes --- backend/src/api/bitcoin/esplora-api.ts | 3 ++- backend/src/api/rbf-cache.ts | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 8f47921c2..d980ad980 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -181,7 +181,8 @@ class FailoverRouter { .catch((e) => { let fallbackHost = this.fallbackHost; if (e?.response?.status !== 404) { - logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`); + logger.warn(`esplora request failed ${e?.response?.status} ${host.host}${path}`); + logger.warn(e instanceof Error ? e.message : e); fallbackHost = this.addFailure(host); } if (retry && e?.code === 'ECONNREFUSED' && this.multihost) { diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index 950e3a4e5..33653a33c 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -480,14 +480,16 @@ class RbfCache { }; if (config.MEMPOOL.BACKEND === 'esplora') { - const sliceLength = 10000; + const sliceLength = 1000; 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); + logger.debug(`fetched ${slice.length} cached rbf transactions`); processTxs(txs); + logger.debug(`processed ${slice.length} cached rbf transactions`); } catch (err) { - logger.err('failed to fetch some cached rbf transactions'); + logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`); } } } else { From c0a481acbe1f4cb1c4db40c32810819f4eed9071 Mon Sep 17 00:00:00 2001 From: wiz Date: Sun, 12 Nov 2023 19:09:36 +0900 Subject: [PATCH 6/6] Reduce /internal/txs RBF cache query chunk size to 250 --- backend/src/api/rbf-cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index 33653a33c..c573d3291 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -480,7 +480,7 @@ class RbfCache { }; if (config.MEMPOOL.BACKEND === 'esplora') { - const sliceLength = 1000; + const sliceLength = 250; for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) { const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength); try {