-
Notifications
You must be signed in to change notification settings - Fork 614
fix(p2p): move gas-limit admission out of GasTxValidator #25221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
65a5883
8cb7e27
9129c85
7d39935
a50e02c
f0340fb
b7d8cb2
340adb5
92939d8
69061c7
3f304e5
cf3aa45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,16 @@ | ||
| 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'; | ||
| 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'; | ||
|
|
||
|
|
@@ -26,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'; | ||
|
|
@@ -39,6 +42,17 @@ function getValidatorNames(aggregate: AggregateTxValidator<unknown>): 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<WorldStateSynchronizer>; | ||
| let contractSource: MockProxy<ContractDataSource>; | ||
|
|
@@ -72,12 +86,68 @@ 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 = Math.floor(MAX_PROCESSABLE_L2_GAS / 2); | ||
| 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('forwards the network DA admission limit to the gas limits validator', async () => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above but for |
||
| const maxTxDAGas = Math.floor(MAX_TX_DA_GAS / 2); | ||
| 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, MAX_PROCESSABLE_L2_GAS), | ||
| 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 +186,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); | ||
| }); | ||
|
|
@@ -245,14 +316,14 @@ describe('Validator factory functions', () => { | |
|
|
||
| const aggregate = validator as AggregateTxValidator<unknown>; | ||
| 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, | ||
|
|
@@ -270,6 +341,42 @@ describe('Validator factory functions', () => { | |
| expect(getValidatorNames(aggregate)).not.toContain(GasLimitsValidator.name); | ||
| }); | ||
|
|
||
| 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', () => { | ||
| const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { | ||
| l1ChainId: 1, | ||
|
|
@@ -309,10 +416,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 () => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before this fix, block building got the ceiling implicitly through the check embedded in GasTxValidator. Since the fix removes that nested check, this test pins that block building didn't silently lose the ceiling. |
||
| // 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, []); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -86,7 +87,8 @@ 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. | ||
| */ | ||
| export function createFirstStageTxValidationsForGossipedTransactions( | ||
| timestamp: UInt64, | ||
|
|
@@ -156,13 +158,16 @@ 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<Tx>({ ...gasLimitOpts, bindings }), | ||
| severity: PeerErrorSeverity.MidToleranceError, | ||
| }, | ||
| gasValidator: { | ||
| validator: new GasTxValidator( | ||
| new DatabasePublicStateSource(merkleTree), | ||
| ProtocolContractAddress.FeeJuice, | ||
| gasFees, | ||
| bindings, | ||
| gasLimitOpts, | ||
| ), | ||
| severity: PeerErrorSeverity.MidToleranceError, | ||
| }, | ||
|
|
@@ -343,8 +348,7 @@ export function createTxValidatorForAcceptingTxsOverRPC( | |
| // 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. | ||
| // `skipFeeEnforcement`, and GasTxValidator does not re-run this same check. | ||
| if (!isSimulation) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I made a red-green to confirm. I split into |
||
| validators.push(new GasLimitsValidator<Tx>({ maxTxL2Gas, maxTxDAGas, bindings })); | ||
| } | ||
|
|
@@ -416,6 +420,9 @@ function createTxValidatorForValidatingAgainstCurrentState( | |
| new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp, bindings), | ||
| new BlockHeaderTxValidator(archiveSource, bindings), | ||
| new DoubleSpendTxValidator(nullifierSource, 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<Tx>({ bindings }), | ||
| new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees, bindings), | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This replaces
forwards L2 limits through GasTxValidatorwhich was removed fromgas_validator.test.ts