From cea125c12114e5e5329d4c2fd3bb867e3ea7cc64 Mon Sep 17 00:00:00 2001 From: mononaut Date: Thu, 4 Jun 2026 07:30:03 +0000 Subject: [PATCH] fix cluster mempool tx eviction bug --- .../cluster-mempool/harness/run-harness.ts | 6 +- .../cluster-mempool/cluster-mempool.test.ts | 70 ++++++++++++++++--- backend/src/api/blocks.ts | 2 +- backend/src/api/mempool.ts | 2 +- .../src/cluster-mempool/cluster-mempool.ts | 26 ++++--- 5 files changed, 78 insertions(+), 28 deletions(-) diff --git a/backend/src/__e2e__/cluster-mempool/harness/run-harness.ts b/backend/src/__e2e__/cluster-mempool/harness/run-harness.ts index 3e252122b..78c19844c 100644 --- a/backend/src/__e2e__/cluster-mempool/harness/run-harness.ts +++ b/backend/src/__e2e__/cluster-mempool/harness/run-harness.ts @@ -244,8 +244,10 @@ class Harness { const added = await this.fetchTransactions(addedTxids); - // Apply diff before deleting from cache — processRemovals needs the tx data - const diff: MempoolDiff = { added, removed: removedTxids, accelerations: {} }; + const removed = removedTxids + .map(txid => this.mempool[txid]) + .filter((tx): tx is MempoolTransactionExtended => !!tx); + const diff: MempoolDiff = { added, removed, accelerations: {} }; const t0 = Date.now(); this.clusterMempool.applyMempoolChange(diff); const dt = Date.now() - t0; diff --git a/backend/src/__tests__/cluster-mempool/cluster-mempool.test.ts b/backend/src/__tests__/cluster-mempool/cluster-mempool.test.ts index 92895017b..0e857bd33 100644 --- a/backend/src/__tests__/cluster-mempool/cluster-mempool.test.ts +++ b/backend/src/__tests__/cluster-mempool/cluster-mempool.test.ts @@ -115,15 +115,16 @@ describe('ClusterMempool', () => { it('should handle removing a tx', () => { const parentId = txid('a1'); const childId = txid('a2'); + const childTx = makeTx(childId, 5000, 100, [parentId]); const mempool = buildMempool([ makeTx(parentId, 100, 100), - makeTx(childId, 5000, 100, [parentId]), + childTx, ]); const cm = new ClusterMempool(mempool); cm.applyMempoolChange({ added: [], - removed: [childId], + removed: [childTx], accelerations: {}, }); @@ -132,13 +133,61 @@ describe('ClusterMempool', () => { expect(cm.getClusterInfo(parentId)).not.toBeNull(); }); + it('should clean spentBy when removed tx is already missing from mempool', () => { + const parentId = txid('a1'); + const childId = txid('a2'); + const childTx = makeTx(childId, 5000, 100, [parentId]); + const mempool = buildMempool([ + makeTx(parentId, 100, 100), + childTx, + ]); + const cm = new ClusterMempool(mempool); + const spentBy = (cm as unknown as { spentBy: Map }).spentBy; + + expect(spentBy.get(`${parentId}:0`)).toBe(childId); + + delete mempool[childId]; + cm.applyMempoolChange({ + added: [], + removed: [childTx], + accelerations: {}, + }); + + expect(spentBy.has(`${parentId}:0`)).toBe(false); + }); + + it('should not delete spentBy for a replacement transaction', () => { + const parentId = txid('a1'); + const replacedId = txid('a2'); + const replacementId = txid('a3'); + const replacedTx = makeTx(replacedId, 5000, 100, [parentId]); + const replacementTx = makeTx(replacementId, 6000, 100, [parentId]); + const mempool = buildMempool([ + makeTx(parentId, 100, 100), + replacementTx, + ]); + const cm = new ClusterMempool(mempool); + const spentBy = (cm as unknown as { spentBy: Map }).spentBy; + + expect(spentBy.get(`${parentId}:0`)).toBe(replacementId); + + cm.applyMempoolChange({ + added: [], + removed: [replacedTx], + accelerations: {}, + }); + + expect(spentBy.get(`${parentId}:0`)).toBe(replacementId); + }); + it('should split cluster when middle tx is removed', () => { const a = txid('a1'); const b = txid('b1'); const c = txid('c1'); + const bTx = makeTx(b, 200, 100, [a]); const mempool = buildMempool([ makeTx(a, 100, 100), - makeTx(b, 200, 100, [a]), + bTx, makeTx(c, 300, 100, [b]), ]); const cm = new ClusterMempool(mempool); @@ -146,7 +195,7 @@ describe('ClusterMempool', () => { cm.applyMempoolChange({ added: [], - removed: [b], + removed: [bTx], accelerations: {}, }); @@ -277,7 +326,7 @@ describe('ClusterMempool', () => { const cm = new ClusterMempool(mempool); expect(cm.getClusterCount()).toBe(1); - cm.applyMempoolChange({ added: [], removed: [center], accelerations: {} }); + cm.applyMempoolChange({ added: [], removed: [centerTx], accelerations: {} }); expect(cm.getTxCount()).toBe(5); expect(cm.getClusterCount()).toBe(5); @@ -297,7 +346,7 @@ describe('ClusterMempool', () => { const cm = new ClusterMempool(mempool); expect(cm.getClusterCount()).toBe(1); - cm.applyMempoolChange({ added: [], removed: [c], accelerations: {} }); + cm.applyMempoolChange({ added: [], removed: [cTx], accelerations: {} }); expect(cm.getClusterCount()).toBe(1); expect(cm.getTxCount()).toBe(2); @@ -306,13 +355,14 @@ describe('ClusterMempool', () => { it('should produce singleton when tx is removed from 2-tx cluster', () => { const a = txid('a1'); const b = txid('b1'); + const bTx = makeTx(b, 200, 100, [a]); const mempool = buildMempool([ makeTx(a, 100, 100), - makeTx(b, 200, 100, [a]), + bTx, ]); const cm = new ClusterMempool(mempool); - cm.applyMempoolChange({ added: [], removed: [b], accelerations: {} }); + cm.applyMempoolChange({ added: [], removed: [bTx], accelerations: {} }); expect(cm.getClusterCount()).toBe(1); expect(cm.getTxCount()).toBe(1); @@ -339,7 +389,7 @@ describe('ClusterMempool', () => { const cm = new ClusterMempool(mempool); expect(cm.getClusterCount()).toBe(1); - cm.applyMempoolChange({ added: [], removed: [hub], accelerations: {} }); + cm.applyMempoolChange({ added: [], removed: [hubTx], accelerations: {} }); expect(cm.getClusterCount()).toBe(3); expect(cm.getTxCount()).toBe(3); @@ -484,7 +534,7 @@ describe('ClusterMempool', () => { cm.applyMempoolChange({ added: [], - removed: [txid('nonexistent')], + removed: [makeTx(txid('nonexistent'), 100, 100)], accelerations: {}, }); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 613b77932..89abd0e38 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -511,7 +511,7 @@ class Blocks { if (config.MEMPOOL.CLUSTER_MEMPOOL) { memPool.clusterMempool?.applyMempoolChange({ added: [], - removed: txIds, + removed: transactions, accelerations: mempool.getAccelerations(), }); } diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 1bd919c6c..8c3e2e82a 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -398,7 +398,7 @@ class Mempool { if (config.MEMPOOL.CLUSTER_MEMPOOL && (newTransactions.length || deletedTransactions.length || accelerationDelta.length)) { this.clusterMempool?.applyMempoolChange({ added: newTransactions, - removed: deletedTransactions.map(tx => tx.txid), + removed: deletedTransactions, accelerations: this.getAccelerations(), }); } diff --git a/backend/src/cluster-mempool/cluster-mempool.ts b/backend/src/cluster-mempool/cluster-mempool.ts index e55e493ba..d3688b866 100644 --- a/backend/src/cluster-mempool/cluster-mempool.ts +++ b/backend/src/cluster-mempool/cluster-mempool.ts @@ -6,7 +6,7 @@ import logger from '../logger'; export interface MempoolDiff { added: MempoolTransactionExtended[]; - removed: string[]; + removed: MempoolTransactionExtended[]; accelerations: { [txid: string]: { feeDelta: number } }; } @@ -349,27 +349,25 @@ export class ClusterMempool { return relatives; } - private processRemovals(removed: string[]): void { - for (const txid of removed) { - const tx = this.mempool[txid]; - if (tx) { - for (const vin of tx.vin) { - if (!vin.is_coinbase) { - this.spentBy.delete(`${vin.txid}:${vin.vout}`); + private processRemovals(removed: MempoolTransactionExtended[]): void { + for (const tx of removed) { + for (const vin of tx.vin) { + if (!vin.is_coinbase) { + const spentOutpoint = `${vin.txid}:${vin.vout}`; + if (this.spentBy.get(spentOutpoint) === tx.txid) { + this.spentBy.delete(spentOutpoint); } } - } else if (this.txToCluster.has(txid)) { - logger.warn(`ClusterMempool.processRemovals: ${txid} missing from mempool, spentBy cleanup skipped`); } } - for (const txid of removed) { - const match = this.getClusterForTx(txid); + for (const tx of removed) { + const match = this.getClusterForTx(tx.txid); if (match) { match.cluster.depgraph.removeTransactions(new Set([match.clusterTx])); - match.cluster.txs.delete(txid); + match.cluster.txs.delete(tx.txid); match.cluster.linearization = match.cluster.linearization.filter(t => t !== match.clusterTx); - this.txToCluster.delete(txid); + this.txToCluster.delete(tx.txid); match.cluster.dirty = true; } }