fix cluster mempool tx eviction bug

This commit is contained in:
mononaut 2026-06-04 07:30:03 +00:00
parent 2d7a7d5a35
commit cea125c121
No known key found for this signature in database
GPG key ID: BFD16BE592A9CD8D
5 changed files with 78 additions and 28 deletions

View file

@ -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;

View file

@ -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<string, string> }).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<string, string> }).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: {},
});

View file

@ -511,7 +511,7 @@ class Blocks {
if (config.MEMPOOL.CLUSTER_MEMPOOL) {
memPool.clusterMempool?.applyMempoolChange({
added: [],
removed: txIds,
removed: transactions,
accelerations: mempool.getAccelerations(),
});
}

View file

@ -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(),
});
}

View file

@ -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;
}
}