diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 72877e64c819..d1a9ebdf626e 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -997,6 +997,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { public async setConfig(config: Partial): Promise { const newConfig = { ...this.config, ...config }; await this.sequencer?.updateSequencerConfig(config); + await this.p2pClient.updateP2PConfig(config); if (newConfig.realProofs !== this.config.realProofs) { this.proofVerifier = config.realProofs ? await BBCircuitVerifier.new(newConfig) : new TestCircuitVerifier(); diff --git a/yarn-project/end-to-end/src/e2e_mempool_limit.test.ts b/yarn-project/end-to-end/src/e2e_mempool_limit.test.ts new file mode 100644 index 000000000000..eb1f1e7344bd --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_mempool_limit.test.ts @@ -0,0 +1,55 @@ +import { AztecAddress, TxStatus, type Wallet, retryUntil } from '@aztec/aztec.js'; +import { TokenContract } from '@aztec/noir-contracts.js/Token'; +import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; + +import { jest } from '@jest/globals'; + +import { setup } from './fixtures/utils.js'; + +describe('e2e_mempool_limit', () => { + let wallet: Wallet; + let aztecNodeAdmin: AztecNodeAdmin | undefined; + let token: TokenContract; + + beforeAll(async () => { + ({ aztecNodeAdmin, wallet } = await setup(1)); + + if (!aztecNodeAdmin) { + throw new Error('Aztec node admin API must be available for this test'); + } + + token = await TokenContract.deploy(wallet, wallet.getAddress(), 'TEST', 'T', 18).send().deployed(); + await token.methods + .mint_to_public(wallet.getAddress(), 10n ** 18n) + .send() + .wait(); + }); + + it('should evict txs if there are too many', async () => { + const tx1 = await token.methods.transfer_in_public(wallet.getAddress(), await AztecAddress.random(), 1, 0).prove(); + const txSize = tx1.getSize(); + + // set a min tx greater than the mempool so that the sequencer doesn't all of a sudden build a block + await aztecNodeAdmin!.setConfig({ maxTxPoolSize: Math.floor(2.5 * txSize), minTxsPerBlock: 4 }); + + const tx2 = await token.methods.transfer_in_public(wallet.getAddress(), await AztecAddress.random(), 1, 0).prove(); + const tx3 = await token.methods.transfer_in_public(wallet.getAddress(), await AztecAddress.random(), 1, 0).prove(); + + const sentTx1 = tx1.send(); + await expect(sentTx1.getReceipt()).resolves.toEqual(expect.objectContaining({ status: TxStatus.PENDING })); + + const sentTx2 = tx2.send(); + await expect(sentTx1.getReceipt()).resolves.toEqual(expect.objectContaining({ status: TxStatus.PENDING })); + await expect(sentTx2.getReceipt()).resolves.toEqual(expect.objectContaining({ status: TxStatus.PENDING })); + + const sendSpy = jest.spyOn(wallet, 'sendTx'); + const sentTx3 = tx3.send(); + + // this retry is needed becauase tx3 is sent asynchronously and we need to wait for the event loop to fully drain + await retryUntil(() => sendSpy.mock.results[0]?.value); + + // one of the txs will be dropped. Which one is picked is somewhat random because all three will have the same fee + const receipts = await Promise.all([sentTx1.getReceipt(), sentTx2.getReceipt(), sentTx3.getReceipt()]); + expect(receipts.reduce((count, r) => (r.status === TxStatus.PENDING ? count + 1 : count), 0)).toBeLessThan(3); + }); +}); diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index 7687b9fb1366..8ee52a4c92c0 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -167,6 +167,8 @@ export type P2P = ProverCoordinati /** Identifies a p2p client. */ isP2PClient(): true; + + updateP2PConfig(config: Partial): Promise; }; /** @@ -200,6 +202,8 @@ export class P2PClient private blockStream; + private config: P2PConfig; + /** * In-memory P2P client constructor. * @param store - The client's instance of the KV store. @@ -221,10 +225,13 @@ export class P2PClient ) { super(telemetry, 'P2PClient'); - const { keepProvenTxsInPoolFor, blockCheckIntervalMS, blockRequestBatchSize, keepAttestationsInPoolFor } = { + this.config = { ...getP2PDefaultConfig(), ...config, }; + + const { keepProvenTxsInPoolFor, blockCheckIntervalMS, blockRequestBatchSize, keepAttestationsInPoolFor } = + this.config; this.keepProvenTxsFor = keepProvenTxsInPoolFor; this.keepAttestationsInPoolFor = keepAttestationsInPoolFor; @@ -256,6 +263,13 @@ export class P2PClient return this.synchedBlockHashes.getAsync(number); } + public async updateP2PConfig(config: Partial): Promise { + if (typeof config.maxTxPoolSize === 'number' && this.config.maxTxPoolSize !== config.maxTxPoolSize) { + await this.txPool.setMaxTxPoolSize(config.maxTxPoolSize); + this.config.maxTxPoolSize = config.maxTxPoolSize; + } + } + public async getL2Tips(): Promise { const latestBlockNumber = await this.getSyncedLatestBlockNum(); let latestBlockHash: string | undefined; diff --git a/yarn-project/p2p/src/mem_pools/tx_pool/aztec_kv_tx_pool.ts b/yarn-project/p2p/src/mem_pools/tx_pool/aztec_kv_tx_pool.ts index 23822717e755..1f7b9094a5b9 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool/aztec_kv_tx_pool.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool/aztec_kv_tx_pool.ts @@ -341,6 +341,11 @@ export class AztecKVTxPool implements TxPool { return vals.map(x => TxHash.fromString(x)); } + public setMaxTxPoolSize(maxSizeBytes: number | undefined): Promise { + this.#maxTxPoolSize = maxSizeBytes; + return Promise.resolve(); + } + /** * Creates a GasTxValidator instance. * @param db - DB for the validator to use diff --git a/yarn-project/p2p/src/mem_pools/tx_pool/memory_tx_pool.ts b/yarn-project/p2p/src/mem_pools/tx_pool/memory_tx_pool.ts index d516888d7a4e..ba3a72974df0 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool/memory_tx_pool.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool/memory_tx_pool.ts @@ -171,4 +171,8 @@ export class InMemoryTxPool implements TxPool { public getAllTxHashes(): Promise { return Promise.resolve(Array.from(this.txs.keys()).map(x => TxHash.fromBigInt(x))); } + + setMaxTxPoolSize(_maxSizeBytes: number | undefined): Promise { + return Promise.resolve(); + } } diff --git a/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool.ts b/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool.ts index 8af093dfabec..346a581d10d4 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool.ts @@ -73,4 +73,10 @@ export interface TxPool { * @returns Pending or mined depending on its status, or undefined if not found. */ getTxStatus(txHash: TxHash): Promise<'pending' | 'mined' | undefined>; + + /** + * Configure the maximum size of the tx pool + * @param maxSizeBytes - The maximum size in bytes of the mempool. Set to undefined to disable it + */ + setMaxTxPoolSize(maxSizeBytes: number | undefined): Promise; } diff --git a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts index 435f10f631a3..d17e45654ef2 100644 --- a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts +++ b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts @@ -45,6 +45,7 @@ function mockTxPool(): TxPool { getPendingTxHashes: () => Promise.resolve([]), getMinedTxHashes: () => Promise.resolve([]), getTxStatus: () => Promise.resolve(TxStatus.PENDING), + setMaxTxPoolSize: () => Promise.resolve(), }; } diff --git a/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts b/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts index 975ab2ed8403..471c60a54a05 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts @@ -15,7 +15,7 @@ export interface AztecNodeAdmin { * Updates the configuration of this node. * @param config - Updated configuration to be merged with the current one. */ - setConfig(config: Partial): Promise; + setConfig(config: Partial): Promise; /** * Forces the next block to be built bypassing all time and pending checks.