diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index e359fa9a4..ea8154206 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -595,7 +595,7 @@ class BitcoinRoutes { private async getRbfHistory(req: Request, res: Response) { try { const result = rbfCache.getReplaces(req.params.txId); - res.json(result?.txids || []); + res.json(result || []); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 80dc846d7..4c475502c 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -236,6 +236,7 @@ class Mempool { const lazyDeleteAt = this.mempoolCache[tx].deleteAfter; if (lazyDeleteAt && lazyDeleteAt < now) { delete this.mempoolCache[tx]; + rbfCache.evict(tx); } } } diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index a81362655..410239e73 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -1,46 +1,29 @@ import { TransactionExtended } from "../mempool.interfaces"; -export interface CachedRbf { - txid: string; - expires: Date; -} - -export interface CachedRbfs { - txids: string[]; - expires: Date; -} - class RbfCache { - private replacedby: { [txid: string]: CachedRbf; } = {}; - private replaces: { [txid: string]: CachedRbfs } = {}; + private replacedBy: { [txid: string]: string; } = {}; + private replaces: { [txid: string]: string[] } = {}; private txs: { [txid: string]: TransactionExtended } = {}; + private expiring: { [txid: string]: Date } = {}; constructor() { setInterval(this.cleanup.bind(this), 1000 * 60 * 60); } public add(replacedTx: TransactionExtended, newTxId: string): void { - const expiry = new Date(Date.now() + 1000 * 604800); // 1 week - this.replacedby[replacedTx.txid] = { - expires: expiry, - txid: newTxId, - }; + this.replacedBy[replacedTx.txid] = newTxId; this.txs[replacedTx.txid] = replacedTx; if (!this.replaces[newTxId]) { - this.replaces[newTxId] = { - txids: [], - expires: expiry, - }; + this.replaces[newTxId] = []; } - this.replaces[newTxId].txids.push(replacedTx.txid); - this.replaces[newTxId].expires = expiry; + this.replaces[newTxId].push(replacedTx.txid); } - public getReplacedBy(txId: string): CachedRbf | undefined { - return this.replacedby[txId]; + public getReplacedBy(txId: string): string | undefined { + return this.replacedBy[txId]; } - public getReplaces(txId: string): CachedRbfs | undefined { + public getReplaces(txId: string): string[] | undefined { return this.replaces[txId]; } @@ -48,17 +31,32 @@ class RbfCache { return this.txs[txId]; } + // flag a transaction as removed from the mempool + public evict(txid): void { + this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours + } + private cleanup(): void { const currentDate = new Date(); - for (const c in this.replacedby) { - if (this.replacedby[c].expires < currentDate) { - delete this.replacedby[c]; - delete this.txs[c]; + for (const txid in this.expiring) { + if (this.expiring[txid] < currentDate) { + delete this.expiring[txid]; + this.remove(txid); } } - for (const c in this.replaces) { - if (this.replaces[c].expires < currentDate) { - delete this.replaces[c]; + } + + // remove a transaction & all previous versions from the cache + private remove(txid): void { + // don't remove a transaction while a newer version remains in the mempool + if (this.replaces[txid] && !this.replacedBy[txid]) { + const replaces = this.replaces[txid]; + delete this.replaces[txid]; + for (const tx of replaces) { + // recursively remove prior versions from the cache + delete this.replacedBy[tx]; + delete this.txs[tx]; + this.remove(tx); } } } diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 0007205be..3ca49293d 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -58,10 +58,10 @@ class WebsocketHandler { client['track-tx'] = parsedMessage['track-tx']; // Client is telling the transaction wasn't found if (parsedMessage['watch-mempool']) { - const rbfCacheTx = rbfCache.getReplacedBy(client['track-tx']); - if (rbfCacheTx) { + const rbfCacheTxid = rbfCache.getReplacedBy(client['track-tx']); + if (rbfCacheTxid) { response['txReplaced'] = { - txid: rbfCacheTx.txid, + txid: rbfCacheTxid, }; client['track-tx'] = null; } else { @@ -467,6 +467,7 @@ class WebsocketHandler { for (const txId of txIds) { delete _memPool[txId]; removed.push(txId); + rbfCache.evict(txId); } if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {