From 65a5883e81ef75a832416db9a127972826cbdbf7 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Thu, 13 Aug 2026 17:07:09 -0400 Subject: [PATCH 01/10] fix(p2p): move gas-limit admission out of GasTxValidator GasTxValidator re-ran the gas-limit check that the RPC factory's isSimulation exemption is meant to skip, so simulations with fee enforcement were rejected with TX_ERROR_GAS_LIMIT_TOO_HIGH. GasLimitsValidator is now the sole owner of declared gas-limit admission; gossip stage 1 and block building include it explicitly. Fixes #25167 --- .../src/msg_validators/tx_validator/README.md | 16 +- .../tx_validator/factory.test.ts | 92 ++++- .../msg_validators/tx_validator/factory.ts | 17 +- .../tx_validator/gas_validator.test.ts | 386 +++++++++--------- .../tx_validator/gas_validator.ts | 30 +- .../public_tx_simulator.ts | 2 +- yarn-project/stdlib/src/gas/gas_settings.ts | 6 +- .../wallet-sdk/src/base-wallet/base_wallet.ts | 4 +- 8 files changed, 323 insertions(+), 230 deletions(-) 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 80365f8e5b7d..dd8c949a7c95 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/README.md +++ b/yarn-project/p2p/src/msg_validators/tx_validator/README.md @@ -19,7 +19,7 @@ Unsolicited transactions from any peer. Fully validated in two stages with a poo | Step | What runs | On failure | |------|-----------|------------| -| **Stage 1** (fast) | TxPermitted, Data, Metadata, Timestamp, DoubleSpend, Gas, Phases, BlockHeader | Penalize peer, reject tx | +| **Stage 1** (fast) | TxPermitted, Data, Metadata, Timestamp, DoubleSpend, GasLimits, Gas, Phases, BlockHeader | Penalize peer, reject tx | | **Pool pre-check** | `canAddPendingTx` — checks for duplicates, pool capacity | Ignore tx (no penalty) | | **Stage 2** (slow) | Proof verification | Penalize peer, reject tx | | **Pool add** | `addPendingTxs` | Accept, ignore, or reject | @@ -34,6 +34,7 @@ Each stage-1 and stage-2 validator is paired with a `PeerErrorSeverity`. If a va Unsolicited transactions from a local wallet/PXE. Runs the full set of checks as a single aggregate validator: - TxPermitted, Size, Data, Metadata, Timestamp, DoubleSpend, Phases, BlockHeader +- GasLimits (skipped for simulations — gas estimation submits limits above the per-tx maximum) - Gas (optional — skipped when `skipFeeEnforcement` is set) - Proof verification (optional — skipped for simulations when no verifier is provided) @@ -56,7 +57,7 @@ State-dependent checks are deferred to either the block building validator (for Transactions already in the pool, about to be sequenced into a block. Re-validates against the current state of the block being built. **This is where invalid txs that entered via req/resp or block proposals are caught** — their invalidity is reported as part of block validation/attestation. Runs: -- Timestamp, DoubleSpend, Phases, Gas, BlockHeader +- Timestamp, DoubleSpend, Phases, GasLimits, Gas, BlockHeader Does **not** run: - Proof, Data — already verified on entry (by gossip, RPC, or req/resp validators) @@ -91,8 +92,8 @@ The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaD | `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 (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 | +| `GasTxValidator` | 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 (optionally clamped further by network admission limits). Sole owner of declared gas-limit admission | 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 | | `AllowedSetupCallsMetaValidator` | Checks the precomputed `allowedSetupCalls` flag on `TxMetaData`. Used in pool migration instead of the full `PhasesTxValidator` | — | @@ -109,16 +110,17 @@ The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaD | Metadata | Stage 1 | Yes | Yes | — | — | | Timestamp | Stage 1 | Yes | — | Yes | Yes | | DoubleSpend | Stage 1 | Yes | — | Yes | Yes | -| Gas (balance + limits) | Stage 1 | Optional* | — | Yes | — | -| GasLimits (standalone) | — | — | — | — | Yes | +| Gas (fee balance) | Stage 1 | Optional* | — | Yes | — | +| GasLimits | Stage 1 | Yes*** | — | Yes | Yes | | MaxFeePerGas (standalone) | — | — | — | — | Yes | | Phases | Stage 1 | Yes | — | Yes | — | | AllowedSetupCalls | — | — | — | — | 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` 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. +\* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `MaxFeePerGasValidator` as its first step, so fee-per-gas is checked wherever `GasTxValidator` runs. Pool migration uses `MaxFeePerGasValidator` standalone because it doesn't need the balance check. Declared gas-limit admission is owned solely by `GasLimitsValidator`. \** Proof verification is skipped for simulations (no verifier provided). +\*** Skipped for simulations: gas estimation submits limits above the per-tx maximum, and the wallet clamps the real tx to the admission limit afterward. The gas-limit bounds `GasLimitsValidator` enforces here — the per-tx protocol maxima and the network admission limits — are documented in [`stdlib/src/gas/README.md`](../../../../stdlib/src/gas/README.md) under "Gas and Data Limits". 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 3828826a0443..baba673a49c7 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 @@ -1,14 +1,16 @@ +import { MAX_TX_DA_GAS } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { ContractDataSource } from '@aztec/stdlib/contract'; -import { GasFees } from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import type { ClientProtocolCircuitVerifier, MerkleTreeReadOperations, WorldStateSynchronizer, } from '@aztec/stdlib/interfaces/server'; import { PeerErrorSeverity } from '@aztec/stdlib/p2p'; -import type { GlobalVariables } from '@aztec/stdlib/tx'; +import { mockTx } from '@aztec/stdlib/testing'; +import { type GlobalVariables, TX_ERROR_GAS_LIMIT_TOO_HIGH } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -39,6 +41,17 @@ function getValidatorNames(aggregate: AggregateTxValidator): string[] { return aggregate.validators.map(v => v.constructor.name); } +/** A tx with no public calls, carrying the given gas settings. */ +async function mockPrivateTxWithGasSettings(gasSettings: GasSettings) { + const tx = await mockTx(1, { + numberOfNonRevertiblePublicCallRequests: 0, + numberOfRevertiblePublicCallRequests: 0, + hasPublicTeardownCallRequest: false, + }); + tx.data.constants.txContext.gasSettings = gasSettings; + return tx; +} + describe('Validator factory functions', () => { let synchronizer: MockProxy; let contractSource: MockProxy; @@ -72,12 +85,39 @@ describe('Validator factory functions', () => { 'phasesValidator', 'blockHeaderValidator', 'doubleSpendValidator', + 'gasLimitsValidator', 'gasValidator', 'dataValidator', 'contractInstanceValidator', ]); }); + it('forwards the network admission limits to the gas limits validator', async () => { + const maxTxL2Gas = 1_000_000; + const validators = createFirstStageTxValidationsForGossipedTransactions( + 0n, + BlockNumber(2), + synchronizer, + new GasFees(1, 1), + 1, + 2, + Fr.ZERO, + contractSource, + true, + [], + undefined, + { maxTxL2Gas }, + ); + + // Over the network admission limit but under the protocol ceiling, so only forwarded opts can reject it. + const tx = await mockPrivateTxWithGasSettings( + GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), maxFeesPerGas: new GasFees(1, 1) }), + ); + const result = await validators.gasLimitsValidator.validator.validateTx(tx); + expect(result.result).toBe('invalid'); + expect((result as { reason: string[] }).reason[0]).toContain(TX_ERROR_GAS_LIMIT_TOO_HIGH); + }); + it('does not include a proof validator', () => { const validators = createFirstStageTxValidationsForGossipedTransactions( 0n, @@ -116,6 +156,7 @@ describe('Validator factory functions', () => { expect(validators.dataValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); expect(validators.metadataValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); expect(validators.doubleSpendValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); + expect(validators.gasLimitsValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); expect(validators.gasValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); expect(validators.phasesValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); }); @@ -270,6 +311,39 @@ describe('Validator factory functions', () => { expect(getValidatorNames(aggregate)).not.toContain(GasLimitsValidator.name); }); + describe('gas-limit admission for estimation gas settings', () => { + // Estimation limits exceed the per-tx protocol maximum by construction, so whether the tx passes admission + // is decided solely by the isSimulation exemption. The aggregate collects reasons from every validator, so + // asserting on the specific error is robust to other validators failing on the mocked db. + const validateEstimationTx = async (isSimulation: boolean) => { + db.findLeafIndices.mockResolvedValue([]); + const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { + l1ChainId: 1, + rollupVersion: 2, + setupAllowList: [], + gasFees: new GasFees(1, 1), + skipFeeEnforcement: false, + isSimulation, + timestamp: 100n, + blockNumber: BlockNumber(5), + txsPermitted: true, + }); + const tx = await mockPrivateTxWithGasSettings(GasSettings.forEstimation({ maxFeesPerGas: new GasFees(1, 1) })); + const result = await validator.validateTx(tx); + return result.result === 'invalid' ? result.reason : []; + }; + + it('rejects estimation gas limits when not simulating', async () => { + const reasons = await validateEstimationTx(false); + expect(reasons.some(r => r.includes(TX_ERROR_GAS_LIMIT_TOO_HIGH))).toBe(true); + }); + + it('accepts estimation gas limits during simulation even with fee enforcement on', async () => { + const reasons = await validateEstimationTx(true); + expect(reasons.some(r => r.includes(TX_ERROR_GAS_LIMIT_TOO_HIGH))).toBe(false); + }); + }); + it('excludes proof validator when no verifier is provided', () => { const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { l1ChainId: 1, @@ -309,10 +383,24 @@ describe('Validator factory functions', () => { PhasesTxValidator.name, BlockHeaderTxValidator.name, DoubleSpendTxValidator.name, + GasLimitsValidator.name, GasTxValidator.name, ]); }); + it('rejects declared gas limits above the protocol ceiling', async () => { + // Block proposal txs get only well-formedness checks on receipt; this is where an over-declared limit must + // be caught, or execution would trip the simulator's MAX_PROCESSABLE_L2_GAS assertion. + db.findLeafIndices.mockResolvedValue([]); + const result = createTxValidatorForBlockBuilding(db, contractSource, globalVariables, []); + + const tx = await mockPrivateTxWithGasSettings(GasSettings.forEstimation({ maxFeesPerGas: new GasFees(1, 1) })); + const validationResult = await result.preprocessValidator!.validateTx(tx); + expect(validationResult.result).toBe('invalid'); + const reasons = (validationResult as { reason: string[] }).reason; + expect(reasons.some(r => r.includes(TX_ERROR_GAS_LIMIT_TOO_HIGH))).toBe(true); + }); + it('returns a nullifierCache alongside the preprocessValidator', () => { const result = createTxValidatorForBlockBuilding(db, contractSource, globalVariables, []); 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 c434da61a76c..7fb70668afdb 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -156,13 +156,18 @@ export function createFirstStageTxValidationsForGossipedTransactions( ), severity: PeerErrorSeverity.MidToleranceError, // This is handled specifically at the point of rejection by considering a recent window where it may have been valid }, + // Must stay after doubleSpendValidator: on equal severities the first failing entry is reported, and + // handleGossipedTx special-cases the doubleSpendValidator name to grade severity by nullifier recency. + gasLimitsValidator: { + validator: new GasLimitsValidator({ ...gasLimitOpts, bindings }), + severity: PeerErrorSeverity.MidToleranceError, + }, gasValidator: { validator: new GasTxValidator( new DatabasePublicStateSource(merkleTree), ProtocolContractAddress.FeeJuice, gasFees, bindings, - gasLimitOpts, ), severity: PeerErrorSeverity.MidToleranceError, }, @@ -342,9 +347,8 @@ export function createTxValidatorForAcceptingTxsOverRPC( // Declared gas-limit admission is not fee enforcement, so it runs even when fees are skipped, but it is // skipped during simulation: gas estimation submits intentionally-inflated `forEstimation` limits (above // the per-tx max) and the wallet clamps the real tx to the admission limit afterward, so enforcing the - // limit on the estimation tx would reject a valid estimation. The fee-balance check below stays behind - // `skipFeeEnforcement`, and GasTxValidator is constructed without the limit opts so it does not re-run - // this same check. + // limit on the estimation tx would reject a valid estimation. GasLimitsValidator is the sole owner of the + // limit check (GasTxValidator below performs none), so this guard fully expresses the exemption. if (!isSimulation) { validators.push(new GasLimitsValidator({ maxTxL2Gas, maxTxDAGas, bindings })); } @@ -416,6 +420,11 @@ function createTxValidatorForValidatingAgainstCurrentState( new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp, bindings), new BlockHeaderTxValidator(archiveSource, bindings), new DoubleSpendTxValidator(nullifierSource, bindings), + // No limit opts: block building enforces only the per-tx protocol ceiling. Network admission limits are + // relay policy; per-block capacity is enforced while packing, and applying them here could reject an + // otherwise valid proposed block. This is where over-declared limits on txs that entered via req/resp or + // block proposals are caught, before execution would trip the AVM's max-processable-gas assertion. + new GasLimitsValidator({ bindings }), new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees, 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 3126ddd53d78..efdf476b1ef8 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 @@ -31,6 +31,21 @@ import { patchNonRevertibleFn, patchRevertibleFn } from './test_utils.js'; const DEFAULT_GAS_LIMITS = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); const TEARDOWN_DA_GAS = 98_304; +/** A tx with no public calls, carrying the default gas limits. */ +const makePrivateTx = async (gasFees: GasFees) => { + const privateTx = await mockTx(1, { + numberOfNonRevertiblePublicCallRequests: 0, + numberOfRevertiblePublicCallRequests: 0, + hasPublicTeardownCallRequest: false, + }); + assert(!privateTx.data.forPublic); + privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, + maxFeesPerGas: gasFees.clone(), + }); + return privateTx; +}; + describe('GasTxValidator', () => { // Vars for validator. let publicStateSource: MockProxy; @@ -116,249 +131,234 @@ describe('GasTxValidator', () => { await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PAYER_BALANCE); }); - const makePrivateTx = async () => { - const privateTx = await mockTx(1, { - numberOfNonRevertiblePublicCallRequests: 0, - numberOfRevertiblePublicCallRequests: 0, - hasPublicTeardownCallRequest: false, + it('does not enforce gas-limit admission, which is owned by GasLimitsValidator', async () => { + // Gas estimation submits limits above the per-tx protocol maximum (GasSettings.forEstimation); whether they + // are admissible is decided by GasLimitsValidator wherever a factory includes it, never by fee enforcement. + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS * 2), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); - assert(!privateTx.data.forPublic); - privateTx.data.feePayer = payer; - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + mockBalance(tx.data.constants.txContext.gasSettings.getFeeLimit().toBigInt()); + await expectValid(tx); + }); + + it('rejects txs with not enough fee per da gas', async () => { + gasFees.feePerDaGas = gasFees.feePerDaGas + 1n; + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); + }); + + it('rejects txs with not enough fee per l2 gas', async () => { + gasFees.feePerL2Gas = gasFees.feePerL2Gas + 1n; + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); + }); +}); + +describe('GasLimitsValidator', () => { + let gasFees: GasFees; + let tx: Tx; + + beforeEach(async () => { + gasFees = new GasFees(11, 22); + tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: DEFAULT_GAS_LIMITS, maxFeesPerGas: gasFees.clone(), }); - return privateTx; + }); + + const expectValid = async (tx: Tx) => { + await expect(new GasLimitsValidator().validateTx(tx)).resolves.toEqual({ result: 'valid' }); }; - describe('gas limits', () => { - it('accepts public tx at exactly the minimum gas limits', async () => { - assert(!!tx.data.forPublic); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - }); - mockBalance(tx.data.constants.txContext.gasSettings.getFeeLimit().toBigInt()); - await expectValid(tx); + const expectInvalid = async (tx: Tx, reason: string) => { + const result = await new GasLimitsValidator().validateTx(tx); + expect(result.result).toEqual('invalid'); + expect((result as { reason: string[] }).reason[0]).toContain(reason); + }; + + it('accepts public tx at exactly the minimum gas limits', async () => { + assert(!!tx.data.forPublic); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), }); + await expectValid(tx); + }); - it('accepts private tx at exactly the minimum gas limits', async () => { - const privateTx = await makePrivateTx(); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - }); - mockBalance(privateTx.data.constants.txContext.gasSettings.getFeeLimit().toBigInt()); - await expectValid(privateTx); + it('accepts private tx at exactly the minimum gas limits', async () => { + const privateTx = await makePrivateTx(gasFees); + privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), }); + await expectValid(privateTx); + }); - it('rejects public tx below the public L2 gas minimum', async () => { - assert(!!tx.data.forPublic); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD - 1), - maxFeesPerGas: gasFees.clone(), - }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + it('rejects public tx below the public L2 gas minimum', async () => { + assert(!!tx.data.forPublic); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD - 1), + maxFeesPerGas: gasFees.clone(), }); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); - it('rejects private tx below the private L2 gas minimum', async () => { - const privateTx = await makePrivateTx(); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD - 1), - maxFeesPerGas: gasFees.clone(), - }); - await expectInvalid(privateTx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + it('rejects private tx below the private L2 gas minimum', async () => { + const privateTx = await makePrivateTx(gasFees); + privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD - 1), + maxFeesPerGas: gasFees.clone(), }); + await expectInvalid(privateTx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); - it('rejects public tx at private L2 gas minimum (between the two thresholds)', async () => { - assert(!!tx.data.forPublic); - // PRIVATE_TX_L2_GAS_OVERHEAD is enough for a private tx but not for a public tx. - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + it('rejects public tx at private L2 gas minimum (between the two thresholds)', async () => { + assert(!!tx.data.forPublic); + // PRIVATE_TX_L2_GAS_OVERHEAD is enough for a private tx but not for a public tx. + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), }); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); + + it('rejects tx below DA gas minimum', async () => { + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + }); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); + + it('rejects tx below both DA and L2 gas minimums', async () => { + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(1, 1), + maxFeesPerGas: gasFees.clone(), + }); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); + + it('rejects public tx if L2 gas limit is too high', async () => { + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expectInvalid(tx, TX_ERROR_GAS_LIMIT_TOO_HIGH); + }); - it('rejects tx below DA gas minimum', async () => { + it('rejects private tx if L2 gas limit is too high', async () => { + const privateTx = await makePrivateTx(gasFees); + privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expectInvalid(privateTx, TX_ERROR_GAS_LIMIT_TOO_HIGH); + }); + + describe('network admission limits (maxTxL2Gas, maxTxDAGas)', () => { + it('rejects tx exceeding maxTxL2Gas', async () => { + const maxTxL2Gas = 1_000_000; + const validator = new GasLimitsValidator({ maxTxL2Gas }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD), + gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); }); - it('rejects tx below both DA and L2 gas minimums', async () => { + it('accepts tx at exactly maxTxL2Gas', async () => { + const maxTxL2Gas = 1_000_000; + const validator = new GasLimitsValidator({ maxTxL2Gas }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(1, 1), + gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas), maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); }); - it('rejects public tx if L2 gas limit is too high', async () => { + it('clamps maxTxL2Gas to the per-tx protocol maximum', async () => { + // Passing a higher network limit cannot raise the ceiling above MAX_PROCESSABLE_L2_GAS. + const validator = new GasLimitsValidator({ maxTxL2Gas: MAX_PROCESSABLE_L2_GAS + 1_000 }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), maxFeesPerGas: gasFees.clone(), teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); - await expectInvalid(tx, TX_ERROR_GAS_LIMIT_TOO_HIGH); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + }); }); - it('rejects private tx if L2 gas limit is too high', async () => { - const privateTx = await makePrivateTx(); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + it('falls back to MAX_PROCESSABLE_L2_GAS when no L2 limit is set', async () => { + const validator = new GasLimitsValidator(); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), maxFeesPerGas: gasFees.clone(), teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); - await expectInvalid(privateTx, TX_ERROR_GAS_LIMIT_TOO_HIGH); - }); - - describe('network admission limits (maxTxL2Gas, maxTxDAGas)', () => { - it('rejects tx exceeding maxTxL2Gas', async () => { - const maxTxL2Gas = 1_000_000; - const validator = new GasLimitsValidator({ maxTxL2Gas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); - }); - - it('accepts tx at exactly maxTxL2Gas', async () => { - const maxTxL2Gas = 1_000_000; - const validator = new GasLimitsValidator({ maxTxL2Gas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); - }); - - it('clamps maxTxL2Gas to the per-tx protocol maximum', async () => { - // Passing a higher network limit cannot raise the ceiling above MAX_PROCESSABLE_L2_GAS. - const validator = new GasLimitsValidator({ maxTxL2Gas: MAX_PROCESSABLE_L2_GAS + 1_000 }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], }); + }); - it('falls back to MAX_PROCESSABLE_L2_GAS when no L2 limit is set', async () => { - const validator = new GasLimitsValidator(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); + it('rejects tx exceeding maxTxDAGas', async () => { + const maxTxDAGas = 100_000; + const validator = new GasLimitsValidator({ maxTxDAGas }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); - - it('rejects tx exceeding maxTxDAGas', async () => { - const maxTxDAGas = 100_000; - const validator = new GasLimitsValidator({ maxTxDAGas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], }); + }); - it('accepts tx at exactly maxTxDAGas', async () => { - const maxTxDAGas = 100_000; - const validator = new GasLimitsValidator({ maxTxDAGas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxTxDAGas, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + it('accepts tx at exactly maxTxDAGas', async () => { + const maxTxDAGas = 100_000; + const validator = new GasLimitsValidator({ maxTxDAGas }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(maxTxDAGas, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }); - it('caps DA at the max tx blob size when no DA limit is set', async () => { - const validator = new GasLimitsValidator(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS + 1, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); + it('caps DA at the max tx blob size when no DA limit is set', async () => { + const validator = new GasLimitsValidator(); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS + 1, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); - - it('accepts a tx at exactly the max tx blob size DA limit when no DA limit is set', async () => { - const validator = new GasLimitsValidator(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); - }); - - it('forwards L2 limits through GasTxValidator', async () => { - const maxTxL2Gas = 1_000_000; - const validator = new GasTxValidator(publicStateSource, feeJuiceAddress, gasFees, undefined, { - maxTxL2Gas, - }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], }); + }); - it('forwards DA limits through GasTxValidator', async () => { - const maxTxDAGas = 100_000; - const validator = new GasTxValidator(publicStateSource, feeJuiceAddress, gasFees, undefined, { - maxTxDAGas, - }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); + it('accepts a tx at exactly the max tx blob size DA limit when no DA limit is set', async () => { + const validator = new GasLimitsValidator(); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); }); }); - - it('rejects txs with not enough fee per da gas', async () => { - gasFees.feePerDaGas = gasFees.feePerDaGas + 1n; - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); - }); - - it('rejects txs with not enough fee per l2 gas', async () => { - gasFees.feePerL2Gas = gasFees.feePerL2Gas + 1n; - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); - }); }); describe('MaxFeePerGasValidator', () => { 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 f076bb0a481c..f08c58eee597 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 @@ -58,7 +58,9 @@ export interface HasMaxFeePerGasData { * 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}. + * Sole owner of declared gas-limit admission. Used via the factory by: gossip (stage 1), RPC + * (except simulation — gas estimation submits limits above the per-tx maximum), block building, + * and pending pool migration. */ export class GasLimitsValidator implements TxValidator { #log: Logger; @@ -86,7 +88,7 @@ export class GasLimitsValidator implements TxValidato } /** Checks gas limits are >= fixed minimums and <= effective max gas (L2 and DA). */ - validateGasLimit(tx: T): TxValidationResult { + private validateGasLimit(tx: T): TxValidationResult { const gasLimits = tx.data.constants.txContext.gasSettings.gasLimits; const minGasLimits = new Gas( TX_DA_GAS_OVERHEAD, @@ -181,15 +183,17 @@ export class MaxFeePerGasValidator implements TxV /** * 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** — 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, + * Runs two checks in order: + * 1. **Max fee per gas** (delegates to {@link MaxFeePerGasValidator}) — rejects the tx if its + * maxFeesPerGas is below the current block's gas fees. + * 2. **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). * + * Declared gas-limit admission is deliberately not checked here: it is owned by {@link GasLimitsValidator}, + * which factories include separately so that exemptions from it (e.g. gas estimation) + * can be expressed without changing fee enforcement. + * * Used by: gossip (stage 1), RPC, and block building validators. */ export class GasTxValidator implements TxValidator { @@ -197,30 +201,20 @@ export class GasTxValidator implements TxValidator { #publicDataSource: PublicStateSource; #feeJuiceAddress: AztecAddress; #gasFees: GasFees; - #gasLimitOpts?: { maxTxL2Gas?: number; maxTxDAGas?: number }; constructor( publicDataSource: PublicStateSource, feeJuiceAddress: AztecAddress, gasFees: GasFees, private bindings?: LoggerBindings, - opts?: { maxTxL2Gas?: number; maxTxDAGas?: number }, ) { this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings); this.#publicDataSource = publicDataSource; this.#feeJuiceAddress = feeJuiceAddress; this.#gasFees = gasFees; - this.#gasLimitOpts = opts; } async validateTx(tx: Tx): Promise { - const gasLimitValidation = new GasLimitsValidator({ - ...this.#gasLimitOpts, - bindings: this.bindings, - }).validateGasLimit(tx); - if (gasLimitValidation.result === 'invalid') { - return gasLimitValidation; - } const maxFeeValidation = new MaxFeePerGasValidator(this.#gasFees, this.bindings).validateMaxFeePerGas(tx); if (maxFeeValidation.result === 'invalid') { return maxFeeValidation; diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts index 85fe0129fd04..019ba38d164f 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts @@ -197,7 +197,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface { context.halt(); - // Such transactions should be filtered by GasTxValidator. + // Such transactions should be filtered by GasLimitsValidator. assert( context.getActualGasUsed().l2Gas <= MAX_PROCESSABLE_L2_GAS, `Transaction consumes ${context.getActualGasUsed().l2Gas} L2 gas, which exceeds the maximum processable gas of ${MAX_PROCESSABLE_L2_GAS}`, diff --git a/yarn-project/stdlib/src/gas/gas_settings.ts b/yarn-project/stdlib/src/gas/gas_settings.ts index 4672f16be52a..514d3c27f268 100644 --- a/yarn-project/stdlib/src/gas/gas_settings.ts +++ b/yarn-project/stdlib/src/gas/gas_settings.ts @@ -132,9 +132,9 @@ export class GasSettings { * the effective gas available for app logic is gasLimits - teardownGasLimits - privateOverhead. * To ensure estimation never hits gas caps, we set both limits above what the protocol allows: * teardown gets MAX_PROCESSABLE and gasLimits gets teardown + MAX_PROCESSABLE, so the full - * processable amount remains available for each phase independently. To be used in conjunction - * with skipTxValidation: true during public simulation, or the node would reject the transaction - * outright due to gas limits being above protocol max. + * processable amount remains available for each phase independently. Tx validation exempts + * simulated txs from gas-limit admission (`isValidTx` with `isSimulation: true`), so these + * inflated limits pass validation; the wallet clamps the real tx to the admission limit afterward. */ static forEstimation(overrides: { gasLimits?: Gas; diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index ccde76c5e0f0..068cfd13c69a 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -309,8 +309,8 @@ export abstract class BaseWallet implements Wallet { // (the node's per-tx admission limit), so the proposer does not skip the tx for over-declaring gas. let fullGasSettings; if (forEstimation) { - // Estimation deliberately uses very high internal limits and skips tx validation, so we do not - // validate against the network admission limit here. + // Estimation deliberately uses very high internal limits, which the node exempts from gas-limit + // admission during simulation, so we do not validate against the network admission limit here. fullGasSettings = GasSettings.forEstimation(gasSettingsOverrides); } else { const maxTxGasLimits = await this.getMaxTxGasLimits(); From 8cb7e2784e67629736cd4d48cf0b33ddf976f83e Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Thu, 13 Aug 2026 17:17:33 -0400 Subject: [PATCH 02/10] fix(p2p): keep validateGasLimit public, gas README wording, DA forwarding test --- .../tx_validator/factory.test.ts | 26 +++++++++++++++++++ .../tx_validator/gas_validator.ts | 2 +- yarn-project/stdlib/src/gas/README.md | 7 ++--- 3 files changed, 31 insertions(+), 4 deletions(-) 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 baba673a49c7..3ba060ce7b7d 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 @@ -118,6 +118,32 @@ describe('Validator factory functions', () => { expect((result as { reason: string[] }).reason[0]).toContain(TX_ERROR_GAS_LIMIT_TOO_HIGH); }); + it('forwards the network DA admission limit to the gas limits validator', async () => { + const maxTxDAGas = 100_000; + const validators = createFirstStageTxValidationsForGossipedTransactions( + 0n, + BlockNumber(2), + synchronizer, + new GasFees(1, 1), + 1, + 2, + Fr.ZERO, + contractSource, + true, + [], + undefined, + { maxTxDAGas }, + ); + + // Over the network DA admission limit but under the protocol DA ceiling. + const tx = await mockPrivateTxWithGasSettings( + GasSettings.fallback({ gasLimits: new Gas(maxTxDAGas + 1, 1_000_000), maxFeesPerGas: new GasFees(1, 1) }), + ); + const result = await validators.gasLimitsValidator.validator.validateTx(tx); + expect(result.result).toBe('invalid'); + expect((result as { reason: string[] }).reason[0]).toContain(TX_ERROR_GAS_LIMIT_TOO_HIGH); + }); + it('does not include a proof validator', () => { const validators = createFirstStageTxValidationsForGossipedTransactions( 0n, 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 f08c58eee597..845aacfffb48 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 @@ -88,7 +88,7 @@ export class GasLimitsValidator implements TxValidato } /** Checks gas limits are >= fixed minimums and <= effective max gas (L2 and DA). */ - private validateGasLimit(tx: T): TxValidationResult { + validateGasLimit(tx: T): TxValidationResult { const gasLimits = tx.data.constants.txContext.gasSettings.gasLimits; const minGasLimits = new Gas( TX_DA_GAS_OVERHEAD, diff --git a/yarn-project/stdlib/src/gas/README.md b/yarn-project/stdlib/src/gas/README.md index 1bce97106cdb..b98eef59ace6 100644 --- a/yarn-project/stdlib/src/gas/README.md +++ b/yarn-project/stdlib/src/gas/README.md @@ -226,7 +226,8 @@ must also be buildable into a block and fit a valid checkpoint. ### Per-tx protocol maxima Hard ceilings on what any single tx may declare, independent of network configuration. Declaring more is -rejected everywhere a tx is validated. +rejected by `GasLimitsValidator` at every tx validation entry point except simulation, which is exempt +because gas estimation deliberately declares limits above the per-tx maximum. - **`MAX_TX_DA_GAS`** (271,200) — `MAX_TX_BLOB_DATA_SIZE_IN_FIELDS` (8,475) × `DA_GAS_PER_FIELD` (32). This is the most DA a single tx's effects can encode into a blob, so it is the most DA gas a tx could ever use. @@ -300,8 +301,8 @@ The outermost limits, enforced as proposal validity in `validateCheckpointLimits | Limit | Value (mainnet defaults) | Scope | Where enforced | | --------------------------------------- | ------------------------------- | ------------- | ----------------------------------------------------------- | -| `MAX_TX_DA_GAS` | 271,200 | per-tx | every gas validator (hard ceiling) | -| `MAX_PROCESSABLE_L2_GAS` | 6,540,000 | per-tx | every gas validator (hard ceiling) | +| `MAX_TX_DA_GAS` | 271,200 | per-tx | `GasLimitsValidator` (hard ceiling; simulation exempt) | +| `MAX_PROCESSABLE_L2_GAS` | 6,540,000 | per-tx | `GasLimitsValidator` (hard ceiling; simulation exempt) | | Network DA admission limit | min(271,200, ceil(784,448/10×1.5)) = 117,668 | per-tx (relay) | RPC, gossip, pending pool (`GasLimitsValidator`) | | Network L2 admission limit | min(6,540,000, ceil(manaLimit/10×1.2)) | per-tx (relay) | RPC, gossip, pending pool (`GasLimitsValidator`) | | Per-block fair share + caps | remaining budget / blocks × multiplier, min absolute caps & blob-field cap | per-block | `CheckpointBuilder.capLimitsByCheckpointBudgets` | From 9129c85234373ebbe3e33599e162baaaeda18085 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Fri, 14 Aug 2026 11:29:31 -0400 Subject: [PATCH 03/10] refactor(p2p): trim validator comments, test flag matrix for RPC gas limits --- .../src/msg_validators/tx_validator/README.md | 4 +- .../tx_validator/factory.test.ts | 71 ++++++++++--------- .../msg_validators/tx_validator/factory.ts | 19 ++--- .../tx_validator/gas_validator.test.ts | 4 +- .../tx_validator/gas_validator.ts | 9 +-- 5 files changed, 51 insertions(+), 56 deletions(-) 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 dd8c949a7c95..d4562a462503 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/README.md +++ b/yarn-project/p2p/src/msg_validators/tx_validator/README.md @@ -93,7 +93,7 @@ The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaD | `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` | 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 (optionally clamped further by network admission limits). Sole owner of declared gas-limit admission | 3–10 us | +| `GasLimitsValidator` | Gas limits are >= fixed minimums and <= AVM max processable L2 gas (optionally clamped further by network admission limits). Sole owner of declared gas-limit validation | 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 | | `AllowedSetupCallsMetaValidator` | Checks the precomputed `allowedSetupCalls` flag on `TxMetaData`. Used in pool migration instead of the full `PhasesTxValidator` | — | @@ -118,7 +118,7 @@ The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaD | 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 `MaxFeePerGasValidator` as its first step, so fee-per-gas is checked wherever `GasTxValidator` runs. Pool migration uses `MaxFeePerGasValidator` standalone because it doesn't need the balance check. Declared gas-limit admission is owned solely by `GasLimitsValidator`. +\* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `MaxFeePerGasValidator` as its first step, so fee-per-gas is checked wherever `GasTxValidator` runs. Pool migration uses `MaxFeePerGasValidator` standalone because it doesn't need the balance check. Declared gas-limit validation is owned solely by `GasLimitsValidator`. \** Proof verification is skipped for simulations (no verifier provided). \*** Skipped for simulations: gas estimation submits limits above the per-tx maximum, and the wallet clamps the real tx to the admission limit afterward. 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 3ba060ce7b7d..1a63cd02ba07 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 @@ -312,14 +312,14 @@ describe('Validator factory functions', () => { const aggregate = validator as AggregateTxValidator; const names = getValidatorNames(aggregate); - // Declared gas-limit admission is not fee enforcement, so it stays even with fees skipped. + // Gas-limit validation is not fee enforcement, so it stays even with fees skipped. expect(names).toContain(GasLimitsValidator.name); expect(names).not.toContain(GasTxValidator.name); expect(names).toContain(TxProofValidator.name); }); - it('excludes the gas-limits admission validator during simulation', () => { - // Gas estimation submits intentionally-inflated forEstimation limits, so the admission limit must not + it('excludes the gas-limits validator during simulation', () => { + // Gas estimation submits intentionally-inflated forEstimation limits, so gas-limit validation must not // reject the estimation tx; the wallet clamps the real tx afterward. const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { l1ChainId: 1, @@ -337,37 +337,40 @@ describe('Validator factory functions', () => { expect(getValidatorNames(aggregate)).not.toContain(GasLimitsValidator.name); }); - describe('gas-limit admission for estimation gas settings', () => { - // Estimation limits exceed the per-tx protocol maximum by construction, so whether the tx passes admission - // is decided solely by the isSimulation exemption. The aggregate collects reasons from every validator, so - // asserting on the specific error is robust to other validators failing on the mocked db. - const validateEstimationTx = async (isSimulation: boolean) => { - db.findLeafIndices.mockResolvedValue([]); - const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { - l1ChainId: 1, - rollupVersion: 2, - setupAllowList: [], - gasFees: new GasFees(1, 1), - skipFeeEnforcement: false, - isSimulation, - timestamp: 100n, - blockNumber: BlockNumber(5), - txsPermitted: true, - }); - const tx = await mockPrivateTxWithGasSettings(GasSettings.forEstimation({ maxFeesPerGas: new GasFees(1, 1) })); - const result = await validator.validateTx(tx); - return result.result === 'invalid' ? result.reason : []; - }; - - it('rejects estimation gas limits when not simulating', async () => { - const reasons = await validateEstimationTx(false); - expect(reasons.some(r => r.includes(TX_ERROR_GAS_LIMIT_TOO_HIGH))).toBe(true); - }); - - it('accepts estimation gas limits during simulation even with fee enforcement on', async () => { - const reasons = await validateEstimationTx(true); - expect(reasons.some(r => r.includes(TX_ERROR_GAS_LIMIT_TOO_HIGH))).toBe(false); - }); + describe('gas-limit validation', () => { + // Estimation limits exceed the per-tx protocol maximum by construction, so whether the tx is rejected is + // decided solely by isSimulation; skipFeeEnforcement must not affect it. The aggregate collects reasons + // from every validator, so asserting on the specific error is robust to other validators failing on the + // mocked db. + it.each` + isSimulation | skipFeeEnforcement | rejected + ${false} | ${false} | ${true} + ${false} | ${true} | ${true} + ${true} | ${false} | ${false} + ${true} | ${true} | ${false} + `( + 'isSimulation=$isSimulation, skipFeeEnforcement=$skipFeeEnforcement: over-limit tx rejected=$rejected', + async ({ isSimulation, skipFeeEnforcement, rejected }) => { + db.findLeafIndices.mockResolvedValue([]); + const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { + l1ChainId: 1, + rollupVersion: 2, + setupAllowList: [], + gasFees: new GasFees(1, 1), + skipFeeEnforcement, + isSimulation, + timestamp: 100n, + blockNumber: BlockNumber(5), + txsPermitted: true, + }); + const tx = await mockPrivateTxWithGasSettings( + GasSettings.forEstimation({ maxFeesPerGas: new GasFees(1, 1) }), + ); + const result = await validator.validateTx(tx); + const reasons = result.result === 'invalid' ? result.reason : []; + expect(reasons.some(r => r.includes(TX_ERROR_GAS_LIMIT_TOO_HIGH))).toBe(rejected); + }, + ); }); it('excludes proof validator when no verifier is provided', () => { 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 7fb70668afdb..09ef68b34b33 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -86,7 +86,9 @@ export interface TransactionValidator { * without consulting the pool or running proof verification. * * The `doubleSpendValidator` failure is special-cased by the caller (`handleGossipedTx`) - * to determine severity based on how recently the nullifier appeared. + * to determine severity based on how recently the nullifier appeared. The caller reports the + * first failing entry among equally severe ones, so `doubleSpendValidator` must stay ahead of + * the other mid-tolerance entries for that special case to apply. */ export function createFirstStageTxValidationsForGossipedTransactions( timestamp: UInt64, @@ -156,8 +158,6 @@ export function createFirstStageTxValidationsForGossipedTransactions( ), severity: PeerErrorSeverity.MidToleranceError, // This is handled specifically at the point of rejection by considering a recent window where it may have been valid }, - // Must stay after doubleSpendValidator: on equal severities the first failing entry is reported, and - // handleGossipedTx special-cases the doubleSpendValidator name to grade severity by nullifier recency. gasLimitsValidator: { validator: new GasLimitsValidator({ ...gasLimitOpts, bindings }), severity: PeerErrorSeverity.MidToleranceError, @@ -344,11 +344,8 @@ export function createTxValidatorForAcceptingTxsOverRPC( new ContractInstanceTxValidator(bindings), ]; - // Declared gas-limit admission is not fee enforcement, so it runs even when fees are skipped, but it is - // skipped during simulation: gas estimation submits intentionally-inflated `forEstimation` limits (above - // the per-tx max) and the wallet clamps the real tx to the admission limit afterward, so enforcing the - // limit on the estimation tx would reject a valid estimation. GasLimitsValidator is the sole owner of the - // limit check (GasTxValidator below performs none), so this guard fully expresses the exemption. + // Gas-limit validation runs even when fee enforcement is skipped, but not during simulation: gas estimation + // submits intentionally-inflated `forEstimation` limits and the wallet clamps the real tx afterward. if (!isSimulation) { validators.push(new GasLimitsValidator({ maxTxL2Gas, maxTxDAGas, bindings })); } @@ -420,10 +417,8 @@ function createTxValidatorForValidatingAgainstCurrentState( new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp, bindings), new BlockHeaderTxValidator(archiveSource, bindings), new DoubleSpendTxValidator(nullifierSource, bindings), - // No limit opts: block building enforces only the per-tx protocol ceiling. Network admission limits are - // relay policy; per-block capacity is enforced while packing, and applying them here could reject an - // otherwise valid proposed block. This is where over-declared limits on txs that entered via req/resp or - // block proposals are caught, before execution would trip the AVM's max-processable-gas assertion. + // No limit opts: enforce only the per-tx protocol ceiling. Network admission limits are relay policy and + // must not invalidate a proposed block. new GasLimitsValidator({ bindings }), new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees, 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 efdf476b1ef8..b9a7623e1e95 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 @@ -131,7 +131,7 @@ describe('GasTxValidator', () => { await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PAYER_BALANCE); }); - it('does not enforce gas-limit admission, which is owned by GasLimitsValidator', async () => { + it('does not enforce gas limits, which are owned by GasLimitsValidator', async () => { // Gas estimation submits limits above the per-tx protocol maximum (GasSettings.forEstimation); whether they // are admissible is decided by GasLimitsValidator wherever a factory includes it, never by fee enforcement. tx.data.constants.txContext.gasSettings = GasSettings.fallback({ @@ -233,7 +233,7 @@ describe('GasLimitsValidator', () => { it('rejects tx below both DA and L2 gas minimums', async () => { tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(1, 1), + gasLimits: new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD - 1), maxFeesPerGas: gasFees.clone(), }); await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); 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 845aacfffb48..f2a973974875 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 @@ -58,9 +58,7 @@ export interface HasMaxFeePerGasData { * Generic over T so it can validate both full {@link Tx} objects and {@link TxMetaData} * (used during pending pool migration). * - * Sole owner of declared gas-limit admission. Used via the factory by: gossip (stage 1), RPC - * (except simulation — gas estimation submits limits above the per-tx maximum), block building, - * and pending pool migration. + * Sole owner of declared gas-limit validation; factories include it explicitly wherever the check applies. */ export class GasLimitsValidator implements TxValidator { #log: Logger; @@ -190,9 +188,8 @@ export class MaxFeePerGasValidator implements TxV * 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). * - * Declared gas-limit admission is deliberately not checked here: it is owned by {@link GasLimitsValidator}, - * which factories include separately so that exemptions from it (e.g. gas estimation) - * can be expressed without changing fee enforcement. + * Gas limits are deliberately not checked here: they are owned by {@link GasLimitsValidator}, which factories + * include separately so that exemptions (e.g. gas estimation) don't change fee enforcement. * * Used by: gossip (stage 1), RPC, and block building validators. */ From 7d39935bf1b18d26560a8779dea96c30f35eb3a9 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Fri, 14 Aug 2026 11:41:28 -0400 Subject: [PATCH 04/10] Apply suggestions from code review Co-authored-by: Maxim Vezenov --- yarn-project/p2p/src/msg_validators/tx_validator/factory.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 09ef68b34b33..6e1d5694cbf5 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -87,8 +87,7 @@ export interface TransactionValidator { * * The `doubleSpendValidator` failure is special-cased by the caller (`handleGossipedTx`) * to determine severity based on how recently the nullifier appeared. The caller reports the - * first failing entry among equally severe ones, so `doubleSpendValidator` must stay ahead of - * the other mid-tolerance entries for that special case to apply. + * first failing entry among equally severe ones. */ export function createFirstStageTxValidationsForGossipedTransactions( timestamp: UInt64, From a50e02c2ff4458d32644247fde3f7e611671bb40 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Fri, 14 Aug 2026 11:41:35 -0400 Subject: [PATCH 05/10] refactor(p2p): restore original comment above the RPC isSimulation guard --- .../p2p/src/msg_validators/tx_validator/factory.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 6e1d5694cbf5..e789592cd633 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -343,8 +343,11 @@ export function createTxValidatorForAcceptingTxsOverRPC( new ContractInstanceTxValidator(bindings), ]; - // Gas-limit validation runs even when fee enforcement is skipped, but not during simulation: gas estimation - // submits intentionally-inflated `forEstimation` limits and the wallet clamps the real tx afterward. + // Declared gas-limit admission is not fee enforcement, so it runs even when fees are skipped, but it is + // skipped during simulation: gas estimation submits intentionally-inflated `forEstimation` limits (above + // the per-tx max) and the wallet clamps the real tx to the admission limit afterward, so enforcing the + // limit on the estimation tx would reject a valid estimation. The fee-balance check below stays behind + // `skipFeeEnforcement`, and GasTxValidator does not re-run this same check. if (!isSimulation) { validators.push(new GasLimitsValidator({ maxTxL2Gas, maxTxDAGas, bindings })); } From f0340fb21cc1c7dab62771084f2bf8345d1bb599 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Fri, 14 Aug 2026 11:53:38 -0400 Subject: [PATCH 06/10] refactor(p2p): move GasLimitsValidator into its own file Code move only, no behavior change. GasLimitsValidator no longer shares ownership with GasTxValidator, so it gets its own gas_limits_validator.ts and test file, matching the one-validator-per-file layout of the directory. Exports still flow through the index barrel, so external imports are unaffected. --- .../mem_pools/tx_pool_v2/tx_pool_v2.test.ts | 3 +- .../tx_validator/factory.test.ts | 3 +- .../msg_validators/tx_validator/factory.ts | 3 +- .../tx_validator/gas_limits_validator.test.ts | 239 +++++++++++++++++ .../tx_validator/gas_limits_validator.ts | 113 ++++++++ .../tx_validator/gas_validator.test.ts | 241 +----------------- .../tx_validator/gas_validator.ts | 109 +------- .../src/msg_validators/tx_validator/index.ts | 1 + 8 files changed, 363 insertions(+), 349 deletions(-) create mode 100644 yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts create mode 100644 yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.ts 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 159a0faf162f..b06d6ff45591 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 @@ -32,7 +32,8 @@ import { getTelemetryClient } from '@aztec/telemetry-client'; import { type MockProxy, mock } from 'jest-mock-extended'; import { AggregateTxValidator } from '../../msg_validators/tx_validator/aggregate_tx_validator.js'; -import { GasLimitsValidator, MaxFeePerGasValidator } from '../../msg_validators/tx_validator/gas_validator.js'; +import { GasLimitsValidator } from '../../msg_validators/tx_validator/gas_limits_validator.js'; +import { MaxFeePerGasValidator } from '../../msg_validators/tx_validator/gas_validator.js'; import { AllowedSetupCallsMetaValidator } from '../../msg_validators/tx_validator/phases_validator.js'; import type { TxMetaData } from './tx_metadata.js'; import { AztecKVTxPoolV2 } from './tx_pool_v2.js'; 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 1a63cd02ba07..2b7af829972e 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 @@ -28,7 +28,8 @@ import { createTxValidatorForOnDemandReceivedTxs, createTxValidatorForTransactionsEnteringPendingTxPool, } from './factory.js'; -import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; +import { GasLimitsValidator } from './gas_limits_validator.js'; +import { GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; import { AllowedSetupCallsMetaValidator, PhasesTxValidator } from './phases_validator.js'; import { SizeTxValidator } from './size_validator.js'; 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 e789592cd633..1f7af43cb7be 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -57,7 +57,8 @@ import { CachedTxValidator } from './cached_tx_validator.js'; import { ContractInstanceTxValidator } from './contract_instance_validator.js'; import { DataTxValidator } from './data_validator.js'; import { DoubleSpendTxValidator, type NullifierSource } from './double_spend_validator.js'; -import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; +import { GasLimitsValidator } from './gas_limits_validator.js'; +import { GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; import { NullifierCache } from './nullifier_cache.js'; import { AllowedSetupCallsMetaValidator, PhasesTxValidator } from './phases_validator.js'; diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts new file mode 100644 index 000000000000..83c52e03f358 --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts @@ -0,0 +1,239 @@ +import { + MAX_PROCESSABLE_L2_GAS, + MAX_TX_DA_GAS, + PRIVATE_TX_L2_GAS_OVERHEAD, + PUBLIC_TX_L2_GAS_OVERHEAD, + TX_DA_GAS_OVERHEAD, +} from '@aztec/constants'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; +import { mockTx } from '@aztec/stdlib/testing'; +import { TX_ERROR_GAS_LIMIT_TOO_HIGH, TX_ERROR_INSUFFICIENT_GAS_LIMIT, type Tx } from '@aztec/stdlib/tx'; + +import assert from 'assert'; + +import { GasLimitsValidator } from './gas_limits_validator.js'; + +const DEFAULT_GAS_LIMITS = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); +const TEARDOWN_DA_GAS = 98_304; + +/** A tx with no public calls, carrying the default gas limits. */ +const makePrivateTx = async (gasFees: GasFees) => { + const privateTx = await mockTx(1, { + numberOfNonRevertiblePublicCallRequests: 0, + numberOfRevertiblePublicCallRequests: 0, + hasPublicTeardownCallRequest: false, + }); + assert(!privateTx.data.forPublic); + privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, + maxFeesPerGas: gasFees.clone(), + }); + return privateTx; +}; + +describe('GasLimitsValidator', () => { + let gasFees: GasFees; + let tx: Tx; + + beforeEach(async () => { + gasFees = new GasFees(11, 22); + tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, + maxFeesPerGas: gasFees.clone(), + }); + }); + + const expectValid = async (tx: Tx) => { + await expect(new GasLimitsValidator().validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }; + + const expectInvalid = async (tx: Tx, reason: string) => { + const result = await new GasLimitsValidator().validateTx(tx); + expect(result.result).toEqual('invalid'); + expect((result as { reason: string[] }).reason[0]).toContain(reason); + }; + + it('accepts public tx at exactly the minimum gas limits', async () => { + assert(!!tx.data.forPublic); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + }); + await expectValid(tx); + }); + + it('accepts private tx at exactly the minimum gas limits', async () => { + const privateTx = await makePrivateTx(gasFees); + privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + }); + await expectValid(privateTx); + }); + + it('rejects public tx below the public L2 gas minimum', async () => { + assert(!!tx.data.forPublic); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD - 1), + maxFeesPerGas: gasFees.clone(), + }); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); + + it('rejects private tx below the private L2 gas minimum', async () => { + const privateTx = await makePrivateTx(gasFees); + privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD - 1), + maxFeesPerGas: gasFees.clone(), + }); + await expectInvalid(privateTx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); + + it('rejects public tx at private L2 gas minimum (between the two thresholds)', async () => { + assert(!!tx.data.forPublic); + // PRIVATE_TX_L2_GAS_OVERHEAD is enough for a private tx but not for a public tx. + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + }); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); + + it('rejects tx below DA gas minimum', async () => { + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + }); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); + + it('rejects tx below both DA and L2 gas minimums', async () => { + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD - 1), + maxFeesPerGas: gasFees.clone(), + }); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }); + + it('rejects public tx if L2 gas limit is too high', async () => { + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expectInvalid(tx, TX_ERROR_GAS_LIMIT_TOO_HIGH); + }); + + it('rejects private tx if L2 gas limit is too high', async () => { + const privateTx = await makePrivateTx(gasFees); + privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expectInvalid(privateTx, TX_ERROR_GAS_LIMIT_TOO_HIGH); + }); + + describe('network admission limits (maxTxL2Gas, maxTxDAGas)', () => { + it('rejects tx exceeding maxTxL2Gas', async () => { + const maxTxL2Gas = 1_000_000; + const validator = new GasLimitsValidator({ maxTxL2Gas }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + }); + }); + + it('accepts tx at exactly maxTxL2Gas', async () => { + const maxTxL2Gas = 1_000_000; + const validator = new GasLimitsValidator({ maxTxL2Gas }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }); + + it('clamps maxTxL2Gas to the per-tx protocol maximum', async () => { + // Passing a higher network limit cannot raise the ceiling above MAX_PROCESSABLE_L2_GAS. + const validator = new GasLimitsValidator({ maxTxL2Gas: MAX_PROCESSABLE_L2_GAS + 1_000 }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + }); + }); + + it('falls back to MAX_PROCESSABLE_L2_GAS when no L2 limit is set', async () => { + const validator = new GasLimitsValidator(); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + }); + }); + + it('rejects tx exceeding maxTxDAGas', async () => { + const maxTxDAGas = 100_000; + const validator = new GasLimitsValidator({ maxTxDAGas }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + }); + }); + + it('accepts tx at exactly maxTxDAGas', async () => { + const maxTxDAGas = 100_000; + const validator = new GasLimitsValidator({ maxTxDAGas }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(maxTxDAGas, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }); + + it('caps DA at the max tx blob size when no DA limit is set', async () => { + const validator = new GasLimitsValidator(); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS + 1, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + }); + }); + + it('accepts a tx at exactly the max tx blob size DA limit when no DA limit is set', async () => { + const validator = new GasLimitsValidator(); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, PUBLIC_TX_L2_GAS_OVERHEAD), + maxFeesPerGas: gasFees.clone(), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + }); + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }); + }); +}); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.ts new file mode 100644 index 000000000000..391ee3f2c48f --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.ts @@ -0,0 +1,113 @@ +import { + MAX_PROCESSABLE_L2_GAS, + MAX_TX_DA_GAS, + PRIVATE_TX_L2_GAS_OVERHEAD, + PUBLIC_TX_L2_GAS_OVERHEAD, + TX_DA_GAS_OVERHEAD, +} from '@aztec/constants'; +import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; +import { Gas } from '@aztec/stdlib/gas'; +import { + TX_ERROR_GAS_LIMIT_TOO_HIGH, + TX_ERROR_INSUFFICIENT_GAS_LIMIT, + type TxValidationResult, + type TxValidator, +} from '@aztec/stdlib/tx'; + +/** Structural interface for types that carry gas limit data, used by {@link GasLimitsValidator}. */ +export interface HasGasLimitData { + txHash: { toString(): string }; + data: { + // We just need to know whether there is something here or not + forPublic?: unknown; + constants: { + txContext: { + gasSettings: { gasLimits: Gas }; + }; + }; + }; +} + +/** + * Validates that a transaction's gas limits are within acceptable bounds. + * + * Rejects transactions whose gas limits fall below the fixed minimums (FIXED_DA_GAS, + * FIXED_L2_GAS) or exceed the AVM's maximum processable L2 gas. This is a cheap, + * stateless check that operates on gas settings alone. + * + * Generic over T so it can validate both full {@link Tx} objects and {@link TxMetaData} + * (used during pending pool migration). + * + * Sole owner of declared gas-limit validation; factories include it explicitly wherever the check applies. + */ +export class GasLimitsValidator implements TxValidator { + #log: Logger; + #effectiveMaxL2Gas: number; + #effectiveMaxDAGas: number; + + /** + * @param maxTxL2Gas - The network admission limit on L2 gas a single tx may declare (the per-block mana + * allocation, see {@link computeNetworkTxGasLimits}). Defaults to the per-tx protocol maximum, so callers + * that pass nothing (e.g. block building) enforce only the protocol ceiling. + * @param maxTxDAGas - The network admission limit on DA gas a single tx may declare. Defaults to the + * per-tx protocol maximum {@link MAX_TX_DA_GAS}. + */ + constructor(opts?: { maxTxL2Gas?: number; maxTxDAGas?: number; bindings?: LoggerBindings }) { + this.#log = createLogger('sequencer:tx_validator:tx_gas', opts?.bindings); + // The passed limits are network admission limits; clamp to the per-tx protocol maxima as a hard ceiling. + // MAX_TX_DA_GAS bounds DA by what a single tx can actually post to a blob; declaring more is meaningless + // and would let a tx reserve checkpoint/block DA budget during proposal building it can't use. + this.#effectiveMaxL2Gas = Math.min(MAX_PROCESSABLE_L2_GAS, opts?.maxTxL2Gas ?? Infinity); + this.#effectiveMaxDAGas = Math.min(MAX_TX_DA_GAS, opts?.maxTxDAGas ?? Infinity); + } + + validateTx(tx: T): Promise { + return Promise.resolve(this.validateGasLimit(tx)); + } + + /** Checks gas limits are >= fixed minimums and <= effective max gas (L2 and DA). */ + validateGasLimit(tx: T): TxValidationResult { + const gasLimits = tx.data.constants.txContext.gasSettings.gasLimits; + const minGasLimits = new Gas( + TX_DA_GAS_OVERHEAD, + tx.data.forPublic ? PUBLIC_TX_L2_GAS_OVERHEAD : PRIVATE_TX_L2_GAS_OVERHEAD, + ); + + if (minGasLimits.gtAny(gasLimits)) { + this.#log.verbose(`Rejecting transaction due to the gas limit(s) not being above the minimum gas limit`, { + gasLimits, + minGasLimits, + }); + return { + result: 'invalid', + reason: [ + `${TX_ERROR_INSUFFICIENT_GAS_LIMIT} (required=da:${minGasLimits.daGas},l2:${minGasLimits.l2Gas} got=da:${gasLimits.daGas},l2:${gasLimits.l2Gas})`, + ], + }; + } + + if (gasLimits.l2Gas > this.#effectiveMaxL2Gas) { + this.#log.verbose(`Rejecting transaction due to the L2 gas limit being higher than the effective maximum`, { + gasLimits, + effectiveMaxL2Gas: this.#effectiveMaxL2Gas, + }); + return { + result: 'invalid', + reason: [`${TX_ERROR_GAS_LIMIT_TOO_HIGH} (l2Gas=${gasLimits.l2Gas}, max=${this.#effectiveMaxL2Gas})`], + }; + } + + if (gasLimits.daGas > this.#effectiveMaxDAGas) { + this.#log.verbose(`Rejecting transaction due to the DA gas limit being higher than the effective maximum`, { + gasLimits, + effectiveMaxDAGas: this.#effectiveMaxDAGas, + }); + return { + result: 'invalid', + reason: [`${TX_ERROR_GAS_LIMIT_TOO_HIGH} (daGas=${gasLimits.daGas}, max=${this.#effectiveMaxDAGas})`], + }; + } + + return { result: 'valid' }; + } +} 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 b9a7623e1e95..94c4538873ee 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 @@ -1,10 +1,4 @@ -import { - MAX_PROCESSABLE_L2_GAS, - MAX_TX_DA_GAS, - PRIVATE_TX_L2_GAS_OVERHEAD, - PUBLIC_TX_L2_GAS_OVERHEAD, - TX_DA_GAS_OVERHEAD, -} from '@aztec/constants'; +import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { Writeable } from '@aztec/foundation/types'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; @@ -14,38 +8,16 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import { mockTx } from '@aztec/stdlib/testing'; import type { PublicStateSource } from '@aztec/stdlib/trees'; -import { - TX_ERROR_GAS_LIMIT_TOO_HIGH, - TX_ERROR_INSUFFICIENT_FEE_PAYER_BALANCE, - TX_ERROR_INSUFFICIENT_FEE_PER_GAS, - TX_ERROR_INSUFFICIENT_GAS_LIMIT, - type Tx, -} from '@aztec/stdlib/tx'; +import { TX_ERROR_INSUFFICIENT_FEE_PAYER_BALANCE, TX_ERROR_INSUFFICIENT_FEE_PER_GAS, type Tx } from '@aztec/stdlib/tx'; -import assert from 'assert'; import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; -import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; +import { GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { patchNonRevertibleFn, patchRevertibleFn } from './test_utils.js'; const DEFAULT_GAS_LIMITS = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); const TEARDOWN_DA_GAS = 98_304; -/** A tx with no public calls, carrying the default gas limits. */ -const makePrivateTx = async (gasFees: GasFees) => { - const privateTx = await mockTx(1, { - numberOfNonRevertiblePublicCallRequests: 0, - numberOfRevertiblePublicCallRequests: 0, - hasPublicTeardownCallRequest: false, - }); - assert(!privateTx.data.forPublic); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: DEFAULT_GAS_LIMITS, - maxFeesPerGas: gasFees.clone(), - }); - return privateTx; -}; - describe('GasTxValidator', () => { // Vars for validator. let publicStateSource: MockProxy; @@ -154,213 +126,6 @@ describe('GasTxValidator', () => { }); }); -describe('GasLimitsValidator', () => { - let gasFees: GasFees; - let tx: Tx; - - beforeEach(async () => { - gasFees = new GasFees(11, 22); - tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: DEFAULT_GAS_LIMITS, - maxFeesPerGas: gasFees.clone(), - }); - }); - - const expectValid = async (tx: Tx) => { - await expect(new GasLimitsValidator().validateTx(tx)).resolves.toEqual({ result: 'valid' }); - }; - - const expectInvalid = async (tx: Tx, reason: string) => { - const result = await new GasLimitsValidator().validateTx(tx); - expect(result.result).toEqual('invalid'); - expect((result as { reason: string[] }).reason[0]).toContain(reason); - }; - - it('accepts public tx at exactly the minimum gas limits', async () => { - assert(!!tx.data.forPublic); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - }); - await expectValid(tx); - }); - - it('accepts private tx at exactly the minimum gas limits', async () => { - const privateTx = await makePrivateTx(gasFees); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - }); - await expectValid(privateTx); - }); - - it('rejects public tx below the public L2 gas minimum', async () => { - assert(!!tx.data.forPublic); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD - 1), - maxFeesPerGas: gasFees.clone(), - }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - - it('rejects private tx below the private L2 gas minimum', async () => { - const privateTx = await makePrivateTx(gasFees); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD - 1), - maxFeesPerGas: gasFees.clone(), - }); - await expectInvalid(privateTx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - - it('rejects public tx at private L2 gas minimum (between the two thresholds)', async () => { - assert(!!tx.data.forPublic); - // PRIVATE_TX_L2_GAS_OVERHEAD is enough for a private tx but not for a public tx. - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - - it('rejects tx below DA gas minimum', async () => { - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - - it('rejects tx below both DA and L2 gas minimums', async () => { - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD - 1), - maxFeesPerGas: gasFees.clone(), - }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - - it('rejects public tx if L2 gas limit is too high', async () => { - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expectInvalid(tx, TX_ERROR_GAS_LIMIT_TOO_HIGH); - }); - - it('rejects private tx if L2 gas limit is too high', async () => { - const privateTx = await makePrivateTx(gasFees); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expectInvalid(privateTx, TX_ERROR_GAS_LIMIT_TOO_HIGH); - }); - - describe('network admission limits (maxTxL2Gas, maxTxDAGas)', () => { - it('rejects tx exceeding maxTxL2Gas', async () => { - const maxTxL2Gas = 1_000_000; - const validator = new GasLimitsValidator({ maxTxL2Gas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); - }); - - it('accepts tx at exactly maxTxL2Gas', async () => { - const maxTxL2Gas = 1_000_000; - const validator = new GasLimitsValidator({ maxTxL2Gas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); - }); - - it('clamps maxTxL2Gas to the per-tx protocol maximum', async () => { - // Passing a higher network limit cannot raise the ceiling above MAX_PROCESSABLE_L2_GAS. - const validator = new GasLimitsValidator({ maxTxL2Gas: MAX_PROCESSABLE_L2_GAS + 1_000 }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); - }); - - it('falls back to MAX_PROCESSABLE_L2_GAS when no L2 limit is set', async () => { - const validator = new GasLimitsValidator(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); - }); - - it('rejects tx exceeding maxTxDAGas', async () => { - const maxTxDAGas = 100_000; - const validator = new GasLimitsValidator({ maxTxDAGas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); - }); - - it('accepts tx at exactly maxTxDAGas', async () => { - const maxTxDAGas = 100_000; - const validator = new GasLimitsValidator({ maxTxDAGas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxTxDAGas, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); - }); - - it('caps DA at the max tx blob size when no DA limit is set', async () => { - const validator = new GasLimitsValidator(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS + 1, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); - }); - - it('accepts a tx at exactly the max tx blob size DA limit when no DA limit is set', async () => { - const validator = new GasLimitsValidator(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); - }); - }); -}); - describe('MaxFeePerGasValidator', () => { it('accepts tx with sufficient max fees per gas', async () => { const gasFees = new GasFees(10, 20); 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 f2a973974875..4faec1385448 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 @@ -1,20 +1,11 @@ -import { - MAX_PROCESSABLE_L2_GAS, - MAX_TX_DA_GAS, - PRIVATE_TX_L2_GAS_OVERHEAD, - PUBLIC_TX_L2_GAS_OVERHEAD, - TX_DA_GAS_OVERHEAD, -} from '@aztec/constants'; import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; import { computeFeePayerBalanceStorageSlot } from '@aztec/protocol-contracts/fee-juice'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { Gas, GasFees } from '@aztec/stdlib/gas'; +import type { GasFees } from '@aztec/stdlib/gas'; import type { PublicStateSource } from '@aztec/stdlib/trees'; import { - TX_ERROR_GAS_LIMIT_TOO_HIGH, TX_ERROR_INSUFFICIENT_FEE_PAYER_BALANCE, TX_ERROR_INSUFFICIENT_FEE_PER_GAS, - TX_ERROR_INSUFFICIENT_GAS_LIMIT, type Tx, type TxValidationResult, type TxValidator, @@ -22,20 +13,6 @@ import { import { getFeePayerClaimAmount, getTxFeeLimit } from './fee_payer_balance.js'; -/** Structural interface for types that carry gas limit data, used by {@link GasLimitsValidator}. */ -export interface HasGasLimitData { - txHash: { toString(): string }; - data: { - // We just need to know whether there is something here or not - forPublic?: unknown; - constants: { - txContext: { - gasSettings: { gasLimits: Gas }; - }; - }; - }; -} - /** Structural interface for types that carry max fee per gas data, used by {@link MaxFeePerGasValidator}. */ export interface HasMaxFeePerGasData { txHash: { toString(): string }; @@ -48,90 +25,6 @@ export interface HasMaxFeePerGasData { }; } -/** - * Validates that a transaction's gas limits are within acceptable bounds. - * - * Rejects transactions whose gas limits fall below the fixed minimums (FIXED_DA_GAS, - * FIXED_L2_GAS) or exceed the AVM's maximum processable L2 gas. This is a cheap, - * stateless check that operates on gas settings alone. - * - * Generic over T so it can validate both full {@link Tx} objects and {@link TxMetaData} - * (used during pending pool migration). - * - * Sole owner of declared gas-limit validation; factories include it explicitly wherever the check applies. - */ -export class GasLimitsValidator implements TxValidator { - #log: Logger; - #effectiveMaxL2Gas: number; - #effectiveMaxDAGas: number; - - /** - * @param maxTxL2Gas - The network admission limit on L2 gas a single tx may declare (the per-block mana - * allocation, see {@link computeNetworkTxGasLimits}). Defaults to the per-tx protocol maximum, so callers - * that pass nothing (e.g. block building) enforce only the protocol ceiling. - * @param maxTxDAGas - The network admission limit on DA gas a single tx may declare. Defaults to the - * per-tx protocol maximum {@link MAX_TX_DA_GAS}. - */ - constructor(opts?: { maxTxL2Gas?: number; maxTxDAGas?: number; bindings?: LoggerBindings }) { - this.#log = createLogger('sequencer:tx_validator:tx_gas', opts?.bindings); - // The passed limits are network admission limits; clamp to the per-tx protocol maxima as a hard ceiling. - // MAX_TX_DA_GAS bounds DA by what a single tx can actually post to a blob; declaring more is meaningless - // and would let a tx reserve checkpoint/block DA budget during proposal building it can't use. - this.#effectiveMaxL2Gas = Math.min(MAX_PROCESSABLE_L2_GAS, opts?.maxTxL2Gas ?? Infinity); - this.#effectiveMaxDAGas = Math.min(MAX_TX_DA_GAS, opts?.maxTxDAGas ?? Infinity); - } - - validateTx(tx: T): Promise { - return Promise.resolve(this.validateGasLimit(tx)); - } - - /** Checks gas limits are >= fixed minimums and <= effective max gas (L2 and DA). */ - validateGasLimit(tx: T): TxValidationResult { - const gasLimits = tx.data.constants.txContext.gasSettings.gasLimits; - const minGasLimits = new Gas( - TX_DA_GAS_OVERHEAD, - tx.data.forPublic ? PUBLIC_TX_L2_GAS_OVERHEAD : PRIVATE_TX_L2_GAS_OVERHEAD, - ); - - if (minGasLimits.gtAny(gasLimits)) { - this.#log.verbose(`Rejecting transaction due to the gas limit(s) not being above the minimum gas limit`, { - gasLimits, - minGasLimits, - }); - return { - result: 'invalid', - reason: [ - `${TX_ERROR_INSUFFICIENT_GAS_LIMIT} (required=da:${minGasLimits.daGas},l2:${minGasLimits.l2Gas} got=da:${gasLimits.daGas},l2:${gasLimits.l2Gas})`, - ], - }; - } - - if (gasLimits.l2Gas > this.#effectiveMaxL2Gas) { - this.#log.verbose(`Rejecting transaction due to the L2 gas limit being higher than the effective maximum`, { - gasLimits, - effectiveMaxL2Gas: this.#effectiveMaxL2Gas, - }); - return { - result: 'invalid', - reason: [`${TX_ERROR_GAS_LIMIT_TOO_HIGH} (l2Gas=${gasLimits.l2Gas}, max=${this.#effectiveMaxL2Gas})`], - }; - } - - if (gasLimits.daGas > this.#effectiveMaxDAGas) { - this.#log.verbose(`Rejecting transaction due to the DA gas limit being higher than the effective maximum`, { - gasLimits, - effectiveMaxDAGas: this.#effectiveMaxDAGas, - }); - return { - result: 'invalid', - reason: [`${TX_ERROR_GAS_LIMIT_TOO_HIGH} (daGas=${gasLimits.daGas}, max=${this.#effectiveMaxDAGas})`], - }; - } - - return { result: 'valid' }; - } -} - /** * Validates that a transaction's max fee per gas meets the current block's gas fees. * diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/index.ts b/yarn-project/p2p/src/msg_validators/tx_validator/index.ts index 4a21303b725e..4542220a3965 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/index.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/index.ts @@ -4,6 +4,7 @@ export * from './double_spend_validator.js'; export * from './metadata_validator.js'; export * from './tx_proof_validator.js'; export * from './block_header_validator.js'; +export * from './gas_limits_validator.js'; export * from './gas_validator.js'; export * from './phases_validator.js'; export * from './test_utils.js'; From b7d8cb2cb5db0acc82e731f6c25d488903851475 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Fri, 14 Aug 2026 11:58:38 -0400 Subject: [PATCH 07/10] test(p2p): derive factory-test gas limits from protocol constants --- .../src/msg_validators/tx_validator/factory.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 2b7af829972e..ad40ab5ef3bd 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 @@ -1,4 +1,4 @@ -import { MAX_TX_DA_GAS } from '@aztec/constants'; +import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { ContractDataSource } from '@aztec/stdlib/contract'; @@ -94,7 +94,7 @@ describe('Validator factory functions', () => { }); it('forwards the network admission limits to the gas limits validator', async () => { - const maxTxL2Gas = 1_000_000; + const maxTxL2Gas = Math.floor(MAX_PROCESSABLE_L2_GAS / 2); const validators = createFirstStageTxValidationsForGossipedTransactions( 0n, BlockNumber(2), @@ -120,7 +120,7 @@ describe('Validator factory functions', () => { }); it('forwards the network DA admission limit to the gas limits validator', async () => { - const maxTxDAGas = 100_000; + const maxTxDAGas = Math.floor(MAX_TX_DA_GAS / 2); const validators = createFirstStageTxValidationsForGossipedTransactions( 0n, BlockNumber(2), @@ -138,7 +138,10 @@ describe('Validator factory functions', () => { // Over the network DA admission limit but under the protocol DA ceiling. const tx = await mockPrivateTxWithGasSettings( - GasSettings.fallback({ gasLimits: new Gas(maxTxDAGas + 1, 1_000_000), maxFeesPerGas: new GasFees(1, 1) }), + GasSettings.fallback({ + gasLimits: new Gas(maxTxDAGas + 1, MAX_PROCESSABLE_L2_GAS), + maxFeesPerGas: new GasFees(1, 1), + }), ); const result = await validators.gasLimitsValidator.validator.validateTx(tx); expect(result.result).toBe('invalid'); From 92939d8b25aaabc7dc0429a772270eb17332c66e Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Mon, 17 Aug 2026 14:25:24 -0400 Subject: [PATCH 08/10] Update yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts Co-authored-by: Nicolas Chamo --- .../src/public/public_tx_simulator/public_tx_simulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts index 019ba38d164f..e55074bee671 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts @@ -197,7 +197,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface { context.halt(); - // Such transactions should be filtered by GasLimitsValidator. + // Gas estimation deliberately declares limits above this ceiling; block building rejects them before execution. assert( context.getActualGasUsed().l2Gas <= MAX_PROCESSABLE_L2_GAS, `Transaction consumes ${context.getActualGasUsed().l2Gas} L2 gas, which exceeds the maximum processable gas of ${MAX_PROCESSABLE_L2_GAS}`, From 69061c73036ee4a4a3f4963e26dabd488443c714 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Tue, 18 Aug 2026 12:47:23 +0100 Subject: [PATCH 09/10] fix(p2p): split gas-limit admission into min and max validators The declared gas-limit floor and ceiling had different exemption rules but shared one validator, so skipping the ceiling for gas estimation also skipped the floor. Splitting them lets the RPC factory keep the floor on the simulation path while exempting only the ceiling. A tx declaring less than the fixed protocol overheads can never be mined, so rejecting it during simulation is the earliest useful feedback rather than a surprise on sendTx. --- .../mem_pools/tx_pool_v2/tx_pool_v2.test.ts | 13 +- .../src/msg_validators/tx_validator/README.md | 21 +- .../tx_validator/factory.test.ts | 68 ++++- .../msg_validators/tx_validator/factory.ts | 30 ++- .../tx_validator/gas_limits_validator.test.ts | 254 +++++++----------- .../tx_validator/gas_limits_validator.ts | 85 ++++-- .../tx_validator/gas_validator.test.ts | 5 +- .../tx_validator/gas_validator.ts | 13 +- yarn-project/validator-client/README.md | 2 +- .../wallet-sdk/src/base-wallet/base_wallet.ts | 2 +- 10 files changed, 267 insertions(+), 226 deletions(-) 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 b06d6ff45591..c7c67d4079cd 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 @@ -32,7 +32,10 @@ import { getTelemetryClient } from '@aztec/telemetry-client'; import { type MockProxy, mock } from 'jest-mock-extended'; import { AggregateTxValidator } from '../../msg_validators/tx_validator/aggregate_tx_validator.js'; -import { GasLimitsValidator } from '../../msg_validators/tx_validator/gas_limits_validator.js'; +import { + MaxGasLimitsValidator, + MinGasLimitsValidator, +} from '../../msg_validators/tx_validator/gas_limits_validator.js'; import { MaxFeePerGasValidator } from '../../msg_validators/tx_validator/gas_validator.js'; import { AllowedSetupCallsMetaValidator } from '../../msg_validators/tx_validator/phases_validator.js'; import type { TxMetaData } from './tx_metadata.js'; @@ -730,7 +733,13 @@ describe('TxPoolV2', () => { gasPool = new AztecKVTxPoolV2(gasStore, gasArchiveStore, { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, - createTxValidator: () => Promise.resolve(new GasLimitsValidator()), + createTxValidator: () => + Promise.resolve( + new AggregateTxValidator( + new MinGasLimitsValidator(), + new MaxGasLimitsValidator(), + ), + ), checkAllowedSetupCalls: () => Promise.resolve(true), blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); 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 d4562a462503..afb465130f19 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/README.md +++ b/yarn-project/p2p/src/msg_validators/tx_validator/README.md @@ -19,7 +19,7 @@ Unsolicited transactions from any peer. Fully validated in two stages with a poo | Step | What runs | On failure | |------|-----------|------------| -| **Stage 1** (fast) | TxPermitted, Data, Metadata, Timestamp, DoubleSpend, GasLimits, Gas, Phases, BlockHeader | Penalize peer, reject tx | +| **Stage 1** (fast) | TxPermitted, Data, Metadata, Timestamp, DoubleSpend, MinGasLimits, MaxGasLimits, Gas, Phases, BlockHeader | Penalize peer, reject tx | | **Pool pre-check** | `canAddPendingTx` — checks for duplicates, pool capacity | Ignore tx (no penalty) | | **Stage 2** (slow) | Proof verification | Penalize peer, reject tx | | **Pool add** | `addPendingTxs` | Accept, ignore, or reject | @@ -34,7 +34,8 @@ Each stage-1 and stage-2 validator is paired with a `PeerErrorSeverity`. If a va Unsolicited transactions from a local wallet/PXE. Runs the full set of checks as a single aggregate validator: - TxPermitted, Size, Data, Metadata, Timestamp, DoubleSpend, Phases, BlockHeader -- GasLimits (skipped for simulations — gas estimation submits limits above the per-tx maximum) +- MinGasLimits +- MaxGasLimits (skipped for simulations — gas estimation submits limits above the per-tx maximum) - Gas (optional — skipped when `skipFeeEnforcement` is set) - Proof verification (optional — skipped for simulations when no verifier is provided) @@ -57,7 +58,7 @@ State-dependent checks are deferred to either the block building validator (for Transactions already in the pool, about to be sequenced into a block. Re-validates against the current state of the block being built. **This is where invalid txs that entered via req/resp or block proposals are caught** — their invalidity is reported as part of block validation/attestation. Runs: -- Timestamp, DoubleSpend, Phases, GasLimits, Gas, BlockHeader +- Timestamp, DoubleSpend, Phases, MinGasLimits, MaxGasLimits, Gas, BlockHeader Does **not** run: - Proof, Data — already verified on entry (by gossip, RPC, or req/resp validators) @@ -76,7 +77,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, MaxFeePerGas, Timestamp, AllowedSetupCalls +- DoubleSpend, BlockHeader, MinGasLimits, MaxGasLimits, MaxFeePerGas, Timestamp, AllowedSetupCalls Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. @@ -93,7 +94,8 @@ The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaD | `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` | 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 (optionally clamped further by network admission limits). Sole owner of declared gas-limit validation | 3–10 us | +| `MinGasLimitsValidator` | Gas limits are >= the fixed protocol overheads. Applies on every entry point, with no exemptions | 3–10 us | +| `MaxGasLimitsValidator` | Gas limits are <= AVM max processable L2 gas (optionally clamped further by network admission limits). Exempted on the gas estimation path | 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 | | `AllowedSetupCallsMetaValidator` | Checks the precomputed `allowedSetupCalls` flag on `TxMetaData`. Used in pool migration instead of the full `PhasesTxValidator` | — | @@ -111,18 +113,19 @@ The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaD | Timestamp | Stage 1 | Yes | — | Yes | Yes | | DoubleSpend | Stage 1 | Yes | — | Yes | Yes | | Gas (fee balance) | Stage 1 | Optional* | — | Yes | — | -| GasLimits | Stage 1 | Yes*** | — | Yes | Yes | +| MinGasLimits | Stage 1 | Yes | — | Yes | Yes | +| MaxGasLimits | Stage 1 | Yes*** | — | Yes | Yes | | MaxFeePerGas (standalone) | — | — | — | — | Yes | | Phases | Stage 1 | Yes | — | Yes | — | | AllowedSetupCalls | — | — | — | — | 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 `MaxFeePerGasValidator` as its first step, so fee-per-gas is checked wherever `GasTxValidator` runs. Pool migration uses `MaxFeePerGasValidator` standalone because it doesn't need the balance check. Declared gas-limit validation is owned solely by `GasLimitsValidator`. +\* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `MaxFeePerGasValidator` as its first step, so fee-per-gas is checked wherever `GasTxValidator` runs. Pool migration uses `MaxFeePerGasValidator` standalone because it doesn't need the balance check. Declared gas-limit validation is owned solely by `MinGasLimitsValidator` and `MaxGasLimitsValidator`. \** Proof verification is skipped for simulations (no verifier provided). -\*** Skipped for simulations: gas estimation submits limits above the per-tx maximum, and the wallet clamps the real tx to the admission limit afterward. +\*** Only the ceiling is skipped for simulations: gas estimation submits limits above the per-tx maximum, and the wallet clamps the real tx to the admission limit afterward. The floor still applies, since a tx below it can never be mined. -The gas-limit bounds `GasLimitsValidator` enforces here — the per-tx protocol maxima and the network admission limits — are documented in [`stdlib/src/gas/README.md`](../../../../stdlib/src/gas/README.md) under "Gas and Data Limits". +The gas-limit bounds `MaxGasLimitsValidator` enforces here — the per-tx protocol maxima and the network admission limits — are documented in [`stdlib/src/gas/README.md`](../../../../stdlib/src/gas/README.md) under "Gas and Data Limits". ## Fee-Per-Gas Rejection Strategy 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 ad40ab5ef3bd..6d3a3c83483c 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 @@ -10,7 +10,7 @@ import type { } from '@aztec/stdlib/interfaces/server'; import { PeerErrorSeverity } from '@aztec/stdlib/p2p'; import { mockTx } from '@aztec/stdlib/testing'; -import { type GlobalVariables, TX_ERROR_GAS_LIMIT_TOO_HIGH } from '@aztec/stdlib/tx'; +import { type GlobalVariables, TX_ERROR_GAS_LIMIT_TOO_HIGH, TX_ERROR_INSUFFICIENT_GAS_LIMIT } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -28,7 +28,7 @@ import { createTxValidatorForOnDemandReceivedTxs, createTxValidatorForTransactionsEnteringPendingTxPool, } from './factory.js'; -import { GasLimitsValidator } from './gas_limits_validator.js'; +import { MaxGasLimitsValidator, MinGasLimitsValidator } from './gas_limits_validator.js'; import { GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; import { AllowedSetupCallsMetaValidator, PhasesTxValidator } from './phases_validator.js'; @@ -86,7 +86,8 @@ describe('Validator factory functions', () => { 'phasesValidator', 'blockHeaderValidator', 'doubleSpendValidator', - 'gasLimitsValidator', + 'minGasLimitsValidator', + 'maxGasLimitsValidator', 'gasValidator', 'dataValidator', 'contractInstanceValidator', @@ -114,7 +115,7 @@ describe('Validator factory functions', () => { const tx = await mockPrivateTxWithGasSettings( GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), maxFeesPerGas: new GasFees(1, 1) }), ); - const result = await validators.gasLimitsValidator.validator.validateTx(tx); + const result = await validators.maxGasLimitsValidator.validator.validateTx(tx); expect(result.result).toBe('invalid'); expect((result as { reason: string[] }).reason[0]).toContain(TX_ERROR_GAS_LIMIT_TOO_HIGH); }); @@ -143,7 +144,7 @@ describe('Validator factory functions', () => { maxFeesPerGas: new GasFees(1, 1), }), ); - const result = await validators.gasLimitsValidator.validator.validateTx(tx); + const result = await validators.maxGasLimitsValidator.validator.validateTx(tx); expect(result.result).toBe('invalid'); expect((result as { reason: string[] }).reason[0]).toContain(TX_ERROR_GAS_LIMIT_TOO_HIGH); }); @@ -186,7 +187,8 @@ describe('Validator factory functions', () => { expect(validators.dataValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); expect(validators.metadataValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); expect(validators.doubleSpendValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); - expect(validators.gasLimitsValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); + expect(validators.minGasLimitsValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); + expect(validators.maxGasLimitsValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); expect(validators.gasValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); expect(validators.phasesValidator.severity).toBe(PeerErrorSeverity.MidToleranceError); }); @@ -296,7 +298,8 @@ describe('Validator factory functions', () => { DoubleSpendTxValidator.name, DataTxValidator.name, ContractInstanceTxValidator.name, - GasLimitsValidator.name, + MinGasLimitsValidator.name, + MaxGasLimitsValidator.name, GasTxValidator.name, TxProofValidator.name, ]); @@ -317,14 +320,15 @@ describe('Validator factory functions', () => { const aggregate = validator as AggregateTxValidator; const names = getValidatorNames(aggregate); // Gas-limit validation is not fee enforcement, so it stays even with fees skipped. - expect(names).toContain(GasLimitsValidator.name); + expect(names).toContain(MinGasLimitsValidator.name); + expect(names).toContain(MaxGasLimitsValidator.name); expect(names).not.toContain(GasTxValidator.name); expect(names).toContain(TxProofValidator.name); }); - it('excludes the gas-limits validator during simulation', () => { - // Gas estimation submits intentionally-inflated forEstimation limits, so gas-limit validation must not - // reject the estimation tx; the wallet clamps the real tx afterward. + it('excludes only the gas-limits ceiling during simulation', () => { + // Gas estimation submits intentionally-inflated forEstimation limits, so the ceiling must not reject the + // estimation tx; the wallet clamps the real tx afterward. The floor has no such exemption. const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { l1ChainId: 1, rollupVersion: 2, @@ -338,7 +342,9 @@ describe('Validator factory functions', () => { }); const aggregate = validator as AggregateTxValidator; - expect(getValidatorNames(aggregate)).not.toContain(GasLimitsValidator.name); + const names = getValidatorNames(aggregate); + expect(names).not.toContain(MaxGasLimitsValidator.name); + expect(names).toContain(MinGasLimitsValidator.name); }); describe('gas-limit validation', () => { @@ -375,6 +381,38 @@ describe('Validator factory functions', () => { expect(reasons.some(r => r.includes(TX_ERROR_GAS_LIMIT_TOO_HIGH))).toBe(rejected); }, ); + + // Estimation only ever needs the ceiling exempted. The minimum is a protocol floor that the real tx can + // never satisfy, so a simulation that passes it would only fail again on sendTx. + it.each` + isSimulation | skipFeeEnforcement + ${false} | ${false} + ${false} | ${true} + ${true} | ${false} + ${true} | ${true} + `( + 'isSimulation=$isSimulation, skipFeeEnforcement=$skipFeeEnforcement: under-minimum tx is rejected', + async ({ isSimulation, skipFeeEnforcement }) => { + db.findLeafIndices.mockResolvedValue([]); + const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { + l1ChainId: 1, + rollupVersion: 2, + setupAllowList: [], + gasFees: new GasFees(1, 1), + skipFeeEnforcement, + isSimulation, + timestamp: 100n, + blockNumber: BlockNumber(5), + txsPermitted: true, + }); + const tx = await mockPrivateTxWithGasSettings( + GasSettings.fallback({ gasLimits: Gas.empty(), maxFeesPerGas: new GasFees(1, 1) }), + ); + const result = await validator.validateTx(tx); + const reasons = result.result === 'invalid' ? result.reason : []; + expect(reasons.some(r => r.includes(TX_ERROR_INSUFFICIENT_GAS_LIMIT))).toBe(true); + }, + ); }); it('excludes proof validator when no verifier is provided', () => { @@ -416,7 +454,8 @@ describe('Validator factory functions', () => { PhasesTxValidator.name, BlockHeaderTxValidator.name, DoubleSpendTxValidator.name, - GasLimitsValidator.name, + MinGasLimitsValidator.name, + MaxGasLimitsValidator.name, GasTxValidator.name, ]); }); @@ -454,7 +493,8 @@ describe('Validator factory functions', () => { const aggregate = validator as AggregateTxValidator; expect(getValidatorNames(aggregate)).toEqual([ - GasLimitsValidator.name, + MinGasLimitsValidator.name, + MaxGasLimitsValidator.name, MaxFeePerGasValidator.name, TimestampTxValidator.name, DoubleSpendTxValidator.name, 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 1f7af43cb7be..bb72090941d3 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -57,7 +57,7 @@ import { CachedTxValidator } from './cached_tx_validator.js'; import { ContractInstanceTxValidator } from './contract_instance_validator.js'; import { DataTxValidator } from './data_validator.js'; import { DoubleSpendTxValidator, type NullifierSource } from './double_spend_validator.js'; -import { GasLimitsValidator } from './gas_limits_validator.js'; +import { MaxGasLimitsValidator, MinGasLimitsValidator } from './gas_limits_validator.js'; import { GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; import { NullifierCache } from './nullifier_cache.js'; @@ -158,8 +158,12 @@ export function createFirstStageTxValidationsForGossipedTransactions( ), severity: PeerErrorSeverity.MidToleranceError, // This is handled specifically at the point of rejection by considering a recent window where it may have been valid }, - gasLimitsValidator: { - validator: new GasLimitsValidator({ ...gasLimitOpts, bindings }), + minGasLimitsValidator: { + validator: new MinGasLimitsValidator(bindings), + severity: PeerErrorSeverity.MidToleranceError, + }, + maxGasLimitsValidator: { + validator: new MaxGasLimitsValidator({ ...gasLimitOpts, bindings }), severity: PeerErrorSeverity.MidToleranceError, }, gasValidator: { @@ -342,15 +346,17 @@ export function createTxValidatorForAcceptingTxsOverRPC( new DoubleSpendTxValidator(new NullifierCache(db), bindings), new DataTxValidator(bindings), new ContractInstanceTxValidator(bindings), + // Declared gas-limit admission is not fee enforcement, so it runs even when fees are skipped. The floor + // has no exemption: a tx declaring less than the fixed overheads can never be mined, so rejecting it + // during simulation is the earliest useful feedback rather than a surprise on sendTx. + new MinGasLimitsValidator(bindings), ]; - // Declared gas-limit admission is not fee enforcement, so it runs even when fees are skipped, but it is - // skipped during simulation: gas estimation submits intentionally-inflated `forEstimation` limits (above - // the per-tx max) and the wallet clamps the real tx to the admission limit afterward, so enforcing the - // limit on the estimation tx would reject a valid estimation. The fee-balance check below stays behind - // `skipFeeEnforcement`, and GasTxValidator does not re-run this same check. + // Only the ceiling is exempted during simulation: gas estimation submits intentionally-inflated + // `forEstimation` limits (above the per-tx max) and the wallet clamps the real tx to the admission limit + // afterward, so enforcing the ceiling on the estimation tx would reject a valid estimation. if (!isSimulation) { - validators.push(new GasLimitsValidator({ maxTxL2Gas, maxTxDAGas, bindings })); + validators.push(new MaxGasLimitsValidator({ maxTxL2Gas, maxTxDAGas, bindings })); } if (!skipFeeEnforcement) { @@ -420,9 +426,10 @@ function createTxValidatorForValidatingAgainstCurrentState( new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp, bindings), new BlockHeaderTxValidator(archiveSource, bindings), new DoubleSpendTxValidator(nullifierSource, bindings), + new MinGasLimitsValidator(bindings), // No limit opts: enforce only the per-tx protocol ceiling. Network admission limits are relay policy and // must not invalidate a proposed block. - new GasLimitsValidator({ bindings }), + new MaxGasLimitsValidator({ bindings }), new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees, bindings), ); } @@ -462,7 +469,8 @@ export async function createTxValidatorForTransactionsEnteringPendingTxPool( }, }; return new AggregateTxValidator( - new GasLimitsValidator({ ...gasLimitOpts, bindings }), + new MinGasLimitsValidator(bindings), + new MaxGasLimitsValidator({ ...gasLimitOpts, bindings }), new MaxFeePerGasValidator(gasFees, bindings), new TimestampTxValidator({ timestamp, blockNumber }, bindings), new DoubleSpendTxValidator(nullifierSource, bindings), diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts index 83c52e03f358..51f823559bff 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts @@ -11,10 +11,9 @@ import { TX_ERROR_GAS_LIMIT_TOO_HIGH, TX_ERROR_INSUFFICIENT_GAS_LIMIT, type Tx } import assert from 'assert'; -import { GasLimitsValidator } from './gas_limits_validator.js'; +import { MaxGasLimitsValidator, MinGasLimitsValidator } from './gas_limits_validator.js'; const DEFAULT_GAS_LIMITS = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); -const TEARDOWN_DA_GAS = 98_304; /** A tx with no public calls, carrying the default gas limits. */ const makePrivateTx = async (gasFees: GasFees) => { @@ -31,209 +30,154 @@ const makePrivateTx = async (gasFees: GasFees) => { return privateTx; }; -describe('GasLimitsValidator', () => { +describe('gas limits validators', () => { let gasFees: GasFees; let tx: Tx; + const setGasLimits = (tx: Tx, gasLimits: Gas) => { + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits, maxFeesPerGas: gasFees.clone() }); + }; + beforeEach(async () => { gasFees = new GasFees(11, 22); tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: DEFAULT_GAS_LIMITS, - maxFeesPerGas: gasFees.clone(), - }); + setGasLimits(tx, DEFAULT_GAS_LIMITS); }); - const expectValid = async (tx: Tx) => { - await expect(new GasLimitsValidator().validateTx(tx)).resolves.toEqual({ result: 'valid' }); - }; + describe('MinGasLimitsValidator', () => { + const expectValid = async (tx: Tx) => { + await expect(new MinGasLimitsValidator().validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }; - const expectInvalid = async (tx: Tx, reason: string) => { - const result = await new GasLimitsValidator().validateTx(tx); - expect(result.result).toEqual('invalid'); - expect((result as { reason: string[] }).reason[0]).toContain(reason); - }; + const expectInvalid = async (tx: Tx) => { + const result = await new MinGasLimitsValidator().validateTx(tx); + expect(result.result).toEqual('invalid'); + expect((result as { reason: string[] }).reason[0]).toContain(TX_ERROR_INSUFFICIENT_GAS_LIMIT); + }; - it('accepts public tx at exactly the minimum gas limits', async () => { - assert(!!tx.data.forPublic); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), + it('accepts public tx at exactly the minimum gas limits', async () => { + assert(!!tx.data.forPublic); + setGasLimits(tx, new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD)); + await expectValid(tx); }); - await expectValid(tx); - }); - it('accepts private tx at exactly the minimum gas limits', async () => { - const privateTx = await makePrivateTx(gasFees); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), + it('accepts private tx at exactly the minimum gas limits', async () => { + const privateTx = await makePrivateTx(gasFees); + setGasLimits(privateTx, new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD)); + await expectValid(privateTx); }); - await expectValid(privateTx); - }); - it('rejects public tx below the public L2 gas minimum', async () => { - assert(!!tx.data.forPublic); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD - 1), - maxFeesPerGas: gasFees.clone(), + it('rejects public tx below the public L2 gas minimum', async () => { + assert(!!tx.data.forPublic); + setGasLimits(tx, new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD - 1)); + await expectInvalid(tx); }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - it('rejects private tx below the private L2 gas minimum', async () => { - const privateTx = await makePrivateTx(gasFees); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD - 1), - maxFeesPerGas: gasFees.clone(), + it('rejects private tx below the private L2 gas minimum', async () => { + const privateTx = await makePrivateTx(gasFees); + setGasLimits(privateTx, new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD - 1)); + await expectInvalid(privateTx); }); - await expectInvalid(privateTx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - it('rejects public tx at private L2 gas minimum (between the two thresholds)', async () => { - assert(!!tx.data.forPublic); - // PRIVATE_TX_L2_GAS_OVERHEAD is enough for a private tx but not for a public tx. - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), + it('rejects public tx at private L2 gas minimum (between the two thresholds)', async () => { + assert(!!tx.data.forPublic); + // PRIVATE_TX_L2_GAS_OVERHEAD is enough for a private tx but not for a public tx. + setGasLimits(tx, new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD)); + await expectInvalid(tx); }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - it('rejects tx below DA gas minimum', async () => { - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), + it('rejects tx below DA gas minimum', async () => { + setGasLimits(tx, new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD)); + await expectInvalid(tx); }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - it('rejects tx below both DA and L2 gas minimums', async () => { - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD - 1), - maxFeesPerGas: gasFees.clone(), + it('rejects tx below both DA and L2 gas minimums', async () => { + setGasLimits(tx, new Gas(TX_DA_GAS_OVERHEAD - 1, PUBLIC_TX_L2_GAS_OVERHEAD - 1)); + await expectInvalid(tx); }); - await expectInvalid(tx, TX_ERROR_INSUFFICIENT_GAS_LIMIT); - }); - it('rejects public tx if L2 gas limit is too high', async () => { - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + it('ignores limits above the protocol ceiling', async () => { + // The ceiling is owned by MaxGasLimitsValidator, which factories include separately so that the + // estimation exemption cannot take the floor with it. + setGasLimits(tx, new Gas(MAX_TX_DA_GAS + 1, MAX_PROCESSABLE_L2_GAS + 1)); + await expectValid(tx); }); - await expectInvalid(tx, TX_ERROR_GAS_LIMIT_TOO_HIGH); }); - it('rejects private tx if L2 gas limit is too high', async () => { - const privateTx = await makePrivateTx(gasFees); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expectInvalid(privateTx, TX_ERROR_GAS_LIMIT_TOO_HIGH); - }); + describe('MaxGasLimitsValidator', () => { + const expectValid = async (tx: Tx, validator = new MaxGasLimitsValidator()) => { + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }; - describe('network admission limits (maxTxL2Gas, maxTxDAGas)', () => { - it('rejects tx exceeding maxTxL2Gas', async () => { - const maxTxL2Gas = 1_000_000; - const validator = new GasLimitsValidator({ maxTxL2Gas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); + const expectInvalid = async (tx: Tx, validator = new MaxGasLimitsValidator()) => { await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], }); + }; + + it('rejects public tx if L2 gas limit is too high', async () => { + setGasLimits(tx, new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1)); + await expectInvalid(tx); }); - it('accepts tx at exactly maxTxL2Gas', async () => { - const maxTxL2Gas = 1_000_000; - const validator = new GasLimitsValidator({ maxTxL2Gas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + it('rejects private tx if L2 gas limit is too high', async () => { + const privateTx = await makePrivateTx(gasFees); + setGasLimits(privateTx, new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1)); + await expectInvalid(privateTx); }); - it('clamps maxTxL2Gas to the per-tx protocol maximum', async () => { - // Passing a higher network limit cannot raise the ceiling above MAX_PROCESSABLE_L2_GAS. - const validator = new GasLimitsValidator({ maxTxL2Gas: MAX_PROCESSABLE_L2_GAS + 1_000 }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], - }); + it('ignores limits below the protocol minimums', async () => { + // The floor is owned by MinGasLimitsValidator. + setGasLimits(tx, Gas.empty()); + await expectValid(tx); }); - it('falls back to MAX_PROCESSABLE_L2_GAS when no L2 limit is set', async () => { - const validator = new GasLimitsValidator(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + describe('network admission limits (maxTxL2Gas, maxTxDAGas)', () => { + it('rejects tx exceeding maxTxL2Gas', async () => { + const maxTxL2Gas = 1_000_000; + setGasLimits(tx, new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1)); + await expectInvalid(tx, new MaxGasLimitsValidator({ maxTxL2Gas })); }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + + it('accepts tx at exactly maxTxL2Gas', async () => { + const maxTxL2Gas = 1_000_000; + setGasLimits(tx, new Gas(MAX_TX_DA_GAS, maxTxL2Gas)); + await expectValid(tx, new MaxGasLimitsValidator({ maxTxL2Gas })); }); - }); - it('rejects tx exceeding maxTxDAGas', async () => { - const maxTxDAGas = 100_000; - const validator = new GasLimitsValidator({ maxTxDAGas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + it('clamps maxTxL2Gas to the per-tx protocol maximum', async () => { + // Passing a higher network limit cannot raise the ceiling above MAX_PROCESSABLE_L2_GAS. + setGasLimits(tx, new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1)); + await expectInvalid(tx, new MaxGasLimitsValidator({ maxTxL2Gas: MAX_PROCESSABLE_L2_GAS + 1_000 })); }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + + it('falls back to MAX_PROCESSABLE_L2_GAS when no L2 limit is set', async () => { + setGasLimits(tx, new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1)); + await expectInvalid(tx); }); - }); - it('accepts tx at exactly maxTxDAGas', async () => { - const maxTxDAGas = 100_000; - const validator = new GasLimitsValidator({ maxTxDAGas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxTxDAGas, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + it('rejects tx exceeding maxTxDAGas', async () => { + const maxTxDAGas = 100_000; + setGasLimits(tx, new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD)); + await expectInvalid(tx, new MaxGasLimitsValidator({ maxTxDAGas })); }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); - }); - it('caps DA at the max tx blob size when no DA limit is set', async () => { - const validator = new GasLimitsValidator(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS + 1, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + it('accepts tx at exactly maxTxDAGas', async () => { + const maxTxDAGas = 100_000; + setGasLimits(tx, new Gas(maxTxDAGas, PUBLIC_TX_L2_GAS_OVERHEAD)); + await expectValid(tx, new MaxGasLimitsValidator({ maxTxDAGas })); }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + + it('caps DA at the max tx blob size when no DA limit is set', async () => { + setGasLimits(tx, new Gas(MAX_TX_DA_GAS + 1, PUBLIC_TX_L2_GAS_OVERHEAD)); + await expectInvalid(tx); }); - }); - it('accepts a tx at exactly the max tx blob size DA limit when no DA limit is set', async () => { - const validator = new GasLimitsValidator(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, PUBLIC_TX_L2_GAS_OVERHEAD), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), + it('accepts a tx at exactly the max tx blob size DA limit when no DA limit is set', async () => { + setGasLimits(tx, new Gas(MAX_TX_DA_GAS, PUBLIC_TX_L2_GAS_OVERHEAD)); + await expectValid(tx); }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); }); }); }); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.ts index 391ee3f2c48f..b106c4495fa7 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.ts @@ -14,7 +14,10 @@ import { type TxValidator, } from '@aztec/stdlib/tx'; -/** Structural interface for types that carry gas limit data, used by {@link GasLimitsValidator}. */ +/** + * Structural interface for types that carry gas limit data, used by {@link MinGasLimitsValidator} and + * {@link MaxGasLimitsValidator}. + */ export interface HasGasLimitData { txHash: { toString(): string }; data: { @@ -29,43 +32,30 @@ export interface HasGasLimitData { } /** - * Validates that a transaction's gas limits are within acceptable bounds. + * Validates that a transaction declares at least the gas it is guaranteed to be charged. * - * Rejects transactions whose gas limits fall below the fixed minimums (FIXED_DA_GAS, - * FIXED_L2_GAS) or exceed the AVM's maximum processable L2 gas. This is a cheap, - * stateless check that operates on gas settings alone. + * Rejects transactions whose gas limits fall below the fixed protocol overheads + * ({@link TX_DA_GAS_OVERHEAD} and {@link PRIVATE_TX_L2_GAS_OVERHEAD} / {@link PUBLIC_TX_L2_GAS_OVERHEAD}). + * This is a cheap, stateless check that operates on gas settings alone. + * + * The floor is a protocol property with no exemptions: a tx below it can never be mined, so every entry + * point applies it, including gas estimation. * * Generic over T so it can validate both full {@link Tx} objects and {@link TxMetaData} * (used during pending pool migration). - * - * Sole owner of declared gas-limit validation; factories include it explicitly wherever the check applies. */ -export class GasLimitsValidator implements TxValidator { +export class MinGasLimitsValidator implements TxValidator { #log: Logger; - #effectiveMaxL2Gas: number; - #effectiveMaxDAGas: number; - /** - * @param maxTxL2Gas - The network admission limit on L2 gas a single tx may declare (the per-block mana - * allocation, see {@link computeNetworkTxGasLimits}). Defaults to the per-tx protocol maximum, so callers - * that pass nothing (e.g. block building) enforce only the protocol ceiling. - * @param maxTxDAGas - The network admission limit on DA gas a single tx may declare. Defaults to the - * per-tx protocol maximum {@link MAX_TX_DA_GAS}. - */ - constructor(opts?: { maxTxL2Gas?: number; maxTxDAGas?: number; bindings?: LoggerBindings }) { - this.#log = createLogger('sequencer:tx_validator:tx_gas', opts?.bindings); - // The passed limits are network admission limits; clamp to the per-tx protocol maxima as a hard ceiling. - // MAX_TX_DA_GAS bounds DA by what a single tx can actually post to a blob; declaring more is meaningless - // and would let a tx reserve checkpoint/block DA budget during proposal building it can't use. - this.#effectiveMaxL2Gas = Math.min(MAX_PROCESSABLE_L2_GAS, opts?.maxTxL2Gas ?? Infinity); - this.#effectiveMaxDAGas = Math.min(MAX_TX_DA_GAS, opts?.maxTxDAGas ?? Infinity); + constructor(bindings?: LoggerBindings) { + this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings); } validateTx(tx: T): Promise { return Promise.resolve(this.validateGasLimit(tx)); } - /** Checks gas limits are >= fixed minimums and <= effective max gas (L2 and DA). */ + /** Checks gas limits are >= the fixed protocol overheads (L2 and DA). */ validateGasLimit(tx: T): TxValidationResult { const gasLimits = tx.data.constants.txContext.gasSettings.gasLimits; const minGasLimits = new Gas( @@ -86,6 +76,51 @@ export class GasLimitsValidator implements TxValidato }; } + return { result: 'valid' }; + } +} + +/** + * Validates that a transaction does not declare more gas than it is allowed to. + * + * Rejects transactions whose gas limits exceed the per-tx protocol maxima, optionally tightened to the + * network admission limits. This is a cheap, stateless check that operates on gas settings alone. + * + * Unlike the floor, the ceiling is exempted on the gas estimation path, where limits are deliberately + * inflated past what the protocol allows (see {@link GasSettings.forEstimation}). + * + * Generic over T so it can validate both full {@link Tx} objects and {@link TxMetaData} + * (used during pending pool migration). + */ +export class MaxGasLimitsValidator implements TxValidator { + #log: Logger; + #effectiveMaxL2Gas: number; + #effectiveMaxDAGas: number; + + /** + * @param maxTxL2Gas - The network admission limit on L2 gas a single tx may declare (the per-block mana + * allocation, see {@link computeNetworkTxGasLimits}). Defaults to the per-tx protocol maximum, so callers + * that pass nothing (e.g. block building) enforce only the protocol ceiling. + * @param maxTxDAGas - The network admission limit on DA gas a single tx may declare. Defaults to the + * per-tx protocol maximum {@link MAX_TX_DA_GAS}. + */ + constructor(opts?: { maxTxL2Gas?: number; maxTxDAGas?: number; bindings?: LoggerBindings }) { + this.#log = createLogger('sequencer:tx_validator:tx_gas', opts?.bindings); + // The passed limits are network admission limits; clamp to the per-tx protocol maxima as a hard ceiling. + // MAX_TX_DA_GAS bounds DA by what a single tx can actually post to a blob; declaring more is meaningless + // and would let a tx reserve checkpoint/block DA budget during proposal building it can't use. + this.#effectiveMaxL2Gas = Math.min(MAX_PROCESSABLE_L2_GAS, opts?.maxTxL2Gas ?? Infinity); + this.#effectiveMaxDAGas = Math.min(MAX_TX_DA_GAS, opts?.maxTxDAGas ?? Infinity); + } + + validateTx(tx: T): Promise { + return Promise.resolve(this.validateGasLimit(tx)); + } + + /** Checks gas limits are <= the effective max gas (L2 and DA). */ + validateGasLimit(tx: T): TxValidationResult { + const gasLimits = tx.data.constants.txContext.gasSettings.gasLimits; + if (gasLimits.l2Gas > this.#effectiveMaxL2Gas) { this.#log.verbose(`Rejecting transaction due to the L2 gas limit being higher than the effective maximum`, { gasLimits, 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 94c4538873ee..51a472f106df 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 @@ -103,9 +103,10 @@ describe('GasTxValidator', () => { await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PAYER_BALANCE); }); - it('does not enforce gas limits, which are owned by GasLimitsValidator', async () => { + it('does not enforce gas limits, which are owned by the gas limits validators', async () => { // Gas estimation submits limits above the per-tx protocol maximum (GasSettings.forEstimation); whether they - // are admissible is decided by GasLimitsValidator wherever a factory includes it, never by fee enforcement. + // are admissible is decided by MinGasLimitsValidator/MaxGasLimitsValidator wherever a factory includes them, + // never by fee enforcement. tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS * 2), maxFeesPerGas: gasFees.clone(), 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 4faec1385448..a8688a02bc14 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 @@ -81,8 +81,9 @@ export class MaxFeePerGasValidator implements TxV * 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). * - * Gas limits are deliberately not checked here: they are owned by {@link GasLimitsValidator}, which factories - * include separately so that exemptions (e.g. gas estimation) don't change fee enforcement. + * Gas limits are deliberately not checked here: they are owned by {@link MinGasLimitsValidator} and + * {@link MaxGasLimitsValidator}, which factories include separately so that exemptions (e.g. gas estimation) + * don't change fee enforcement. * * Used by: gossip (stage 1), RPC, and block building validators. */ @@ -90,22 +91,22 @@ export class GasTxValidator implements TxValidator { #log: Logger; #publicDataSource: PublicStateSource; #feeJuiceAddress: AztecAddress; - #gasFees: GasFees; + #maxFeePerGasValidator: MaxFeePerGasValidator; constructor( publicDataSource: PublicStateSource, feeJuiceAddress: AztecAddress, gasFees: GasFees, - private bindings?: LoggerBindings, + bindings?: LoggerBindings, ) { this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings); this.#publicDataSource = publicDataSource; this.#feeJuiceAddress = feeJuiceAddress; - this.#gasFees = gasFees; + this.#maxFeePerGasValidator = new MaxFeePerGasValidator(gasFees, bindings); } async validateTx(tx: Tx): Promise { - const maxFeeValidation = new MaxFeePerGasValidator(this.#gasFees, this.bindings).validateMaxFeePerGas(tx); + const maxFeeValidation = this.#maxFeePerGasValidator.validateMaxFeePerGas(tx); if (maxFeeValidation.result === 'invalid') { return maxFeeValidation; } diff --git a/yarn-project/validator-client/README.md b/yarn-project/validator-client/README.md index 06afb9a0328d..b10e5ceea63e 100644 --- a/yarn-project/validator-client/README.md +++ b/yarn-project/validator-client/README.md @@ -246,7 +246,7 @@ Per-block budgets prevent one block from consuming the entire checkpoint budget. ### Per-transaction enforcement -**Mempool entry** (`GasLimitsValidator`): L2 gas must be ≤ `MAX_PROCESSABLE_L2_GAS` (6,540,000) and ≥ fixed minimums. +**Mempool entry** (`MaxGasLimitsValidator` / `MinGasLimitsValidator`): L2 gas must be ≤ `MAX_PROCESSABLE_L2_GAS` (6,540,000) and ≥ fixed minimums. **Block building** (`PublicProcessor.process`): Before processing, txs are skipped if their estimated blob fields or gas limits would exceed the block budget. After processing, actual values are checked and the tx is reverted if limits are exceeded. diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index 068cfd13c69a..a9b544539cb6 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -315,7 +315,7 @@ export abstract class BaseWallet implements Wallet { } else { const maxTxGasLimits = await this.getMaxTxGasLimits(); // If the caller declared explicit gas limits, reject them up front when they exceed the network's - // per-tx admission limit (mirroring the node's GasLimitsValidator). Otherwise fill in the limit. + // per-tx admission limit (mirroring the node's MaxGasLimitsValidator). Otherwise fill in the limit. if (gasSettingsOverrides.gasLimits) { assertGasLimitsWithinNetworkLimits(gasSettingsOverrides.gasLimits, maxTxGasLimits); } From cf3aa45998dcff658a26ff568891ce3b5390f716 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Tue, 18 Aug 2026 15:24:28 +0100 Subject: [PATCH 10/10] cleanup --- .../tx_validator/gas_limits_validator.test.ts | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts index 51f823559bff..317104d6a393 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_limits_validator.test.ts @@ -13,35 +13,27 @@ import assert from 'assert'; import { MaxGasLimitsValidator, MinGasLimitsValidator } from './gas_limits_validator.js'; -const DEFAULT_GAS_LIMITS = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); +/** Sets the gas limits under test. These validators read nothing else, so the fees are arbitrary. */ +const setGasLimits = (tx: Tx, gasLimits: Gas) => { + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits, maxFeesPerGas: new GasFees(11, 22) }); +}; -/** A tx with no public calls, carrying the default gas limits. */ -const makePrivateTx = async (gasFees: GasFees) => { +/** A tx with no public calls. */ +const makePrivateTx = async () => { const privateTx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 0, numberOfRevertiblePublicCallRequests: 0, hasPublicTeardownCallRequest: false, }); assert(!privateTx.data.forPublic); - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: DEFAULT_GAS_LIMITS, - maxFeesPerGas: gasFees.clone(), - }); return privateTx; }; describe('gas limits validators', () => { - let gasFees: GasFees; let tx: Tx; - const setGasLimits = (tx: Tx, gasLimits: Gas) => { - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits, maxFeesPerGas: gasFees.clone() }); - }; - beforeEach(async () => { - gasFees = new GasFees(11, 22); tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); - setGasLimits(tx, DEFAULT_GAS_LIMITS); }); describe('MinGasLimitsValidator', () => { @@ -62,7 +54,7 @@ describe('gas limits validators', () => { }); it('accepts private tx at exactly the minimum gas limits', async () => { - const privateTx = await makePrivateTx(gasFees); + const privateTx = await makePrivateTx(); setGasLimits(privateTx, new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD)); await expectValid(privateTx); }); @@ -74,7 +66,7 @@ describe('gas limits validators', () => { }); it('rejects private tx below the private L2 gas minimum', async () => { - const privateTx = await makePrivateTx(gasFees); + const privateTx = await makePrivateTx(); setGasLimits(privateTx, new Gas(TX_DA_GAS_OVERHEAD, PRIVATE_TX_L2_GAS_OVERHEAD - 1)); await expectInvalid(privateTx); }); @@ -122,7 +114,7 @@ describe('gas limits validators', () => { }); it('rejects private tx if L2 gas limit is too high', async () => { - const privateTx = await makePrivateTx(gasFees); + const privateTx = await makePrivateTx(); setGasLimits(privateTx, new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1)); await expectInvalid(privateTx); }); @@ -134,14 +126,16 @@ describe('gas limits validators', () => { }); describe('network admission limits (maxTxL2Gas, maxTxDAGas)', () => { + // Network limits are deployment config; any value below the protocol ceiling stands in for one here. + const maxTxL2Gas = Math.floor(MAX_PROCESSABLE_L2_GAS / 2); + const maxTxDAGas = Math.floor(MAX_TX_DA_GAS / 2); + it('rejects tx exceeding maxTxL2Gas', async () => { - const maxTxL2Gas = 1_000_000; setGasLimits(tx, new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1)); await expectInvalid(tx, new MaxGasLimitsValidator({ maxTxL2Gas })); }); it('accepts tx at exactly maxTxL2Gas', async () => { - const maxTxL2Gas = 1_000_000; setGasLimits(tx, new Gas(MAX_TX_DA_GAS, maxTxL2Gas)); await expectValid(tx, new MaxGasLimitsValidator({ maxTxL2Gas })); }); @@ -158,13 +152,11 @@ describe('gas limits validators', () => { }); it('rejects tx exceeding maxTxDAGas', async () => { - const maxTxDAGas = 100_000; setGasLimits(tx, new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD)); await expectInvalid(tx, new MaxGasLimitsValidator({ maxTxDAGas })); }); it('accepts tx at exactly maxTxDAGas', async () => { - const maxTxDAGas = 100_000; setGasLimits(tx, new Gas(maxTxDAGas, PUBLIC_TX_L2_GAS_OVERHEAD)); await expectValid(tx, new MaxGasLimitsValidator({ maxTxDAGas })); });