From 128290d6dbd732d9247c09eba9a47e6f3375e584 Mon Sep 17 00:00:00 2001 From: Michal Rzeszutko Date: Thu, 19 Feb 2026 13:47:18 +0000 Subject: [PATCH] feat: run low priority eviction rule on chain_pruned --- .../low_priority_eviction_rule.test.ts | 58 ++++++++++++++++++- .../eviction/low_priority_eviction_rule.ts | 16 +++-- .../mem_pools/tx_pool_v2/tx_pool_v2.test.ts | 52 +++++++++++++++++ 3 files changed, 118 insertions(+), 8 deletions(-) diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.test.ts index 93744abc603b..3915b1ca6652 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.test.ts @@ -43,7 +43,7 @@ describe('LowPriorityEvictionRule', () => { }); describe('evict method', () => { - describe('non-TXS_ADDED events', () => { + describe('BLOCK_MINED events', () => { it('returns empty result for BLOCK_MINED event', async () => { const context: EvictionContext = { event: EvictionEvent.BLOCK_MINED, @@ -60,8 +60,45 @@ describe('LowPriorityEvictionRule', () => { txsEvicted: [], }); }); + }); + + describe('CHAIN_PRUNED events', () => { + it('evicts transactions when pool is over limit', async () => { + rule.updateConfig({ maxPendingTxCount: 2 }); + pool = createPoolOps(4, ['0x3333', '0x4444']); + + const context: EvictionContext = { + event: EvictionEvent.CHAIN_PRUNED, + blockNumber: BlockNumber(1), + }; + + const result = await rule.evict(context, pool); + + expect(result.success).toBe(true); + expect(result.txsEvicted).toEqual(['0x3333', '0x4444']); + expect(deleteTxsMock).toHaveBeenCalledWith(['0x3333', '0x4444'], 'LowPriorityEviction'); + }); + + it('returns empty result when pool is under limit', async () => { + pool = createPoolOps(50); + + const context: EvictionContext = { + event: EvictionEvent.CHAIN_PRUNED, + blockNumber: BlockNumber(1), + }; + + const result = await rule.evict(context, pool); + + expect(result).toEqual({ + reason: 'low_priority', + success: true, + txsEvicted: [], + }); + }); + + it('returns empty result when maxPoolSize is 0', async () => { + rule.updateConfig({ maxPendingTxCount: 0 }); - it('returns empty result for CHAIN_PRUNED event', async () => { const context: EvictionContext = { event: EvictionEvent.CHAIN_PRUNED, blockNumber: BlockNumber(1), @@ -75,6 +112,23 @@ describe('LowPriorityEvictionRule', () => { txsEvicted: [], }); }); + + it('handles error from pool operations', async () => { + rule.updateConfig({ maxPendingTxCount: 1 }); + pool = createPoolOps(2, ['0x3333']); + deleteTxsMock.mockRejectedValue(new Error('Test error')); + + const context: EvictionContext = { + event: EvictionEvent.CHAIN_PRUNED, + blockNumber: BlockNumber(1), + }; + + const result = await rule.evict(context, pool); + + expect(result.success).toBe(false); + expect(result.txsEvicted).toEqual([]); + expect(result.error?.message).toContain('Failed to evict low priority txs'); + }); }); describe('TXS_ADDED events', () => { diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.ts index 047695aaf681..283c9fe3467b 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.ts @@ -5,7 +5,7 @@ import { EvictionEvent } from './interfaces.js'; /** * Eviction rule that removes low-priority transactions when the pool exceeds configured limits. - * Only triggers on TXS_ADDED events. + * Triggers on TXS_ADDED and CHAIN_PRUNED events. */ export class LowPriorityEvictionRule implements EvictionRule { public readonly name = 'LowPriorityEviction'; @@ -18,7 +18,7 @@ export class LowPriorityEvictionRule implements EvictionRule { } async evict(context: EvictionContext, pool: PoolOperations): Promise { - if (context.event !== EvictionEvent.TXS_ADDED) { + if (context.event !== EvictionEvent.TXS_ADDED && context.event !== EvictionEvent.CHAIN_PRUNED) { return { reason: 'low_priority', success: true, @@ -51,15 +51,19 @@ export class LowPriorityEvictionRule implements EvictionRule { this.log.info(`Evicting low priority txs. Pending tx count above limit: ${currentTxCount} > ${this.maxPoolSize}`); const numberToEvict = currentTxCount - this.maxPoolSize; const txsToEvict = pool.getLowestPriorityPending(numberToEvict); - const toEvictSet = new Set(txsToEvict); - const numNewTxsEvicted = context.newTxHashes.filter(newTxHash => toEvictSet.has(newTxHash)).length; if (txsToEvict.length > 0) { - this.log.info(`Evicted ${txsToEvict.length} low priority txs, including ${numNewTxsEvicted} newly added txs`); + if (context.event === EvictionEvent.TXS_ADDED) { + const toEvictSet = new Set(txsToEvict); + const numNewTxsEvicted = context.newTxHashes.filter(newTxHash => toEvictSet.has(newTxHash)).length; + this.log.info(`Evicted ${txsToEvict.length} low priority txs, including ${numNewTxsEvicted} newly added txs`); + } else { + this.log.info(`Evicted ${txsToEvict.length} low priority txs after chain prune`); + } await pool.deleteTxs(txsToEvict, this.name); } - this.log.debug(`Evicted ${txsToEvict.length} low priority txs, including ${numNewTxsEvicted} newly added txs`, { + this.log.debug(`Evicted ${txsToEvict.length} low priority txs`, { txHashes: txsToEvict, }); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts index 83106caec308..2095815d1762 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts @@ -1817,6 +1817,58 @@ describe('TxPoolV2', () => { expect(await pool.getTxStatus(txMined.getTxHash())).toBe('deleted'); expectRemovedTxs(txMined); // txMined deleted }); + + it('evicts low priority txs after chain prune when pool exceeds limit', async () => { + const txLow = await mockTxWithFee(1, 1); + const txMed = await mockTxWithFee(2, 5); + const txHigh = await mockTxWithFee(3, 10); + + // Add all 3 txs (no pool limit by default) + await pool.addPendingTxs([txLow, txMed, txHigh]); + expectAddedTxs(txLow, txMed, txHigh); + expect(await pool.getPendingTxCount()).toBe(3); + + // Mine all three + await pool.handleMinedBlock(makeBlock([txLow, txMed, txHigh], slot1Header)); + expectNoCallbacks(); + expect(await pool.getPendingTxCount()).toBe(0); + + // Now set pool limit to 2 + await pool.updateConfig({ maxPendingTxCount: 2 }); + + // Prune - all 3 txs return to pending, but pool limit is 2 + await pool.handlePrunedBlocks(block0Id); + + // Lowest priority tx should be evicted + const pending = toStrings(await pool.getPendingTxHashes()); + expect(pending).toHaveLength(2); + expect(pending).toContain(hashOf(txMed)); + expect(pending).toContain(hashOf(txHigh)); + expect(await pool.getTxStatus(txLow.getTxHash())).toBe('deleted'); + }); + + it('does not evict txs after chain prune when pool is within limit', async () => { + const tx1 = await mockTxWithFee(1, 1); + const tx2 = await mockTxWithFee(2, 2); + + await pool.addPendingTxs([tx1, tx2]); + expectAddedTxs(tx1, tx2); + expect(await pool.getPendingTxCount()).toBe(2); + + // Mine both + await pool.handleMinedBlock(makeBlock([tx1, tx2], slot1Header)); + expectNoCallbacks(); + + // Set limit to 3 (above what will be restored) + await pool.updateConfig({ maxPendingTxCount: 3 }); + + // Prune - both txs return to pending, under the limit + await pool.handlePrunedBlocks(block0Id); + + expect(await pool.getPendingTxCount()).toBe(2); + expect(await pool.getTxStatus(tx1.getTxHash())).toBe('pending'); + expect(await pool.getTxStatus(tx2.getTxHash())).toBe('pending'); + }); }); describe('validation during restore', () => {