diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.test.ts index 7341a099f5da..3b48d3cd47e1 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.test.ts @@ -75,7 +75,9 @@ describe('FeePayerBalanceEvictionRule', () => { db = mock(); mockWorldState.getCommitted.mockReturnValue(db); mockWorldState.getSnapshot.mockReturnValue(db); - mockWorldState.syncImmediate.mockResolvedValue(BlockNumber(1)); + mockWorldState.syncImmediate.mockImplementation((blockNumber?: number) => + Promise.resolve(BlockNumber(blockNumber ?? 1)), + ); feePayerBalances = new Map(); setupBalances(feePayerBalances); @@ -312,7 +314,7 @@ describe('FeePayerBalanceEvictionRule', () => { await rule.evict(context, pool); - expect(mockWorldState.syncImmediate).toHaveBeenCalledWith(5); + expect(mockWorldState.syncImmediate).toHaveBeenCalledWith(5, true); expect(mockWorldState.getSnapshot).toHaveBeenCalledWith(5); }); @@ -337,6 +339,29 @@ describe('FeePayerBalanceEvictionRule', () => { expect(result.success).toBe(true); expect(result.txsEvicted).toEqual([lowMeta.txHash]); }); + + it('skips eviction gracefully when block was pruned (reorg race)', async () => { + const meta = createMeta('0x1111', { feeLimit: 100n }); + const txsByFeePayer = new Map([[feePayer1, [meta]]]); + const pool = createPoolOps(txsByFeePayer); + setupBalances(new Map([[feePayer1, 0n]])); + + // Simulate reorg: syncImmediate returns a block number less than the target + mockWorldState.syncImmediate.mockResolvedValue(BlockNumber(3)); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, // block number 5 + newNullifiers: [], + feePayers: [feePayer1], + }; + + const result = await rule.evict(context, pool); + + expect(result.success).toBe(true); + expect(result.txsEvicted).toHaveLength(0); + expect(mockWorldState.getSnapshot).not.toHaveBeenCalled(); + }); }); describe('CHAIN_PRUNED events', () => { @@ -368,6 +393,32 @@ describe('FeePayerBalanceEvictionRule', () => { expect(mockWorldState.getSnapshot).toHaveBeenCalledWith(BlockNumber(3)); }); + it('reports failure when snapshot data is unavailable', async () => { + const meta = createMeta('0x1111', { feeLimit: 100n }); + const txsByFeePayer = new Map([[feePayer1, [meta]]]); + const pool = createPoolOps(txsByFeePayer); + + // Balance insufficient - would trigger eviction if snapshot were available + setupBalances(new Map([[feePayer1, 0n]])); + + // Make getSnapshot return a db that throws (simulating pruned block data) + const dbFailing = mock(); + dbFailing.getPreviousValueIndex.mockRejectedValue(new Error('Unable to find low leaf')); + mockWorldState.getSnapshot.mockReturnValue(dbFailing); + + const context: EvictionContext = { + event: EvictionEvent.CHAIN_PRUNED, + blockNumber: BlockNumber(3), + }; + + const result = await rule.evict(context, pool); + + // Error propagates to outer catch, reported as failure + expect(result.success).toBe(false); + expect(result.txsEvicted).toHaveLength(0); + expect(result.error).toBeDefined(); + }); + it('evicts txs when balance changed after prune', async () => { const meta = createMeta('0x1111', { feeLimit: 100n }); const txsByFeePayer = new Map([[feePayer1, [meta]]]); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts index 6bd67d2929d0..31bf344c4d7a 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts @@ -29,7 +29,15 @@ export class FeePayerBalanceEvictionRule implements EvictionRule { if (context.event === EvictionEvent.BLOCK_MINED) { const blockNumber = context.block.getBlockNumber(); - await this.worldState.syncImmediate(blockNumber); + const syncedTo = await this.worldState.syncImmediate(blockNumber, true); + if (syncedTo < blockNumber) { + // Block was likely pruned due to a reorg. Skip eviction; + // the subsequent CHAIN_PRUNED event will re-evaluate all pending fee payers. + this.log.debug( + `Skipping BLOCK_MINED fee payer eviction: world state at block ${syncedTo}, expected ${blockNumber}`, + ); + return { reason: this.reason, success: true, txsEvicted: [] }; + } return await this.evictForFeePayers(context.feePayers, this.worldState.getSnapshot(blockNumber), pool); } @@ -45,7 +53,7 @@ export class FeePayerBalanceEvictionRule implements EvictionRule { txsEvicted: [], }; } catch (err) { - this.log.error('Failed to evict txs due to fee payer balance', { err }); + this.log.warn('Failed to evict txs due to fee payer balance', { err }); return { reason: this.reason, success: false,