From af3d7c1b47e2386c459d272e4e3532158e8651e5 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 9 Mar 2026 22:21:05 -0300 Subject: [PATCH 1/5] feat(p2p): reject and evict txs with insufficient max fee per gas Co-Authored-By: Claude Opus 4.6 --- yarn-project/p2p/src/client/factory.ts | 4 + .../mem_pools/tx_pool_v2/eviction/index.ts | 1 + ...fficient_fee_per_gas_eviction_rule.test.ts | 157 ++++++++++++++++++ .../insufficient_fee_per_gas_eviction_rule.ts | 61 +++++++ .../src/mem_pools/tx_pool_v2/tx_metadata.ts | 18 +- .../mem_pools/tx_pool_v2/tx_pool_v2_impl.ts | 2 + .../src/msg_validators/tx_validator/README.md | 14 +- .../tx_validator/factory.test.ts | 14 +- .../msg_validators/tx_validator/factory.ts | 4 +- .../tx_validator/gas_validator.test.ts | 46 ++++- .../tx_validator/gas_validator.ts | 88 ++++++---- 11 files changed, 357 insertions(+), 52 deletions(-) create mode 100644 yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts create mode 100644 yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index da52e261ceb9..6cfff77ecd72 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -7,6 +7,7 @@ import { AztecLMDBStoreV2, createStore } from '@aztec/kv-store/lmdb-v2'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ChainConfig } from '@aztec/stdlib/config'; import type { ContractDataSource } from '@aztec/stdlib/contract'; +import { GasFees } from '@aztec/stdlib/gas'; import type { AztecNode, ClientProtocolCircuitVerifier, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; @@ -88,6 +89,8 @@ export async function createP2PClient( const currentBlockNumber = await archiver.getBlockNumber(); const { ts: nextSlotTimestamp } = epochCache.getEpochAndSlotInNextL1Slot(); const l1Constants = await archiver.getL1Constants(); + const header = await archiver.getBlockHeader(BlockNumber(currentBlockNumber)); + const gasFees = header?.globalVariables.gasFees ?? GasFees.empty(); return createTxValidatorForTransactionsEnteringPendingTxPool( worldStateSynchronizer, nextSlotTimestamp, @@ -97,6 +100,7 @@ export async function createP2PClient( maxBlockL2Gas: config.validateMaxL2BlockGas, maxBlockDAGas: config.validateMaxDABlockGas, }, + gasFees, ); }, }, diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/index.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/index.ts index 79abcdc12812..acbe9165c5b9 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/index.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/index.ts @@ -21,6 +21,7 @@ export { FeePayerBalancePreAddRule } from './fee_payer_balance_pre_add_rule.js'; export { LowPriorityPreAddRule } from './low_priority_pre_add_rule.js'; // Post-event eviction rules +export { InsufficientFeePerGasEvictionRule } from './insufficient_fee_per_gas_eviction_rule.js'; export { InvalidTxsAfterMiningRule } from './invalid_txs_after_mining_rule.js'; export { InvalidTxsAfterReorgRule } from './invalid_txs_after_reorg_rule.js'; export { FeePayerBalanceEvictionRule } from './fee_payer_balance_eviction_rule.js'; diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts new file mode 100644 index 000000000000..26e44f463e5e --- /dev/null +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts @@ -0,0 +1,157 @@ +import { BlockNumber } from '@aztec/foundation/branded-types'; +import { GasFees } from '@aztec/stdlib/gas'; +import { BlockHeader } from '@aztec/stdlib/tx'; + +import { jest } from '@jest/globals'; + +import { type TxMetaData, stubTxMetaData } from '../tx_metadata.js'; +import { InsufficientFeePerGasEvictionRule } from './insufficient_fee_per_gas_eviction_rule.js'; +import type { EvictionContext, PoolOperations } from './interfaces.js'; +import { EvictionEvent } from './interfaces.js'; + +describe('InsufficientFeePerGasEvictionRule', () => { + let pool: PoolOperations; + let rule: InsufficientFeePerGasEvictionRule; + let deleteTxsMock: jest.MockedFunction; + + const createPoolOps = (pendingTxs: TxMetaData[]): PoolOperations => { + deleteTxsMock = jest.fn(() => Promise.resolve()); + return { + getPendingTxs: () => pendingTxs, + getPendingFeePayers: () => [...new Set(pendingTxs.map(t => t.feePayer))], + getFeePayerPendingTxs: (feePayer: string) => pendingTxs.filter(t => t.feePayer === feePayer), + getPendingTxCount: () => pendingTxs.length, + getLowestPriorityPending: () => [], + deleteTxs: deleteTxsMock as (txHashes: string[]) => Promise, + }; + }; + + beforeEach(() => { + pool = createPoolOps([]); + rule = new InsufficientFeePerGasEvictionRule(); + }); + + describe('non-BLOCK_MINED events', () => { + it('returns empty result for TXS_ADDED event', async () => { + const context: EvictionContext = { + event: EvictionEvent.TXS_ADDED, + newTxHashes: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result).toEqual({ + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: [], + }); + }); + + it('returns empty result for CHAIN_PRUNED event', async () => { + const context: EvictionContext = { + event: EvictionEvent.CHAIN_PRUNED, + blockNumber: BlockNumber(1), + }; + + const result = await rule.evict(context, pool); + + expect(result).toEqual({ + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: [], + }); + }); + }); + + describe('BLOCK_MINED events', () => { + let blockHeader: BlockHeader; + + beforeEach(() => { + blockHeader = BlockHeader.empty(); + blockHeader.globalVariables.blockNumber = BlockNumber(100); + blockHeader.globalVariables.timestamp = 1000n; + blockHeader.globalVariables.gasFees = new GasFees(10, 20); + }); + + it('evicts txs with insufficient DA fee per gas', async () => { + const tx1 = stubTxMetaData('0x1111', { maxFeesPerGas: new GasFees(9, 20) }); // DA too low + const tx2 = stubTxMetaData('0x2222', { maxFeesPerGas: new GasFees(10, 20) }); // Exactly enough + + pool = createPoolOps([tx1, tx2]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result.success).toBe(true); + expect(result.txsEvicted).toEqual([tx1.txHash]); + expect(deleteTxsMock).toHaveBeenCalledWith([tx1.txHash], 'InsufficientFeePerGas'); + }); + + it('evicts txs with insufficient L2 fee per gas', async () => { + const tx1 = stubTxMetaData('0x1111', { maxFeesPerGas: new GasFees(10, 19) }); // L2 too low + const tx2 = stubTxMetaData('0x2222', { maxFeesPerGas: new GasFees(10, 20) }); // Exactly enough + + pool = createPoolOps([tx1, tx2]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result.success).toBe(true); + expect(result.txsEvicted).toEqual([tx1.txHash]); + expect(deleteTxsMock).toHaveBeenCalledWith([tx1.txHash], 'InsufficientFeePerGas'); + }); + + it('keeps txs with sufficient fees', async () => { + const tx1 = stubTxMetaData('0x1111', { maxFeesPerGas: new GasFees(10, 20) }); + const tx2 = stubTxMetaData('0x2222', { maxFeesPerGas: new GasFees(100, 200) }); + + pool = createPoolOps([tx1, tx2]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result.success).toBe(true); + expect(result.txsEvicted).toEqual([]); + expect(deleteTxsMock).not.toHaveBeenCalled(); + }); + + it('handles empty pending list', async () => { + pool = createPoolOps([]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result).toEqual({ + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: [], + }); + expect(deleteTxsMock).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts new file mode 100644 index 000000000000..2ab3a136c9b5 --- /dev/null +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts @@ -0,0 +1,61 @@ +import { createLogger } from '@aztec/foundation/log'; + +import type { EvictionContext, EvictionResult, EvictionRule, PoolOperations } from './interfaces.js'; +import { EvictionEvent } from './interfaces.js'; + +/** + * Eviction rule that removes transactions whose maxFeesPerGas no longer meets + * the current block's gas fees after a new block is mined. + * Only triggers on BLOCK_MINED events. + */ +export class InsufficientFeePerGasEvictionRule implements EvictionRule { + public readonly name = 'InsufficientFeePerGas'; + + private log = createLogger('p2p:tx_pool_v2:insufficient_fee_per_gas_eviction_rule'); + + async evict(context: EvictionContext, pool: PoolOperations): Promise { + if (context.event !== EvictionEvent.BLOCK_MINED) { + return { + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: [], + }; + } + + try { + const { gasFees } = context.block.globalVariables; + const txsToEvict: string[] = []; + const pendingTxs = pool.getPendingTxs(); + + for (const meta of pendingTxs) { + const maxFeesPerGas = meta.data.constants.txContext.gasSettings.maxFeesPerGas; + if (maxFeesPerGas.feePerDaGas < gasFees.feePerDaGas || maxFeesPerGas.feePerL2Gas < gasFees.feePerL2Gas) { + this.log.verbose(`Evicting tx ${meta.txHash} from pool due to insufficient fee per gas`, { + txMaxFeesPerGas: maxFeesPerGas.toInspect(), + blockGasFees: gasFees.toInspect(), + }); + txsToEvict.push(meta.txHash); + } + } + + if (txsToEvict.length > 0) { + this.log.info(`Evicted ${txsToEvict.length} txs with insufficient fee per gas after block mined`); + await pool.deleteTxs(txsToEvict, this.name); + } + + return { + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: txsToEvict, + }; + } catch (err) { + this.log.error('Failed to evict transactions with insufficient fee per gas', { err }); + return { + reason: 'insufficient_fee_per_gas', + success: false, + txsEvicted: [], + error: new Error('Failed to evict txs with insufficient fee per gas', { cause: err }), + }; + } + } +} diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts index 1d2f434fb2fb..39666ae27cf0 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts @@ -2,7 +2,7 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { BlockHash, type L2BlockId } from '@aztec/stdlib/block'; -import { Gas } from '@aztec/stdlib/gas'; +import { Gas, GasFees } from '@aztec/stdlib/gas'; import { type Tx, TxHash } from '@aztec/stdlib/tx'; import { getFeePayerBalanceDelta } from '../../msg_validators/tx_validator/fee_payer_balance.js'; @@ -23,7 +23,7 @@ export type TxMetaValidationData = { }; }; txContext: { - gasSettings: { gasLimits: Gas }; + gasSettings: { gasLimits: Gas; maxFeesPerGas: GasFees }; }; }; }; @@ -124,7 +124,10 @@ export async function buildTxMetaData(tx: Tx): Promise { globalVariables: { blockNumber: anchorBlockNumber }, }, txContext: { - gasSettings: { gasLimits: tx.data.constants.txContext.gasSettings.gasLimits }, + gasSettings: { + gasLimits: tx.data.constants.txContext.gasSettings.gasLimits, + maxFeesPerGas: tx.data.constants.txContext.gasSettings.maxFeesPerGas, + }, }, }, }, @@ -277,7 +280,9 @@ export function checkNullifierConflict( } /** Creates a stub TxMetaValidationData for tests that don't exercise validators. */ -export function stubTxMetaValidationData(overrides: { expirationTimestamp?: bigint } = {}): TxMetaValidationData { +export function stubTxMetaValidationData( + overrides: { expirationTimestamp?: bigint; maxFeesPerGas?: GasFees } = {}, +): TxMetaValidationData { return { getNonEmptyNullifiers: () => [], expirationTimestamp: overrides.expirationTimestamp ?? 0n, @@ -287,7 +292,7 @@ export function stubTxMetaValidationData(overrides: { expirationTimestamp?: bigi globalVariables: { blockNumber: BlockNumber(0) }, }, txContext: { - gasSettings: { gasLimits: Gas.empty() }, + gasSettings: { gasLimits: Gas.empty(), maxFeesPerGas: overrides.maxFeesPerGas ?? GasFees.empty() }, }, }, }; @@ -304,6 +309,7 @@ export function stubTxMetaData( nullifiers?: string[]; expirationTimestamp?: bigint; anchorBlockHeaderHash?: string; + maxFeesPerGas?: GasFees; } = {}, ): TxMetaData { const txHashBigInt = Fr.fromHexString(txHash).toBigInt(); @@ -322,6 +328,6 @@ export function stubTxMetaData( expirationTimestamp, receivedAt: 0, estimatedSizeBytes: 0, - data: stubTxMetaValidationData({ expirationTimestamp }), + data: stubTxMetaValidationData({ expirationTimestamp, maxFeesPerGas: overrides.maxFeesPerGas }), }; } diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts index 53e88e0e806e..bcbc443f9b79 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts @@ -17,6 +17,7 @@ import { EvictionManager, FeePayerBalanceEvictionRule, FeePayerBalancePreAddRule, + InsufficientFeePerGasEvictionRule, InvalidTxsAfterMiningRule, InvalidTxsAfterReorgRule, LowPriorityEvictionRule, @@ -114,6 +115,7 @@ export class TxPoolV2Impl { // Post-event eviction rules (run after events to check ALL pending txs) this.#evictionManager.registerRule(new InvalidTxsAfterMiningRule()); + this.#evictionManager.registerRule(new InsufficientFeePerGasEvictionRule()); this.#evictionManager.registerRule(new InvalidTxsAfterReorgRule(deps.worldStateSynchronizer)); this.#evictionManager.registerRule(new FeePayerBalanceEvictionRule(deps.worldStateSynchronizer)); // LowPriorityEvictionRule handles cases where txs become pending via prepareForSlot (unprotect) diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/README.md b/yarn-project/p2p/src/msg_validators/tx_validator/README.md index 91087c1559c5..1ae77d3bd190 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/README.md +++ b/yarn-project/p2p/src/msg_validators/tx_validator/README.md @@ -75,7 +75,7 @@ This validator is invoked on **every** transaction potentially entering the pend - Startup hydration — revalidating persisted non-mined txs on node restart Runs: -- DoubleSpend, BlockHeader, GasLimits, Timestamp +- DoubleSpend, BlockHeader, GasLimits, MaxFeePerGas, Timestamp Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. @@ -89,8 +89,9 @@ Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. | `MetadataTxValidator` | Chain ID, rollup version, protocol contracts hash, VK tree root | 4.18 us | | `TimestampTxValidator` | Transaction has not expired (expiration timestamp vs next slot) | 1.56 us | | `DoubleSpendTxValidator` | Nullifiers do not already exist in the nullifier tree | 106.08 us | -| `GasTxValidator` | Gas limits are within bounds (delegates to `GasLimitsValidator`), max fee per gas meets current block fees, and fee payer has sufficient FeeJuice balance | 1.02 ms | +| `GasTxValidator` | Gas limits are within bounds (delegates to `GasLimitsValidator`), max fee per gas meets current block fees (delegates to `MaxFeePerGasValidator`), and fee payer has sufficient FeeJuice balance | 1.02 ms | | `GasLimitsValidator` | Gas limits are >= fixed minimums and <= AVM max processable L2 gas. Used standalone in pool migration; also called internally by `GasTxValidator` | 3–10 us | +| `MaxFeePerGasValidator` | Max fee per gas >= current block gas fees on both dimensions (DA and L2). Used standalone in pool migration; also called internally by `GasTxValidator` | 3–10 us | | `PhasesTxValidator` | Public function calls in setup phase are on the allow list | 10.12–13.12 us | | `BlockHeaderTxValidator` | Transaction's anchor block hash exists in the archive tree | 98.88 us | | `TxProofValidator` | Client proof verifies correctly | ~250ms | @@ -107,9 +108,16 @@ Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. | DoubleSpend | Stage 1 | Yes | — | Yes | Yes | | Gas (balance + limits) | Stage 1 | Optional* | — | Yes | — | | GasLimits (standalone) | — | — | — | — | Yes | +| MaxFeePerGas (standalone) | — | — | — | — | Yes | | Phases | Stage 1 | Yes | — | Yes | — | | BlockHeader | Stage 1 | Yes | — | Yes | Yes | | Proof | Stage 2 | Optional** | Yes | — | — | -\* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `GasLimitsValidator` as its first step, so gas limits are checked wherever `GasTxValidator` runs. Pool migration uses `GasLimitsValidator` standalone because it doesn't need the balance or fee-per-gas checks. +\* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `GasLimitsValidator` and `MaxFeePerGasValidator` as its first steps, so gas limits and fee-per-gas are checked wherever `GasTxValidator` runs. Pool migration uses `GasLimitsValidator` and `MaxFeePerGasValidator` standalone because it doesn't need the balance check. \** Proof verification is skipped for simulations (no verifier provided). + +## Fee-Per-Gas Rejection Strategy + +The `MaxFeePerGasValidator` and `InsufficientFeePerGasEvictionRule` reject and evict transactions whose `maxFeesPerGas` falls below the current block's gas fees. This is a simple strategy: if a tx can't pay the current fees, it gets rejected on entry and evicted after each new block. + +**Caveat**: This may evict transactions that would become valid again if block fees drop. A more nuanced approach would be to define a threshold (e.g., 50%) and only reject/evict when the tx's max fee falls below that fraction of the current fees. The current approach is simpler and ensures the pool doesn't accumulate transactions with low max fees that are unlikely to be mined soon. diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts index 7cb2e794566c..890014117acd 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts @@ -25,7 +25,7 @@ import { createTxValidatorForReqResponseReceivedTxs, createTxValidatorForTransactionsEnteringPendingTxPool, } from './factory.js'; -import { GasLimitsValidator, GasTxValidator } from './gas_validator.js'; +import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; import { PhasesTxValidator } from './phases_validator.js'; import { SizeTxValidator } from './size_validator.js'; @@ -304,11 +304,13 @@ describe('Validator factory functions', () => { 100n, BlockNumber(5), { rollupManaLimit: Number.MAX_SAFE_INTEGER }, + new GasFees(1, 1), ); const aggregate = validator as AggregateTxValidator; expect(getValidatorNames(aggregate)).toEqual([ GasLimitsValidator.name, + MaxFeePerGasValidator.name, TimestampTxValidator.name, DoubleSpendTxValidator.name, BlockHeaderTxValidator.name, @@ -316,9 +318,13 @@ describe('Validator factory functions', () => { }); it('syncs world state before creating the validator', async () => { - await createTxValidatorForTransactionsEnteringPendingTxPool(synchronizer, 100n, BlockNumber(5), { - rollupManaLimit: Number.MAX_SAFE_INTEGER, - }); + await createTxValidatorForTransactionsEnteringPendingTxPool( + synchronizer, + 100n, + BlockNumber(5), + { rollupManaLimit: Number.MAX_SAFE_INTEGER }, + new GasFees(1, 1), + ); expect(synchronizer.syncImmediate).toHaveBeenCalled(); }); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts index 9acd13d88c3d..371adf2ef89f 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -55,7 +55,7 @@ import { ArchiveCache } from './archive_cache.js'; import { type ArchiveSource, BlockHeaderTxValidator } from './block_header_validator.js'; import { DataTxValidator } from './data_validator.js'; import { DoubleSpendTxValidator, type NullifierSource } from './double_spend_validator.js'; -import { GasLimitsValidator, GasTxValidator } from './gas_validator.js'; +import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; import { NullifierCache } from './nullifier_cache.js'; import { PhasesTxValidator } from './phases_validator.js'; @@ -416,6 +416,7 @@ export async function createTxValidatorForTransactionsEnteringPendingTxPool( timestamp: bigint, blockNumber: BlockNumber, gasLimitOpts: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number }, + gasFees: GasFees, bindings?: LoggerBindings, ): Promise> { await worldStateSynchronizer.syncImmediate(); @@ -433,6 +434,7 @@ export async function createTxValidatorForTransactionsEnteringPendingTxPool( }; return new AggregateTxValidator( new GasLimitsValidator({ ...gasLimitOpts, bindings }), + new MaxFeePerGasValidator(gasFees, bindings), new TimestampTxValidator({ timestamp, blockNumber }, bindings), new DoubleSpendTxValidator(nullifierSource, bindings), new BlockHeaderTxValidator(archiveSource, bindings), diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts index 719be28a8454..1b0fe02e5935 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts @@ -27,7 +27,7 @@ import { import assert from 'assert'; import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; -import { GasLimitsValidator, GasTxValidator } from './gas_validator.js'; +import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { patchNonRevertibleFn, patchRevertibleFn } from './test_utils.js'; describe('GasTxValidator', () => { @@ -75,10 +75,6 @@ describe('GasTxValidator', () => { await expect(validateTx(tx)).resolves.toEqual({ result: 'invalid', reason: [reason] }); }; - const expectSkipped = async (tx: Tx, reason: string) => { - await expect(validateTx(tx)).resolves.toEqual({ result: 'skipped', reason: [reason] }); - }; - it('allows fee paying txs if fee payer has enough balance', async () => { mockBalance(feeLimit); await expectValid(tx); @@ -351,13 +347,45 @@ describe('GasTxValidator', () => { }); }); - it('skips txs with not enough fee per da gas', async () => { + it('rejects txs with not enough fee per da gas', async () => { gasFees.feePerDaGas = gasFees.feePerDaGas + 1n; - await expectSkipped(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); }); - it('skips txs with not enough fee per l2 gas', async () => { + it('rejects txs with not enough fee per l2 gas', async () => { gasFees.feePerL2Gas = gasFees.feePerL2Gas + 1n; - await expectSkipped(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); + }); +}); + +describe('MaxFeePerGasValidator', () => { + it('accepts tx with sufficient max fees per gas', async () => { + const gasFees = new GasFees(10, 20); + const validator = new MaxFeePerGasValidator(gasFees); + const tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ maxFeesPerGas: new GasFees(10, 20) }); + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }); + + it('rejects tx with insufficient DA fee per gas', async () => { + const gasFees = new GasFees(10, 20); + const validator = new MaxFeePerGasValidator(gasFees); + const tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ maxFeesPerGas: new GasFees(9, 20) }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [TX_ERROR_INSUFFICIENT_FEE_PER_GAS], + }); + }); + + it('rejects tx with insufficient L2 fee per gas', async () => { + const gasFees = new GasFees(10, 20); + const validator = new MaxFeePerGasValidator(gasFees); + const tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ maxFeesPerGas: new GasFees(10, 19) }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [TX_ERROR_INSUFFICIENT_FEE_PER_GAS], + }); }); }); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts index 19b94402e324..e791024232d0 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts @@ -36,6 +36,18 @@ export interface HasGasLimitData { }; } +/** Structural interface for types that carry max fee per gas data, used by {@link MaxFeePerGasValidator}. */ +export interface HasMaxFeePerGasData { + txHash: { toString(): string }; + data: { + constants: { + txContext: { + gasSettings: { maxFeesPerGas: GasFees }; + }; + }; + }; +} + /** * Validates that a transaction's gas limits are within acceptable bounds. * @@ -113,15 +125,55 @@ export class GasLimitsValidator implements TxValidato } } +/** + * Validates that a transaction's max fee per gas meets the current block's gas fees. + * + * Rejects transactions whose maxFeesPerGas is below the current block's gas fees + * on either dimension (DA or L2). This is a cheap, stateless check. + * + * Generic over T so it can validate both full {@link Tx} objects and {@link TxMetaData} + * (used during pending pool migration). + * + * Used by: pending pool migration (via factory), and indirectly by {@link GasTxValidator}. + */ +export class MaxFeePerGasValidator implements TxValidator { + #log: Logger; + #gasFees: GasFees; + + constructor(gasFees: GasFees, bindings?: LoggerBindings) { + this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings); + this.#gasFees = gasFees; + } + + validateTx(tx: T): Promise { + return Promise.resolve(this.validateMaxFeePerGas(tx)); + } + + /** Checks maxFeesPerGas >= current block gas fees on both dimensions. */ + validateMaxFeePerGas(tx: T): TxValidationResult { + const maxFeesPerGas = tx.data.constants.txContext.gasSettings.maxFeesPerGas; + const notEnoughMaxFees = + maxFeesPerGas.feePerDaGas < this.#gasFees.feePerDaGas || maxFeesPerGas.feePerL2Gas < this.#gasFees.feePerL2Gas; + + if (notEnoughMaxFees) { + this.#log.verbose(`Rejecting transaction ${tx.txHash.toString()} due to insufficient fee per gas`, { + txMaxFeesPerGas: maxFeesPerGas.toInspect(), + currentGasFees: this.#gasFees.toInspect(), + }); + return { result: 'invalid', reason: [TX_ERROR_INSUFFICIENT_FEE_PER_GAS] }; + } + return { result: 'valid' }; + } +} + /** * Validates that a transaction can pay its gas fees. * * Runs three checks in order: * 1. **Gas limits** (delegates to {@link GasLimitsValidator}) — rejects if limits are * out of bounds. - * 2. **Max fee per gas** — skips (not rejects) the tx if its maxFeesPerGas is below - * the current block's gas fees. We skip rather than reject because the tx may - * become eligible in a later block with lower fees. + * 2. **Max fee per gas** — rejects the tx if its maxFeesPerGas is below + * the current block's gas fees. * 3. **Fee payer balance** — reads the fee payer's FeeJuice balance from public state, * adds any pending claim from a setup-phase `_increase_public_balance` call, and * rejects if the total is less than the tx's fee limit (gasLimits * maxFeePerGas). @@ -155,37 +207,15 @@ export class GasTxValidator implements TxValidator { bindings: this.bindings, }).validateGasLimit(tx); if (gasLimitValidation.result === 'invalid') { - return Promise.resolve(gasLimitValidation); + return gasLimitValidation; } - if (this.#shouldSkip(tx)) { - return Promise.resolve({ result: 'skipped', reason: [TX_ERROR_INSUFFICIENT_FEE_PER_GAS] }); + const maxFeeValidation = new MaxFeePerGasValidator(this.#gasFees, this.bindings).validateMaxFeePerGas(tx); + if (maxFeeValidation.result === 'invalid') { + return maxFeeValidation; } return await this.validateTxFee(tx); } - /** - * Check whether the tx's max fees are valid for the current block, and skip if not. - * We skip instead of invalidating since the tx may become eligible later. - * Note that circuits check max fees even if fee payer is unset, so we - * keep this validation even if the tx does not pay fees. - */ - #shouldSkip(tx: Tx): boolean { - const gasSettings = tx.data.constants.txContext.gasSettings; - - // Skip the tx if its max fees are not enough for the current block's gas fees. - const maxFeesPerGas = gasSettings.maxFeesPerGas; - const notEnoughMaxFees = - maxFeesPerGas.feePerDaGas < this.#gasFees.feePerDaGas || maxFeesPerGas.feePerL2Gas < this.#gasFees.feePerL2Gas; - - if (notEnoughMaxFees) { - this.#log.verbose(`Skipping transaction ${tx.getTxHash().toString()} due to insufficient fee per gas`, { - txMaxFeesPerGas: maxFeesPerGas.toInspect(), - currentGasFees: this.#gasFees.toInspect(), - }); - } - return notEnoughMaxFees; - } - /** * Checks the fee payer has enough FeeJuice balance to cover the tx's fee limit. * Accounts for any pending claim from a setup-phase `_increase_public_balance` call. From 0614fbc9d08341071f21518f75c56b0f6a3de28a Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 10 Mar 2026 20:52:25 +0000 Subject: [PATCH 2/5] test(p2p): add tx pool tests for max fee per gas validation and eviction Co-Authored-By: Claude Opus 4.6 --- .../mem_pools/tx_pool_v2/tx_pool_v2.test.ts | 221 +++++++++++++++++- 1 file changed, 220 insertions(+), 1 deletion(-) 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 f778f6bb3f14..4cfa8672d574 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 @@ -27,7 +27,7 @@ import { BlockHeader, GlobalVariables, type Tx, TxEffect, TxHash, type TxValidat import { type MockProxy, mock } from 'jest-mock-extended'; -import { GasLimitsValidator } from '../../msg_validators/tx_validator/gas_validator.js'; +import { GasLimitsValidator, MaxFeePerGasValidator } from '../../msg_validators/tx_validator/gas_validator.js'; import type { TxMetaData } from './tx_metadata.js'; import { AztecKVTxPoolV2 } from './tx_pool_v2.js'; @@ -5386,4 +5386,223 @@ describe('TxPoolV2', () => { } }); }); + + describe('max fee per gas validation', () => { + let feePool: AztecKVTxPoolV2; + let feeStore: Awaited>; + let feeArchiveStore: Awaited>; + + // Block gas fees that the validator will compare against + const blockGasFees = new GasFees(10, 20); + + beforeEach(async () => { + feeStore = await openTmpStore('p2p'); + feeArchiveStore = await openTmpStore('archive'); + feePool = new AztecKVTxPoolV2(feeStore, feeArchiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(new MaxFeePerGasValidator(blockGasFees)), + }); + await feePool.start(); + }); + + afterEach(async () => { + await feePool.stop(); + await feeStore.delete(); + await feeArchiveStore.delete(); + }); + + const makeTxWithMaxFees = async (seed: number, maxFeesPerGas: GasFees) => { + const tx = await mockTx(seed, { numberOfNonRevertiblePublicCallRequests: 1 }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ maxFeesPerGas }); + return tx; + }; + + it('accepts tx with maxFeesPerGas exactly equal to block gas fees', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(10, 20)); + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(1); + expect(result.rejected).toHaveLength(0); + }); + + it('accepts tx with maxFeesPerGas above block gas fees', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(100, 200)); + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(1); + expect(result.rejected).toHaveLength(0); + }); + + it('rejects tx with insufficient DA fee per gas', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(9, 20)); // DA too low + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(0); + expect(toStrings(result.rejected)).toContain(hashOf(tx)); + }); + + it('rejects tx with insufficient L2 fee per gas', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(10, 19)); // L2 too low + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(0); + expect(toStrings(result.rejected)).toContain(hashOf(tx)); + }); + + it('rejects tx with both DA and L2 fee per gas insufficient', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(5, 10)); + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(0); + expect(toStrings(result.rejected)).toContain(hashOf(tx)); + }); + + it('handles batch with mixed sufficient and insufficient fees', async () => { + const txGood = await makeTxWithMaxFees(1, new GasFees(10, 20)); + const txBadDA = await makeTxWithMaxFees(2, new GasFees(9, 20)); + const txBadL2 = await makeTxWithMaxFees(3, new GasFees(10, 19)); + const txAlsoGood = await makeTxWithMaxFees(4, new GasFees(50, 50)); + + const result = await feePool.addPendingTxs([txGood, txBadDA, txBadL2, txAlsoGood]); + + expect(toStrings(result.accepted)).toContain(hashOf(txGood)); + expect(toStrings(result.accepted)).toContain(hashOf(txAlsoGood)); + expect(toStrings(result.rejected)).toContain(hashOf(txBadDA)); + expect(toStrings(result.rejected)).toContain(hashOf(txBadL2)); + expect(await feePool.getPendingTxCount()).toBe(2); + }); + }); + + describe('max fee per gas eviction after block mined', () => { + // The default pool already has InsufficientFeePerGasEvictionRule registered. + // Default mockTx uses maxFeesPerGas = GasFees(10, 10). + // The existing slot headers (slot1Header, slot2Header) use GasFees.empty() (0, 0), + // so we need a header with non-zero gas fees to trigger eviction. + + const makeTxWithMaxFees = async (seed: number, maxFeesPerGas: GasFees) => { + const tx = await mockTx(seed, { + numberOfNonRevertiblePublicCallRequests: 1, + maxPriorityFeesPerGas: new GasFees(1, 1), + }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ + maxFeesPerGas, + maxPriorityFeesPerGas: new GasFees(1, 1), + }); + return tx; + }; + + const headerWithGasFees = (gasFees: GasFees) => + BlockHeader.empty({ + globalVariables: GlobalVariables.empty({ + blockNumber: BlockNumber(1), + slotNumber: SlotNumber(1), + timestamp: 0n, + gasFees, + }), + }); + + it('evicts pending txs when mined block has higher gas fees', async () => { + // Txs with maxFeesPerGas = (10, 10) + const tx1 = await makeTxWithMaxFees(1, new GasFees(10, 10)); + const tx2 = await makeTxWithMaxFees(2, new GasFees(10, 10)); + + await pool.addPendingTxs([tx1, tx2]); + expect(await pool.getPendingTxCount()).toBe(2); + + // Mine a block with gas fees (20, 20) - higher than txs' maxFeesPerGas + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + // Both txs should be evicted since their maxFeesPerGas (10, 10) < block fees (20, 20) + expect(await pool.getTxStatus(tx1.getTxHash())).toBe('deleted'); + expect(await pool.getTxStatus(tx2.getTxHash())).toBe('deleted'); + expect(await pool.getPendingTxCount()).toBe(0); + }); + + it('keeps pending txs when their maxFeesPerGas meets block gas fees', async () => { + // Txs with maxFeesPerGas = (50, 50) + const tx1 = await makeTxWithMaxFees(1, new GasFees(50, 50)); + const tx2 = await makeTxWithMaxFees(2, new GasFees(50, 50)); + + await pool.addPendingTxs([tx1, tx2]); + expect(await pool.getPendingTxCount()).toBe(2); + + // Mine a block with gas fees (20, 20) - lower than txs' maxFeesPerGas + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + // Both txs should remain pending since their maxFeesPerGas (50, 50) >= block fees (20, 20) + expect(await pool.getTxStatus(tx1.getTxHash())).toBe('pending'); + expect(await pool.getTxStatus(tx2.getTxHash())).toBe('pending'); + expect(await pool.getPendingTxCount()).toBe(2); + }); + + it('selectively evicts only txs with insufficient fees', async () => { + const txLowFee = await makeTxWithMaxFees(1, new GasFees(5, 5)); + const txHighFee = await makeTxWithMaxFees(2, new GasFees(50, 50)); + const txBorderline = await makeTxWithMaxFees(3, new GasFees(20, 20)); + + await pool.addPendingTxs([txLowFee, txHighFee, txBorderline]); + expect(await pool.getPendingTxCount()).toBe(3); + + // Mine a block with gas fees (20, 20) + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + // txLowFee (5, 5) < (20, 20) -> evicted + expect(await pool.getTxStatus(txLowFee.getTxHash())).toBe('deleted'); + // txHighFee (50, 50) >= (20, 20) -> still pending + expect(await pool.getTxStatus(txHighFee.getTxHash())).toBe('pending'); + // txBorderline (20, 20) >= (20, 20) -> still pending (exactly equal is sufficient) + expect(await pool.getTxStatus(txBorderline.getTxHash())).toBe('pending'); + expect(await pool.getPendingTxCount()).toBe(2); + }); + + it('evicts when only DA fee is insufficient', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(5, 50)); // DA too low, L2 fine + + await pool.addPendingTxs([tx]); + expect(await pool.getPendingTxCount()).toBe(1); + + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('deleted'); + }); + + it('evicts when only L2 fee is insufficient', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(50, 5)); // L2 too low, DA fine + + await pool.addPendingTxs([tx]); + expect(await pool.getPendingTxCount()).toBe(1); + + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('deleted'); + }); + + it('does not evict when block gas fees are zero', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(10, 10)); + + await pool.addPendingTxs([tx]); + expect(await pool.getPendingTxCount()).toBe(1); + + // Mine a block with zero gas fees (GasFees.empty) + await pool.handleMinedBlock(makeEmptyBlock(slot1Header)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('pending'); + }); + + it('does not evict protected txs even with insufficient fees', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(5, 5)); + + // Add as protected (not pending) + await pool.addProtectedTxs([tx], slot1Header); + expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + + // Mine a block with gas fees higher than the tx's maxFeesPerGas + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + // Protected tx should not be evicted (eviction rules only check pending txs) + expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + }); + }); }); From 26534ab91256a3069848ea1189604d33669dc2d8 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 11 Mar 2026 10:44:29 -0300 Subject: [PATCH 3/5] update test wallet min fee padding to 10x --- yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts | 1 - yarn-project/end-to-end/src/fixtures/setup.ts | 2 +- yarn-project/end-to-end/src/test-wallet/test_wallet.ts | 5 ++++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts index a3c99dcf3226..39a0d7c80de4 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts @@ -129,7 +129,6 @@ export class P2PNetworkTest { metricsPort: metricsPort, numberOfInitialFundedAccounts: 2, startProverNode, - walletMinFeePadding: 2.0, }; this.deployL1ContractsArgs = { diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index acb06a638f73..e9d00ee3fd5a 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -195,7 +195,7 @@ export type SetupOptions = { skipAccountDeployment?: boolean; /** L1 contracts deployment arguments. */ l1ContractsArgs?: Partial; - /** Wallet minimum fee padding multiplier (defaults to 0.5, which is 50% padding). */ + /** Wallet minimum fee padding multiplier */ walletMinFeePadding?: number; } & Partial; diff --git a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts index 24dc0adb6c85..2249763da3e5 100644 --- a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts +++ b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts @@ -39,6 +39,8 @@ export interface AccountData { contract: AccountContract; } +const TEST_DEFAULT_MIN_FEE_PADDING = 10; + /** * Wallet implementation that stores accounts in memory and provides extra debugging * utilities @@ -50,6 +52,7 @@ export class TestWallet extends BaseWallet { private readonly nodeRef: AztecNodeProxy, ) { super(pxe, nodeRef); + this.minFeePadding = TEST_DEFAULT_MIN_FEE_PADDING; } static async create( @@ -143,7 +146,7 @@ export class TestWallet extends BaseWallet { } setMinFeePadding(value?: number) { - this.minFeePadding = value ?? 0.5; + this.minFeePadding = value ?? TEST_DEFAULT_MIN_FEE_PADDING; } protected getAccountFromAddress(address: AztecAddress): Promise { From 8391e717ad4adc6c3131e594ce9c3d9109ed76ee Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 12 Mar 2026 14:04:31 +0000 Subject: [PATCH 4/5] fix: use next block gas fees rather than last block for validation --- .../aztec-node/src/aztec-node/server.ts | 15 +++-- .../cli-wallet/src/utils/constants.ts | 2 +- .../client_flows/client_flows_benchmark.ts | 4 +- .../e2e_local_network_example.test.ts | 3 +- .../src/e2e_fees/account_init.test.ts | 7 +- .../end-to-end/src/e2e_fees/fees_test.ts | 6 +- .../end-to-end/src/fixtures/fixtures.ts | 3 + .../src/spartan/block_capacity.test.ts | 5 +- .../src/spartan/n_tps_prove.test.ts | 5 +- yarn-project/p2p/src/client/factory.ts | 7 +- .../proposal_tx_collector_worker.ts | 2 + ...fficient_fee_per_gas_eviction_rule.test.ts | 30 ++++++++- .../insufficient_fee_per_gas_eviction_rule.ts | 8 ++- .../src/mem_pools/tx_pool_v2/interfaces.ts | 3 + .../tx_pool_v2/tx_pool_v2.compat.test.ts | 6 ++ .../mem_pools/tx_pool_v2/tx_pool_v2.test.ts | 64 ++++++++++++++++--- .../tx_pool_v2/tx_pool_v2_bench.test.ts | 3 + .../mem_pools/tx_pool_v2/tx_pool_v2_impl.ts | 2 +- .../src/test-helpers/make-test-p2p-clients.ts | 2 + .../testbench/p2p_client_testbench_worker.ts | 2 + yarn-project/stdlib/src/gas/gas_fees.ts | 5 ++ .../stdlib/src/tx/global_variable_builder.ts | 6 +- yarn-project/yarn.lock | 1 + 23 files changed, 152 insertions(+), 39 deletions(-) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 242c8204f744..3d9067aedebf 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -330,6 +330,13 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems'); } + const globalVariableBuilder = new GlobalVariableBuilder({ + ...config, + rollupVersion: BigInt(config.rollupVersion), + l1GenesisTime, + slotDuration: Number(slotDuration), + }); + // create the tx pool and the p2p client, which will need the l2 block source const p2pClient = await createP2PClient( config, @@ -337,6 +344,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { proofVerifier, worldStateSynchronizer, epochCache, + globalVariableBuilder, packageVersion, dateProvider, telemetry, @@ -550,13 +558,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { } } - const globalVariableBuilder = new GlobalVariableBuilder({ - ...config, - rollupVersion: BigInt(config.rollupVersion), - l1GenesisTime, - slotDuration: Number(slotDuration), - }); - const node = new AztecNodeService( config, p2pClient, diff --git a/yarn-project/cli-wallet/src/utils/constants.ts b/yarn-project/cli-wallet/src/utils/constants.ts index ab747206179a..9c472b096e82 100644 --- a/yarn-project/cli-wallet/src/utils/constants.ts +++ b/yarn-project/cli-wallet/src/utils/constants.ts @@ -1,4 +1,4 @@ -export const MIN_FEE_PADDING = 0.5; +export const MIN_FEE_PADDING = 10; export const AccountTypes = ['schnorr', 'ecdsasecp256r1', 'ecdsasecp256r1ssh', 'ecdsasecp256k1'] as const; export type AccountType = (typeof AccountTypes)[number]; diff --git a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts index 0e19339412fd..cf1434476c4e 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts @@ -27,7 +27,7 @@ import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import { GasSettings } from '@aztec/stdlib/gas'; import { deriveSigningKey } from '@aztec/stdlib/keys'; -import { MNEMONIC } from '../../fixtures/fixtures.js'; +import { E2E_DEFAULT_MIN_FEE_PADDING, MNEMONIC } from '../../fixtures/fixtures.js'; import { type EndToEndContext, type SetupOptions, deployAccounts, setup, teardown } from '../../fixtures/setup.js'; import { mintTokensToPrivate } from '../../fixtures/token_utils.js'; import { setupSponsoredFPC } from '../../fixtures/utils.js'; @@ -377,7 +377,7 @@ export class ClientFlowsBenchmark { public async getPrivateFPCPaymentMethodForWallet(wallet: Wallet, sender: AztecAddress) { // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay - const maxFeesPerGas = (await this.aztecNode.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await this.aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); return new PrivateFeePaymentMethod(this.bananaFPC.address, sender, wallet, gasSettings); } diff --git a/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts b/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts index 7bd8d64e0fad..c311e1e225c5 100644 --- a/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts @@ -16,6 +16,7 @@ import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/test import { format } from 'util'; +import { E2E_DEFAULT_MIN_FEE_PADDING } from '../fixtures/fixtures.js'; import { deployToken, mintTokensToPrivate } from '../fixtures/token_utils.js'; import { TestWallet } from '../test-wallet/test_wallet.js'; @@ -185,7 +186,7 @@ describe('e2e_local_network_example', () => { // docs:start:private_fpc_payment // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay - const maxFeesPerGas = (await node.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await node.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); const paymentMethod = new PrivateFeePaymentMethod(bananaFPCAddress, alice, wallet, gasSettings); const { receipt: receiptForAlice } = await bananaCoin.methods diff --git a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts index a02e539963db..e948a58eaf11 100644 --- a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts @@ -16,6 +16,7 @@ import { GasSettings } from '@aztec/stdlib/gas'; import { jest } from '@jest/globals'; +import { E2E_DEFAULT_MIN_FEE_PADDING } from '../fixtures/fixtures.js'; import type { TestWallet } from '../test-wallet/test_wallet.js'; import { FeesTest } from './fees_test.js'; @@ -115,7 +116,7 @@ describe('e2e_fees account_init', () => { // Bob deploys his account through the private FPC // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay - const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); const paymentMethod = new PrivateFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); const { receipt: tx } = await bobsDeployMethod.send({ @@ -144,7 +145,7 @@ describe('e2e_fees account_init', () => { // The public fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay - const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); const paymentMethod = new PublicFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); const { receipt: tx } = await bobsDeployMethod.send({ @@ -203,7 +204,7 @@ describe('e2e_fees account_init', () => { expect(aliceBalanceAfter).toBe(aliceBalanceBefore - tx.transactionFee!); // bob can now use his wallet for sending txs - const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); const bobPaymentMethod = new PrivateFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); await bananaCoin.methods diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index fca622ac6bff..d54fc061b941 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -23,7 +23,7 @@ import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { getContract } from 'viem'; -import { MNEMONIC } from '../fixtures/fixtures.js'; +import { E2E_DEFAULT_MIN_FEE_PADDING, MNEMONIC } from '../fixtures/fixtures.js'; import { type EndToEndContext, type SetupOptions, @@ -193,7 +193,9 @@ export class FeesTest { this.wallet = this.context.wallet; this.aztecNode = this.context.aztecNodeService; this.aztecNodeAdmin = this.context.aztecNodeService; - this.gasSettings = GasSettings.default({ maxFeesPerGas: (await this.aztecNode.getCurrentMinFees()).mul(2) }); + this.gasSettings = GasSettings.default({ + maxFeesPerGas: (await this.aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING), + }); this.cheatCodes = this.context.cheatCodes; this.accounts = deployedAccounts.map(a => a.address); this.accounts.forEach((a, i) => this.logger.verbose(`Account ${i} address: ${a}`)); diff --git a/yarn-project/end-to-end/src/fixtures/fixtures.ts b/yarn-project/end-to-end/src/fixtures/fixtures.ts index 6b2002c6ffcd..d7cab015e53b 100644 --- a/yarn-project/end-to-end/src/fixtures/fixtures.ts +++ b/yarn-project/end-to-end/src/fixtures/fixtures.ts @@ -1,5 +1,8 @@ export const METRICS_PORT = 4318; +/** Default fee multiplier applied to getCurrentMinFees in e2e tests to cover fee decay between blocks. */ +export const E2E_DEFAULT_MIN_FEE_PADDING = 15; + export const shouldCollectMetrics = () => { if (process.env.COLLECT_METRICS) { return METRICS_PORT; diff --git a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts index 308d84285c9a..10621320ed1a 100644 --- a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts +++ b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts @@ -20,6 +20,7 @@ import { jest } from '@jest/globals'; import { mkdir, writeFile } from 'fs/promises'; import { dirname } from 'path'; +import { E2E_DEFAULT_MIN_FEE_PADDING } from '../fixtures/fixtures.js'; import { getSponsoredFPCAddress, registerSponsoredFPC } from '../fixtures/utils.js'; import type { WorkerWallet } from '../test-wallet/worker_wallet.js'; import { type WorkerWalletWrapper, createWorkerWalletClient } from './setup_test_wallets.js'; @@ -447,9 +448,9 @@ describe('block capacity benchmark', () => { async function cloneTx(tx: Tx, aztecNode: AztecNode): Promise { const clonedTx = Tx.clone(tx, false); - // Fetch current minimum fees and apply 50% buffer for safety + // Fetch current minimum fees and apply 15x buffer to cover fee decay between blocks const currentFees = await aztecNode.getCurrentMinFees(); - const paddedFees = currentFees.mul(1.5); + const paddedFees = currentFees.mul(E2E_DEFAULT_MIN_FEE_PADDING); // Update gas settings with current fees (clonedTx.data.constants.txContext.gasSettings as any).maxFeesPerGas = paddedFees; diff --git a/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts b/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts index 48986bd06c53..c98d5b0275ae 100644 --- a/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts +++ b/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts @@ -24,6 +24,7 @@ import type { ChildProcess } from 'child_process'; import { mkdir, writeFile } from 'fs/promises'; import { dirname } from 'path'; +import { E2E_DEFAULT_MIN_FEE_PADDING } from '../fixtures/fixtures.js'; import { getSponsoredFPCAddress, registerSponsoredFPC } from '../fixtures/utils.js'; import { PrometheusClient } from '../quality_of_service/prometheus_client.js'; import type { WorkerWallet } from '../test-wallet/worker_wallet.js'; @@ -647,9 +648,9 @@ async function createTx( async function cloneTx(tx: Tx, aztecNode: AztecNode): Promise { const clonedTx = Tx.clone(tx, false); - // Fetch current minimum fees and apply 50% buffer for safety + // Fetch current minimum fees and apply 15x buffer to cover fee decay between blocks const currentFees = await aztecNode.getCurrentMinFees(); - const paddedFees = currentFees.mul(1.5); + const paddedFees = currentFees.mul(E2E_DEFAULT_MIN_FEE_PADDING); // Update gas settings with current fees (clonedTx.data.constants.txContext.gasSettings as any).maxFeesPerGas = paddedFees; diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index 6cfff77ecd72..5834ad62738f 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -7,7 +7,7 @@ import { AztecLMDBStoreV2, createStore } from '@aztec/kv-store/lmdb-v2'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ChainConfig } from '@aztec/stdlib/config'; import type { ContractDataSource } from '@aztec/stdlib/contract'; -import { GasFees } from '@aztec/stdlib/gas'; +import type { BlockMinFeesProvider } from '@aztec/stdlib/gas'; import type { AztecNode, ClientProtocolCircuitVerifier, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; @@ -48,6 +48,7 @@ export async function createP2PClient( proofVerifier: ClientProtocolCircuitVerifier, worldStateSynchronizer: WorldStateSynchronizer, epochCache: EpochCacheInterface, + blockMinFeesProvider: BlockMinFeesProvider, packageVersion: string, dateProvider: DateProvider = new DateProvider(), telemetry: TelemetryClient = getTelemetryClient(), @@ -89,8 +90,7 @@ export async function createP2PClient( const currentBlockNumber = await archiver.getBlockNumber(); const { ts: nextSlotTimestamp } = epochCache.getEpochAndSlotInNextL1Slot(); const l1Constants = await archiver.getL1Constants(); - const header = await archiver.getBlockHeader(BlockNumber(currentBlockNumber)); - const gasFees = header?.globalVariables.gasFees ?? GasFees.empty(); + const gasFees = await blockMinFeesProvider.getCurrentMinFees(); return createTxValidatorForTransactionsEnteringPendingTxPool( worldStateSynchronizer, nextSlotTimestamp, @@ -103,6 +103,7 @@ export async function createP2PClient( gasFees, ); }, + blockMinFeesProvider, }, telemetry, { diff --git a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts index a76672a1e1de..89136643fb10 100644 --- a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +++ b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts @@ -6,6 +6,7 @@ import { DateProvider, Timer, executeTimeout } from '@aztec/foundation/timer'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; +import { GasFees } from '@aztec/stdlib/gas'; import type { ClientProtocolCircuitVerifier } from '@aztec/stdlib/interfaces/server'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; import { PeerErrorSeverity } from '@aztec/stdlib/p2p'; @@ -119,6 +120,7 @@ async function startClient(config: P2PConfig, clientIndex: number) { proofVerifier as ClientProtocolCircuitVerifier, worldState, epochCache, + { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, 'proposal-tx-collector-bench-worker', new DateProvider(), telemetry as TelemetryClient, diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts index 26e44f463e5e..3f083c0c42b6 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts @@ -14,6 +14,8 @@ describe('InsufficientFeePerGasEvictionRule', () => { let rule: InsufficientFeePerGasEvictionRule; let deleteTxsMock: jest.MockedFunction; + const blockGasFees = new GasFees(10, 20); + const createPoolOps = (pendingTxs: TxMetaData[]): PoolOperations => { deleteTxsMock = jest.fn(() => Promise.resolve()); return { @@ -28,7 +30,7 @@ describe('InsufficientFeePerGasEvictionRule', () => { beforeEach(() => { pool = createPoolOps([]); - rule = new InsufficientFeePerGasEvictionRule(); + rule = new InsufficientFeePerGasEvictionRule({ getCurrentMinFees: () => Promise.resolve(blockGasFees) }); }); describe('non-BLOCK_MINED events', () => { @@ -153,5 +155,31 @@ describe('InsufficientFeePerGasEvictionRule', () => { }); expect(deleteTxsMock).not.toHaveBeenCalled(); }); + + it('uses blockMinFeesProvider to determine eviction threshold', async () => { + // blockMinFeesProvider returns lower projected fees (5, 10) than block header (10, 20) + const getCurrentMinFees = jest.fn(() => Promise.resolve(new GasFees(5, 10))); + rule = new InsufficientFeePerGasEvictionRule({ getCurrentMinFees }); + + const tx1 = stubTxMetaData('0x1111', { maxFeesPerGas: new GasFees(5, 10) }); // Sufficient for projected fees + const tx2 = stubTxMetaData('0x2222', { maxFeesPerGas: new GasFees(4, 10) }); // DA too low for projected fees + + pool = createPoolOps([tx1, tx2]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(getCurrentMinFees).toHaveBeenCalled(); + expect(result.success).toBe(true); + // Only tx2 is evicted (DA fee 4 < projected 5), tx1 is kept despite block header fees being higher + expect(result.txsEvicted).toEqual([tx2.txHash]); + expect(deleteTxsMock).toHaveBeenCalledWith([tx2.txHash], 'InsufficientFeePerGas'); + }); }); }); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts index 2ab3a136c9b5..b096cf568a31 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts @@ -1,11 +1,13 @@ import { createLogger } from '@aztec/foundation/log'; +import type { BlockMinFeesProvider } from '@aztec/stdlib/gas'; import type { EvictionContext, EvictionResult, EvictionRule, PoolOperations } from './interfaces.js'; import { EvictionEvent } from './interfaces.js'; /** * Eviction rule that removes transactions whose maxFeesPerGas no longer meets - * the current block's gas fees after a new block is mined. + * the projected minimum gas fees after a new block is mined. + * Uses the BlockMinFeesProvider (forward-looking) to get the projected minimum fees. * Only triggers on BLOCK_MINED events. */ export class InsufficientFeePerGasEvictionRule implements EvictionRule { @@ -13,6 +15,8 @@ export class InsufficientFeePerGasEvictionRule implements EvictionRule { private log = createLogger('p2p:tx_pool_v2:insufficient_fee_per_gas_eviction_rule'); + constructor(private blockMinFeesProvider: BlockMinFeesProvider) {} + async evict(context: EvictionContext, pool: PoolOperations): Promise { if (context.event !== EvictionEvent.BLOCK_MINED) { return { @@ -23,7 +27,7 @@ export class InsufficientFeePerGasEvictionRule implements EvictionRule { } try { - const { gasFees } = context.block.globalVariables; + const gasFees = await this.blockMinFeesProvider.getCurrentMinFees(); const txsToEvict: string[] = []; const pendingTxs = pool.getPendingTxs(); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts index a78a70482024..307ccad18ef6 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts @@ -1,6 +1,7 @@ import type { SlotNumber } from '@aztec/foundation/branded-types'; import type { TypedEventEmitter } from '@aztec/foundation/types'; import type { L2Block, L2BlockId, L2BlockSource } from '@aztec/stdlib/block'; +import type { BlockMinFeesProvider } from '@aztec/stdlib/gas'; import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import type { BlockHeader, Tx, TxHash, TxValidator } from '@aztec/stdlib/tx'; @@ -72,6 +73,8 @@ export type TxPoolV2Dependencies = { worldStateSynchronizer: WorldStateSynchronizer; /** Factory that creates a validator for re-validating pool transactions using metadata */ createTxValidator: () => Promise>; + /** Provides projected minimum fees for the next block. Used by eviction rules instead of stale block header fees. */ + blockMinFeesProvider: BlockMinFeesProvider; }; /** diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts index 7f0e52312729..ad9bb40fe95b 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts @@ -87,6 +87,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool.start(); }); @@ -325,6 +326,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { archivedTxLimit: 2 }, @@ -366,6 +368,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 3 }, @@ -422,6 +425,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 10 }, @@ -465,6 +469,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 10 }, @@ -637,6 +642,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 0 }, 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 4cfa8672d574..1e41958968a2 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 @@ -133,6 +133,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool.start(); @@ -540,6 +541,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(rejectingValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await rejectingPool.start(); }); @@ -655,6 +657,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(new GasLimitsValidator()), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await gasPool.start(); }); @@ -1290,6 +1293,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await poolWithValidator.start(); }); @@ -1998,6 +2002,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await poolWithValidator.start(); }); @@ -2179,6 +2184,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await poolWithValidator.start(); }); @@ -4391,6 +4397,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4416,6 +4423,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -4444,6 +4452,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 100 }, @@ -4470,6 +4479,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 3 }, @@ -4502,6 +4512,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4519,6 +4530,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -4548,6 +4560,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4582,6 +4595,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 0 }, // No pending txs allowed @@ -4610,6 +4624,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4636,6 +4651,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(selectiveValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -4661,6 +4677,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4686,6 +4703,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -4880,6 +4898,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { minTxPoolAgeMs: 2_000 }, @@ -4978,6 +4997,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, { minTxPoolAgeMs: 2_000 }, @@ -5214,6 +5234,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await poolWithValidator.start(); @@ -5280,6 +5301,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -5314,6 +5336,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -5348,6 +5371,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(throwingValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -5373,6 +5397,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -5402,6 +5427,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(new MaxFeePerGasValidator(blockGasFees)), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await feePool.start(); }); @@ -5470,10 +5496,26 @@ describe('TxPoolV2', () => { }); describe('max fee per gas eviction after block mined', () => { - // The default pool already has InsufficientFeePerGasEvictionRule registered. - // Default mockTx uses maxFeesPerGas = GasFees(10, 10). - // The existing slot headers (slot1Header, slot2Header) use GasFees.empty() (0, 0), - // so we need a header with non-zero gas fees to trigger eviction. + // The eviction rule uses getCurrentMinFees to determine the fee threshold. + // We use a mutable variable so each test can set the projected min fees. + let currentMinFees = GasFees.empty(); + + beforeEach(async () => { + // Re-create the pool with a getCurrentMinFees that returns the test-controlled value + await pool.stop(); + await store.delete(); + await archiveStore.delete(); + store = await openTmpStore('p2p'); + archiveStore = await openTmpStore('archive'); + currentMinFees = GasFees.empty(); + pool = new AztecKVTxPoolV2(store, archiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(currentMinFees) }, + }); + await pool.start(); + }); const makeTxWithMaxFees = async (seed: number, maxFeesPerGas: GasFees) => { const tx = await mockTx(seed, { @@ -5505,7 +5547,8 @@ describe('TxPoolV2', () => { await pool.addPendingTxs([tx1, tx2]); expect(await pool.getPendingTxCount()).toBe(2); - // Mine a block with gas fees (20, 20) - higher than txs' maxFeesPerGas + // Set projected min fees higher than txs' maxFeesPerGas + currentMinFees = new GasFees(20, 20); const blockHeader = headerWithGasFees(new GasFees(20, 20)); await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); @@ -5523,7 +5566,8 @@ describe('TxPoolV2', () => { await pool.addPendingTxs([tx1, tx2]); expect(await pool.getPendingTxCount()).toBe(2); - // Mine a block with gas fees (20, 20) - lower than txs' maxFeesPerGas + // Set projected min fees lower than txs' maxFeesPerGas + currentMinFees = new GasFees(20, 20); const blockHeader = headerWithGasFees(new GasFees(20, 20)); await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); @@ -5541,7 +5585,8 @@ describe('TxPoolV2', () => { await pool.addPendingTxs([txLowFee, txHighFee, txBorderline]); expect(await pool.getPendingTxCount()).toBe(3); - // Mine a block with gas fees (20, 20) + // Set projected min fees to (20, 20) + currentMinFees = new GasFees(20, 20); const blockHeader = headerWithGasFees(new GasFees(20, 20)); await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); @@ -5560,6 +5605,7 @@ describe('TxPoolV2', () => { await pool.addPendingTxs([tx]); expect(await pool.getPendingTxCount()).toBe(1); + currentMinFees = new GasFees(20, 20); const blockHeader = headerWithGasFees(new GasFees(20, 20)); await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); @@ -5572,6 +5618,7 @@ describe('TxPoolV2', () => { await pool.addPendingTxs([tx]); expect(await pool.getPendingTxCount()).toBe(1); + currentMinFees = new GasFees(20, 20); const blockHeader = headerWithGasFees(new GasFees(20, 20)); await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); @@ -5597,7 +5644,8 @@ describe('TxPoolV2', () => { await pool.addProtectedTxs([tx], slot1Header); expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); - // Mine a block with gas fees higher than the tx's maxFeesPerGas + // Set projected min fees higher than the tx's maxFeesPerGas + currentMinFees = new GasFees(20, 20); const blockHeader = headerWithGasFees(new GasFees(20, 20)); await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts index 8e196bcf3cfc..e880bd705633 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts @@ -141,6 +141,7 @@ describe('TxPoolV2: benchmarks', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool.start(); const cleanup = async () => { @@ -495,6 +496,7 @@ describe('TxPoolV2: benchmarks', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -512,6 +514,7 @@ describe('TxPoolV2: benchmarks', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); const startTime = performance.now(); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts index bcbc443f9b79..69d42641dd93 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts @@ -115,7 +115,7 @@ export class TxPoolV2Impl { // Post-event eviction rules (run after events to check ALL pending txs) this.#evictionManager.registerRule(new InvalidTxsAfterMiningRule()); - this.#evictionManager.registerRule(new InsufficientFeePerGasEvictionRule()); + this.#evictionManager.registerRule(new InsufficientFeePerGasEvictionRule(deps.blockMinFeesProvider)); this.#evictionManager.registerRule(new InvalidTxsAfterReorgRule(deps.worldStateSynchronizer)); this.#evictionManager.registerRule(new FeePayerBalanceEvictionRule(deps.worldStateSynchronizer)); // LowPriorityEvictionRule handles cases where txs become pending via prepareForSlot (unprotect) diff --git a/yarn-project/p2p/src/test-helpers/make-test-p2p-clients.ts b/yarn-project/p2p/src/test-helpers/make-test-p2p-clients.ts index 3ba6375f5d3c..bdfe07c00df2 100644 --- a/yarn-project/p2p/src/test-helpers/make-test-p2p-clients.ts +++ b/yarn-project/p2p/src/test-helpers/make-test-p2p-clients.ts @@ -5,6 +5,7 @@ import { type Logger, createLogger } from '@aztec/foundation/log'; import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { GasFees } from '@aztec/stdlib/gas'; import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; @@ -102,6 +103,7 @@ export async function makeTestP2PClient( proofVerifier, mockWorldState, mockEpochCache, + { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, 'test-p2p-client', undefined, undefined, 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 32c46967fc65..5d1a52ba8f43 100644 --- a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts +++ b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts @@ -17,6 +17,7 @@ import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import { protocolContractsHash } from '@aztec/protocol-contracts'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; +import { GasFees } from '@aztec/stdlib/gas'; import type { ClientProtocolCircuitVerifier, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; import { type BlockProposal, P2PMessage } from '@aztec/stdlib/p2p'; @@ -369,6 +370,7 @@ process.on('message', async msg => { proofVerifier as ClientProtocolCircuitVerifier, worldState, epochCache, + { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, 'test-p2p-bench-worker', undefined, telemetry as TelemetryClient, diff --git a/yarn-project/stdlib/src/gas/gas_fees.ts b/yarn-project/stdlib/src/gas/gas_fees.ts index 7387b2df0496..6d3ee5112168 100644 --- a/yarn-project/stdlib/src/gas/gas_fees.ts +++ b/yarn-project/stdlib/src/gas/gas_fees.ts @@ -126,3 +126,8 @@ export class GasFees { return `GasFees { feePerDaGas=${this.feePerDaGas} feePerL2Gas=${this.feePerL2Gas} }`; } } + +/** Provides projected minimum gas fees for the next block. */ +export interface BlockMinFeesProvider { + getCurrentMinFees(): Promise; +} diff --git a/yarn-project/stdlib/src/tx/global_variable_builder.ts b/yarn-project/stdlib/src/tx/global_variable_builder.ts index 7cc64ab7bf18..b2460516a182 100644 --- a/yarn-project/stdlib/src/tx/global_variable_builder.ts +++ b/yarn-project/stdlib/src/tx/global_variable_builder.ts @@ -2,16 +2,14 @@ import type { EthAddress } from '@aztec/foundation/eth-address'; import type { SlotNumber } from '@aztec/foundation/schemas'; import type { AztecAddress } from '../aztec-address/index.js'; -import type { GasFees } from '../gas/gas_fees.js'; +import type { BlockMinFeesProvider } from '../gas/gas_fees.js'; import type { UInt32 } from '../types/index.js'; import type { CheckpointGlobalVariables, GlobalVariables } from './global_variables.js'; /** * Interface for building global variables for Aztec blocks. */ -export interface GlobalVariableBuilder { - getCurrentMinFees(): Promise; - +export interface GlobalVariableBuilder extends BlockMinFeesProvider { /** * Builds global variables for a given block. * @param blockNumber - The block number to build global variables for. diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index a65c4d938b33..e424235adc3d 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -1902,6 +1902,7 @@ __metadata: "@aztec/foundation": "workspace:^" "@aztec/kv-store": "workspace:^" "@aztec/l1-artifacts": "workspace:^" + "@aztec/native": "workspace:^" "@aztec/node-keystore": "workspace:^" "@aztec/node-lib": "workspace:^" "@aztec/noir-protocol-circuits-types": "workspace:^" From b13804b9950e4645bd5b1931171e03a2f45bd13e Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 12 Mar 2026 14:48:42 +0000 Subject: [PATCH 5/5] fix gossip_network_no_cheat test --- .../end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts index 40abce5a9573..54e467eb0ff2 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts @@ -203,6 +203,13 @@ describe('e2e_p2p_network', () => { // blocks without them (since targetCommitteeSize is set to the number of nodes) await t.setupAccount(); + // Wait for the next L1 block so that all nodes' getCurrentMinFees() caches are + // refreshed after the first L2 checkpoint is published. Without this, some wallets + // may estimate fees based on pre-checkpoint values (very low due to fee decay), + // while receiving nodes already see the post-checkpoint fees (much higher). + const ethereumSlotDuration = t.ctx.aztecNodeConfig.ethereumSlotDuration ?? 4; + await sleep((ethereumSlotDuration + 1) * 1000); + t.logger.info('Submitting transactions'); for (const node of nodes) { const txs = await submitTransactions(t.logger, node, NUM_TXS_PER_NODE, t.fundedAccount);