Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions yarn-project/p2p/src/client/p2p_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,19 +275,58 @@ describe('P2P Client', () => {
});

describe('Chain prunes', () => {
it('calls handlePrunedBlocks when chain is pruned', async () => {
it('passes deleteAllTxs: false when prune does not cross a checkpoint boundary', async () => {
client = createClient({ txPoolDeleteTxsAfterReorg: true });
blockSource.setProvenBlockNumber(0);
// Only checkpoint up to block 90 — blocks 91-100 are proposed but not checkpointed
blockSource.setCheckpointedBlockNumber(90);
await client.start();

// Prune the chain back to block 90
// Prune 5 blocks (91-100): checkpointed tip stays at checkpoint 90
blockSource.removeBlocks(10);
await client.sync();

// Verify handlePrunedBlocks is called with the correct block ID
expect(txPool.handlePrunedBlocks).toHaveBeenCalledWith({
number: BlockNumber(90),
hash: expect.any(String),
});
expect(txPool.handlePrunedBlocks).toHaveBeenCalledWith(
{ number: BlockNumber(90), hash: expect.any(String) },
{ deleteAllTxs: false },
);
await client.stop();
});

it('passes deleteAllTxs: true when prune crosses a checkpoint boundary', async () => {
client = createClient({ txPoolDeleteTxsAfterReorg: true });
blockSource.setProvenBlockNumber(0);
// Checkpoint all 100 blocks
blockSource.setCheckpointedBlockNumber(100);
await client.start();

// Prune 5 blocks (96-100): checkpointed tip moves from checkpoint 100 to 95
blockSource.removeBlocks(5);
await client.sync();

expect(txPool.handlePrunedBlocks).toHaveBeenCalledWith(
{ number: BlockNumber(95), hash: expect.any(String) },
{ deleteAllTxs: true },
);
await client.stop();
});

it('passes deleteAllTxs: false for cross-checkpoint prune when txPoolDeleteTxsAfterReorg is disabled', async () => {
// Default config has txPoolDeleteTxsAfterReorg: false
blockSource.setProvenBlockNumber(0);
// Checkpoint all 100 blocks
blockSource.setCheckpointedBlockNumber(100);
await client.start();

// Prune 5 blocks (96-100): checkpointed tip moves from checkpoint 100 to 95
blockSource.removeBlocks(5);
await client.sync();

// Should delete all txs but flag is off
expect(txPool.handlePrunedBlocks).toHaveBeenCalledWith(
{ number: BlockNumber(95), hash: expect.any(String) },
{ deleteAllTxs: false },
);
await client.stop();
});

Expand Down
30 changes: 26 additions & 4 deletions yarn-project/p2p/src/client/p2p_client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants';
import type { EpochCacheInterface } from '@aztec/epoch-cache';
import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { createLogger } from '@aztec/foundation/log';
import { RunningPromise } from '@aztec/foundation/promise';
import { DateProvider } from '@aztec/foundation/timer';
import type { AztecAsyncKVStore, AztecAsyncSingleton } from '@aztec/kv-store';
import { L2TipsKVStore } from '@aztec/kv-store/stores';
import {
type CheckpointId,
type EthAddress,
type L2Block,
type L2BlockId,
Expand Down Expand Up @@ -200,7 +201,7 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
break;
case 'chain-pruned':
this.txCollection.stopCollectingForBlocksAfter(event.block.number);
await this.handlePruneL2Blocks(event.block);
await this.handlePruneL2Blocks(event.block, event.checkpoint);
break;
case 'chain-checkpointed':
break;
Expand Down Expand Up @@ -750,10 +751,31 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>

/**
* Updates the tx pool after a chain prune.
* Detects epoch prunes (checkpoint number changed) and deletes all txs in that case.
* @param latestBlock - The block ID the chain was pruned to.
* @param newCheckpoint - The checkpoint ID after the prune.
*/
private async handlePruneL2Blocks(latestBlock: L2BlockId): Promise<void> {
await this.txPool.handlePrunedBlocks(latestBlock);
private async handlePruneL2Blocks(latestBlock: L2BlockId, newCheckpoint: CheckpointId): Promise<void> {
const deleteAllTxs = this.config.txPoolDeleteTxsAfterReorg && (await this.isEpochPrune(newCheckpoint));
await this.txPool.handlePrunedBlocks(latestBlock, { deleteAllTxs });
}

/**
* Returns true if the prune crossed a checkpoint boundary.
* If the old and new checkpoint numbers are the same, the prune is within a single checkpoint.
* If they differ, the prune spans across checkpoints (epoch prune).
*/
private async isEpochPrune(newCheckpoint: CheckpointId): Promise<boolean> {
const tips = await this.l2Tips.getL2Tips();
const oldCheckpointNumber = tips.checkpointed.checkpoint.number;
if (oldCheckpointNumber <= CheckpointNumber.ZERO) {
return false;
}
const isEpochPrune = oldCheckpointNumber !== newCheckpoint.number;
this.log.info(
`Detected epoch prune: ${isEpochPrune}. Old checkpoint: ${oldCheckpointNumber}, new checkpoint: ${newCheckpoint.number}`,
);
return isEpochPrune;
}

/** Checks if the slot has changed and calls prepareForSlot if so. */
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export interface TxPoolV2 extends TypedEventEmitter<TxPoolV2Events> {
* and validates them before returning to pending.
* @param latestBlock - The latest valid block ID after the prune
*/
handlePrunedBlocks(latestBlock: L2BlockId): Promise<void>;
handlePrunedBlocks(latestBlock: L2BlockId, options?: { deleteAllTxs?: boolean }): Promise<void>;

/**
* Handles failed transaction execution.
Expand Down
21 changes: 21 additions & 0 deletions yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,27 @@ describe('TxPoolV2', () => {
expectNoCallbacks(); // handlePrunedBlocks restores to pending, no removal
});

it('deleteAllTxs option deletes all un-mined txs instead of restoring to pending', async () => {
const tx1 = await mockTx(1);
const tx2 = await mockTx(2);
await pool.addPendingTxs([tx1, tx2]);
expectAddedTxs(tx1, tx2);

// Mine both txs
await pool.handleMinedBlock(makeBlock([tx1, tx2], slot1Header));
expectNoCallbacks();
expect(await pool.getTxStatus(tx1.getTxHash())).toBe('mined');
expect(await pool.getTxStatus(tx2.getTxHash())).toBe('mined');

// Prune with deleteAllTxs - should delete all instead of restoring to pending
await pool.handlePrunedBlocks(block0Id, { deleteAllTxs: true });

expect(await pool.getTxStatus(tx1.getTxHash())).toBe('deleted');
expect(await pool.getTxStatus(tx2.getTxHash())).toBe('deleted');
expect(await pool.getPendingTxCount()).toBe(0);
expectRemovedTxs(tx1, tx2);
});

it('un-mined tx with higher priority evicts conflicting pending tx', async () => {
// Ensure anchor block is valid
db.findLeafIndices.mockResolvedValue([1n]);
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export class AztecKVTxPoolV2 extends (EventEmitter as new () => TypedEventEmitte
return this.#queue.put(() => this.#impl.prepareForSlot(slotNumber));
}

handlePrunedBlocks(latestBlock: L2BlockId): Promise<void> {
return this.#queue.put(() => this.#impl.handlePrunedBlocks(latestBlock));
handlePrunedBlocks(latestBlock: L2BlockId, options?: { deleteAllTxs?: boolean }): Promise<void> {
return this.#queue.put(() => this.#impl.handlePrunedBlocks(latestBlock, options));
}

handleFailedExecution(txHashes: TxHash[]): Promise<void> {
Expand Down
14 changes: 12 additions & 2 deletions yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ export class TxPoolV2Impl {
}
}

async handlePrunedBlocks(latestBlock: L2BlockId): Promise<void> {
async handlePrunedBlocks(latestBlock: L2BlockId, options?: { deleteAllTxs?: boolean }): Promise<void> {
// Step 1: Find transactions mined after the prune point
const txsToUnmine = this.#indices.findTxsMinedAfter(latestBlock.number);
if (txsToUnmine.length === 0) {
Expand All @@ -535,10 +535,20 @@ export class TxPoolV2Impl {
this.#indices.markAsUnmined(meta);
}

// If deleteAllTxs is set (epoch prune), delete all un-mined txs and return early
if (options?.deleteAllTxs) {
const allTxHashes = txsToUnmine.map(m => m.txHash);
await this.#deleteTxsBatch(allTxHashes);
this.#log.info(
`Handled prune to block ${latestBlock.number} with deleteAllTxs: deleted ${allTxHashes.length} txs`,
);
return;
}

// Step 4: Filter out protected txs (they'll be handled by prepareForSlot)
const unprotectedTxs = this.#indices.filterUnprotected(txsToUnmine);

// Step 4: Validate for pending pool
// Step 5: Validate for pending pool
const { valid, invalid } = await this.#revalidateMetadata(unprotectedTxs, 'during handlePrunedBlocks');

// Step 6: Resolve nullifier conflicts and add winners to pending indices
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/p2p/src/test-helpers/testbench-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class InMemoryTxPool extends EventEmitter implements TxPoolV2 {
return Promise.resolve();
}

handlePrunedBlocks(_latestBlock: L2BlockId): Promise<void> {
handlePrunedBlocks(_latestBlock: L2BlockId, _options?: { deleteAllTxs?: boolean }): Promise<void> {
return Promise.resolve();
}

Expand Down
Loading