From 684b5d066400dadd2a6d0b850fa4b93603a4ad2c Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Tue, 9 Apr 2024 15:51:05 +0000 Subject: [PATCH] feat: tx_validator checks fn selectors --- .../circuit-types/src/interfaces/configs.ts | 25 +- yarn-project/circuit-types/src/mocks.ts | 10 +- .../src/benchmarks/bench_tx_size_fees.test.ts | 24 +- .../src/e2e_dapp_subscription.test.ts | 23 +- yarn-project/end-to-end/src/e2e_fees.test.ts | 30 +- .../src/client/sequencer-client.ts | 2 +- yarn-project/sequencer-client/src/config.ts | 33 +- .../src/sequencer/abstract_phase_manager.ts | 15 +- .../src/sequencer/public_processor.test.ts | 4 +- .../src/sequencer/public_processor.ts | 4 +- .../src/sequencer/sequencer.test.ts | 5 +- .../src/sequencer/sequencer.ts | 37 +- .../src/sequencer/tx_validator.test.ts | 325 ------------------ .../src/sequencer/tx_validator.ts | 265 -------------- .../src/sequencer/tx_validator_factory.ts | 32 -- .../aggregate_tx_validator.test.ts | 30 ++ .../tx_validator/aggregate_tx_validator.ts | 26 ++ .../double_spend_validator.test.ts | 54 +++ .../tx_validator/double_spend_validator.ts | 67 ++++ .../src/tx_validator/gas_validator.test.ts | 69 ++++ .../src/tx_validator/gas_validator.ts | 85 +++++ .../tx_validator/metadata_validator.test.ts | 57 +++ .../src/tx_validator/metadata_validator.ts | 62 ++++ .../src/tx_validator/phases_validator.test.ts | 194 +++++++++++ .../src/tx_validator/phases_validator.ts | 103 ++++++ .../src/tx_validator/tx_validator.ts | 7 + .../src/tx_validator/tx_validator_factory.ts | 38 ++ 27 files changed, 952 insertions(+), 674 deletions(-) delete mode 100644 yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts delete mode 100644 yarn-project/sequencer-client/src/sequencer/tx_validator.ts delete mode 100644 yarn-project/sequencer-client/src/sequencer/tx_validator_factory.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/aggregate_tx_validator.test.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/aggregate_tx_validator.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/double_spend_validator.test.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/double_spend_validator.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/gas_validator.test.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/gas_validator.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/metadata_validator.test.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/metadata_validator.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/phases_validator.test.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/phases_validator.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/tx_validator.ts create mode 100644 yarn-project/sequencer-client/src/tx_validator/tx_validator_factory.ts diff --git a/yarn-project/circuit-types/src/interfaces/configs.ts b/yarn-project/circuit-types/src/interfaces/configs.ts index ee90be579d2b..2698d7aeb78f 100644 --- a/yarn-project/circuit-types/src/interfaces/configs.ts +++ b/yarn-project/circuit-types/src/interfaces/configs.ts @@ -1,4 +1,19 @@ -import { type AztecAddress, type EthAddress, type Fr } from '@aztec/circuits.js'; +import { type AztecAddress, type EthAddress, type Fr, type FunctionSelector } from '@aztec/circuits.js'; + +/** A function that the sequencer allows to run in either setup or teardown phase */ +export type AllowedFunction = + | { + /** The contract address this selector is valid for */ + address: AztecAddress; + /** The function selector */ + selector: FunctionSelector; + } + | { + /** The contract class this selector is valid for */ + classId: Fr; + /** The function selector */ + selector: FunctionSelector; + }; /** * The sequencer configuration. @@ -19,9 +34,9 @@ export interface SequencerConfig { /** The path to the ACVM binary */ acvmBinaryPath?: string; - /** The list of permitted fee payment contract classes */ - allowedFeePaymentContractClasses?: Fr[]; + /** The list of functions calls allowed to run in setup */ + allowedFunctionsInSetup?: AllowedFunction[]; - /** The list of permitted fee payment contract instances. Takes precedence over contract classes */ - allowedFeePaymentContractInstances?: AztecAddress[]; + /** The list of functions calls allowed to run teardown */ + allowedFunctionsInTeardown?: AllowedFunction[]; } diff --git a/yarn-project/circuit-types/src/mocks.ts b/yarn-project/circuit-types/src/mocks.ts index 6b742f38a8e8..3f6c48c134f3 100644 --- a/yarn-project/circuit-types/src/mocks.ts +++ b/yarn-project/circuit-types/src/mocks.ts @@ -57,13 +57,13 @@ export const mockTx = ( data.forPublic.endNonRevertibleData.newNullifiers[0] = firstNullifier; - data.forPublic.endNonRevertibleData.publicCallStack = makeTuple(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, i => - i < numberOfNonRevertiblePublicCallRequests ? publicCallRequests[i].toCallRequest() : CallRequest.empty(), + data.forPublic.end.publicCallStack = makeTuple(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, i => + i < numberOfRevertiblePublicCallRequests ? publicCallRequests[i].toCallRequest() : CallRequest.empty(), ); - data.forPublic.end.publicCallStack = makeTuple(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, i => - i < numberOfRevertiblePublicCallRequests - ? publicCallRequests[i + numberOfNonRevertiblePublicCallRequests].toCallRequest() + data.forPublic.endNonRevertibleData.publicCallStack = makeTuple(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, i => + i < numberOfNonRevertiblePublicCallRequests + ? publicCallRequests[i + numberOfRevertiblePublicCallRequests].toCallRequest() : CallRequest.empty(), ); } else { diff --git a/yarn-project/end-to-end/src/benchmarks/bench_tx_size_fees.test.ts b/yarn-project/end-to-end/src/benchmarks/bench_tx_size_fees.test.ts index c79c504aa452..0c4a800526d0 100644 --- a/yarn-project/end-to-end/src/benchmarks/bench_tx_size_fees.test.ts +++ b/yarn-project/end-to-end/src/benchmarks/bench_tx_size_fees.test.ts @@ -3,13 +3,14 @@ import { type AztecAddress, type EthAddress, type FeePaymentMethod, + FunctionSelector, NativeFeePaymentMethod, PrivateFeePaymentMethod, PublicFeePaymentMethod, TxStatus, getContractClassFromArtifact, } from '@aztec/aztec.js'; -import { FPCContract, GasTokenContract, TokenContract } from '@aztec/noir-contracts.js'; +import { FPCContract, GasTokenContract, SchnorrAccountContractArtifact, TokenContract } from '@aztec/noir-contracts.js'; import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; import { jest } from '@jest/globals'; @@ -39,7 +40,26 @@ describe('benchmarks/tx_size_fees', () => { await aztecNode.setConfig({ feeRecipient: sequencerAddress, - allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], + allowedFunctionsInSetup: [ + { + classId: getContractClassFromArtifact(SchnorrAccountContractArtifact).id, + selector: FunctionSelector.fromSignature('approve_public_authwit(Field'), + }, + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('prepare_fee((Field),Field,(Field),Field)'), + }, + ], + allowedFunctionsInTeardown: [ + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee((Field),Field,(Field))'), + }, + { + classId: getContractClassFromArtifact(GasTokenContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee(Field)'), + }, + ], }); await publicDeployAccounts(aliceWallet, wallets); diff --git a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts index 90348fa8d28b..e58a69bd0201 100644 --- a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts +++ b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts @@ -6,6 +6,7 @@ import { type DeployL1Contracts, type FeePaymentMethod, Fr, + FunctionSelector, type PXE, PrivateFeePaymentMethod, PublicFeePaymentMethod, @@ -19,6 +20,7 @@ import { CounterContract, FPCContract, GasTokenContract, + SchnorrAccountContractArtifact, } from '@aztec/noir-contracts.js'; import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; @@ -71,7 +73,26 @@ describe('e2e_dapp_subscription', () => { await publicDeployAccounts(wallets[0], wallets); await aztecNode.setConfig({ - allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], + allowedFunctionsInSetup: [ + { + classId: getContractClassFromArtifact(SchnorrAccountContractArtifact).id, + selector: FunctionSelector.fromSignature('approve_public_authwit(Field'), + }, + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('prepare_fee((Field),Field,(Field),Field)'), + }, + ], + allowedFunctionsInTeardown: [ + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee((Field),Field,(Field))'), + }, + { + classId: getContractClassFromArtifact(GasTokenContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee(Field)'), + }, + ], }); // this should be a SignerlessWallet but that can't call public functions directly diff --git a/yarn-project/end-to-end/src/e2e_fees.test.ts b/yarn-project/end-to-end/src/e2e_fees.test.ts index 7aed8e282ebf..cfe3aed875d3 100644 --- a/yarn-project/end-to-end/src/e2e_fees.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees.test.ts @@ -23,6 +23,7 @@ import { FPCContract, GasTokenContract, SchnorrAccountContract, + SchnorrAccountContractArtifact, } from '@aztec/noir-contracts.js'; import { jest } from '@jest/globals'; @@ -58,7 +59,34 @@ describe('e2e_fees', () => { wallets = _wallets; await aztecNode.setConfig({ - allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], + allowedFunctionsInSetup: [ + { + classId: getContractClassFromArtifact(SchnorrAccountContractArtifact).id, + selector: FunctionSelector.fromSignature('approve_public_authwit(Field)'), + }, + { + classId: getContractClassFromArtifact(BananaCoin.artifact).id, + selector: FunctionSelector.fromSignature('_increase_public_balance((Field),Field)'), + }, + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('prepare_fee((Field),Field,(Field),Field)'), + }, + ], + allowedFunctionsInTeardown: [ + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee((Field),Field,(Field))'), + }, + { + classId: getContractClassFromArtifact(GasTokenContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee(Field)'), + }, + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee_with_shielded_rebate(Field,(Field),Field)'), + }, + ], }); logFunctionSignatures(BananaCoin.artifact, logger); diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index 3cbf36567845..24d9cbd99f68 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -10,7 +10,7 @@ import { getGlobalVariableBuilder } from '../global_variable_builder/index.js'; import { getL1Publisher } from '../publisher/index.js'; import { Sequencer, type SequencerConfig } from '../sequencer/index.js'; import { PublicProcessorFactory } from '../sequencer/public_processor.js'; -import { TxValidatorFactory } from '../sequencer/tx_validator_factory.js'; +import { TxValidatorFactory } from '../tx_validator/tx_validator_factory.js'; /** * Encapsulates the full sequencer and publisher. diff --git a/yarn-project/sequencer-client/src/config.ts b/yarn-project/sequencer-client/src/config.ts index b86863b9393d..e8bd1e399bf4 100644 --- a/yarn-project/sequencer-client/src/config.ts +++ b/yarn-project/sequencer-client/src/config.ts @@ -1,4 +1,5 @@ -import { AztecAddress, Fr } from '@aztec/circuits.js'; +import { type AllowedFunction } from '@aztec/circuit-types'; +import { AztecAddress, Fr, FunctionSelector } from '@aztec/circuits.js'; import { type L1ContractAddresses, NULL_KEY } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -40,8 +41,8 @@ export function getConfigEnvVars(): SequencerClientConfig { SEQ_TX_POLLING_INTERVAL_MS, SEQ_MAX_TX_PER_BLOCK, SEQ_MIN_TX_PER_BLOCK, - SEQ_FPC_CLASSES, - SEQ_FPC_INSTANCES, + SEQ_ALLOWED_SETUP_FN, + SEQ_ALLOWED_TEARDOWN_FN, AVAILABILITY_ORACLE_CONTRACT_ADDRESS, ROLLUP_CONTRACT_ADDRESS, REGISTRY_CONTRACT_ADDRESS, @@ -90,9 +91,27 @@ export function getConfigEnvVars(): SequencerClientConfig { feeRecipient: FEE_RECIPIENT ? AztecAddress.fromString(FEE_RECIPIENT) : undefined, acvmWorkingDirectory: ACVM_WORKING_DIRECTORY ? ACVM_WORKING_DIRECTORY : undefined, acvmBinaryPath: ACVM_BINARY_PATH ? ACVM_BINARY_PATH : undefined, - allowedFeePaymentContractClasses: SEQ_FPC_CLASSES ? SEQ_FPC_CLASSES.split(',').map(Fr.fromString) : [], - allowedFeePaymentContractInstances: SEQ_FPC_INSTANCES - ? SEQ_FPC_INSTANCES.split(',').map(AztecAddress.fromString) - : [], + allowedFunctionsInSetup: parseSequencerAllowList(SEQ_ALLOWED_SETUP_FN ?? ''), + allowedFunctionsInTeardown: parseSequencerAllowList(SEQ_ALLOWED_TEARDOWN_FN ?? ''), }; } + +function parseSequencerAllowList(value: string): AllowedFunction[] { + const entries: AllowedFunction[] = []; + + if (!value) { + return entries; + } + + for (const val of value.split(',')) { + const [identifierString, selectorString] = val.split(':'); + const selector = FunctionSelector.fromString(selectorString); + const identifier = identifierString.startsWith('0x') + ? AztecAddress.fromString(identifierString) + : Fr.fromString(identifierString); + + entries.push({ address: identifier, selector }); + } + + return entries; +} diff --git a/yarn-project/sequencer-client/src/sequencer/abstract_phase_manager.ts b/yarn-project/sequencer-client/src/sequencer/abstract_phase_manager.ts index fa1cbebee4cf..fe266b476f87 100644 --- a/yarn-project/sequencer-client/src/sequencer/abstract_phase_manager.ts +++ b/yarn-project/sequencer-client/src/sequencer/abstract_phase_manager.ts @@ -127,11 +127,18 @@ export abstract class AbstractPhaseManager { publicInputs: PrivateKernelTailCircuitPublicInputs, enqueuedPublicFunctionCalls: PublicCallRequest[], ): Record { + const data = publicInputs.forPublic; + if (!data) { + return { + [PublicKernelPhase.SETUP]: [], + [PublicKernelPhase.APP_LOGIC]: [], + [PublicKernelPhase.TEARDOWN]: [], + [PublicKernelPhase.TAIL]: [], + }; + } const publicCallsStack = enqueuedPublicFunctionCalls.slice().reverse(); - const nonRevertibleCallStack = publicInputs.forPublic!.endNonRevertibleData.publicCallStack.filter( - i => !i.isEmpty(), - ); - const revertibleCallStack = publicInputs.forPublic!.end.publicCallStack.filter(i => !i.isEmpty()); + const nonRevertibleCallStack = data.endNonRevertibleData.publicCallStack.filter(i => !i.isEmpty()); + const revertibleCallStack = data.end.publicCallStack.filter(i => !i.isEmpty()); const callRequestsStack = publicCallsStack .map(call => call.toCallRequest()) diff --git a/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts b/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts index 0e572d0a1f97..d85bf63a05c7 100644 --- a/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts @@ -51,8 +51,8 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import { type PublicKernelCircuitSimulator } from '../simulator/index.js'; import { type ContractsDataSourcePublicDB, type WorldStatePublicDB } from '../simulator/public_executor.js'; import { RealPublicKernelCircuitSimulator } from '../simulator/public_kernel.js'; +import { type TxValidator } from '../tx_validator/tx_validator.js'; import { PublicProcessor } from './public_processor.js'; -import { type TxValidator } from './tx_validator.js'; describe('public_processor', () => { let db: MockProxy; @@ -276,7 +276,7 @@ describe('public_processor', () => { throw new Error(`Unexpected execution request: ${execution}`); }); - const txValidator: MockProxy = mock(); + const txValidator: MockProxy> = mock(); txValidator.validateTxs.mockRejectedValue([[], [tx]]); const [processed, failed] = await processor.process([tx], 1, prover, txValidator); diff --git a/yarn-project/sequencer-client/src/sequencer/public_processor.ts b/yarn-project/sequencer-client/src/sequencer/public_processor.ts index 132b69d476f0..ce68916309bc 100644 --- a/yarn-project/sequencer-client/src/sequencer/public_processor.ts +++ b/yarn-project/sequencer-client/src/sequencer/public_processor.ts @@ -21,9 +21,9 @@ import { type MerkleTreeOperations } from '@aztec/world-state'; import { type PublicKernelCircuitSimulator } from '../simulator/index.js'; import { ContractsDataSourcePublicDB, WorldStateDB, WorldStatePublicDB } from '../simulator/public_executor.js'; import { RealPublicKernelCircuitSimulator } from '../simulator/public_kernel.js'; +import { type TxValidator } from '../tx_validator/tx_validator.js'; import { type AbstractPhaseManager, PublicKernelPhase } from './abstract_phase_manager.js'; import { PhaseManagerFactory } from './phase_manager_factory.js'; -import { type TxValidator } from './tx_validator.js'; /** * Creates new instances of PublicProcessor given the provided merkle tree db and contract data source. @@ -90,7 +90,7 @@ export class PublicProcessor { txs: Tx[], maxTransactions = txs.length, blockProver?: BlockProver, - txValidator?: TxValidator, + txValidator?: TxValidator, ): Promise<[ProcessedTx[], FailedTx[], ProcessReturnValues[]]> { // The processor modifies the tx objects in place, so we need to clone them. txs = txs.map(tx => Tx.clone(tx)); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 76d6fb6a0501..447a07ec2488 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -29,9 +29,9 @@ import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js'; import { type L1Publisher } from '../index.js'; +import { TxValidatorFactory } from '../tx_validator/tx_validator_factory.js'; import { type PublicProcessor, type PublicProcessorFactory } from './public_processor.js'; import { Sequencer } from './sequencer.js'; -import { TxValidatorFactory } from './tx_validator_factory.js'; describe('sequencer', () => { let publisher: MockProxy; @@ -109,9 +109,6 @@ describe('sequencer', () => { l1ToL2MessageSource, publicProcessorFactory, new TxValidatorFactory(merkleTreeOps, contractSource, EthAddress.random()), - { - allowedFeePaymentContractClasses: [fpcClassId], - }, ); }); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 2efab40df55a..0c7ffdd01dea 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -1,5 +1,5 @@ import { type L1ToL2MessageSource, type L2Block, type L2BlockSource, type ProcessedTx, Tx } from '@aztec/circuit-types'; -import { type BlockProver, PROVING_STATUS } from '@aztec/circuit-types/interfaces'; +import { type AllowedFunction, type BlockProver, PROVING_STATUS } from '@aztec/circuit-types/interfaces'; import { type L2BlockBuiltStats } from '@aztec/circuit-types/stats'; import { AztecAddress, EthAddress } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; @@ -11,10 +11,10 @@ import { type WorldStateStatus, type WorldStateSynchronizer } from '@aztec/world import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js'; import { type L1Publisher } from '../publisher/l1-publisher.js'; +import { type TxValidator } from '../tx_validator/tx_validator.js'; +import { type TxValidatorFactory } from '../tx_validator/tx_validator_factory.js'; import { type SequencerConfig } from './config.js'; import { type PublicProcessorFactory } from './public_processor.js'; -import { type TxValidator } from './tx_validator.js'; -import { type TxValidatorFactory } from './tx_validator_factory.js'; /** * Sequencer client @@ -35,8 +35,8 @@ export class Sequencer { private _feeRecipient = AztecAddress.ZERO; private lastPublishedBlock = 0; private state = SequencerState.STOPPED; - private allowedFeePaymentContractClasses: Fr[] = []; - private allowedFeePaymentContractInstances: AztecAddress[] = []; + private allowedFunctionsInSetup: AllowedFunction[] = []; + private allowedFunctionsInTeardown: AllowedFunction[] = []; constructor( private publisher: L1Publisher, @@ -75,11 +75,11 @@ export class Sequencer { if (config.feeRecipient) { this._feeRecipient = config.feeRecipient; } - if (config.allowedFeePaymentContractClasses) { - this.allowedFeePaymentContractClasses = config.allowedFeePaymentContractClasses; + if (config.allowedFunctionsInSetup) { + this.allowedFunctionsInSetup = config.allowedFunctionsInSetup; } - if (config.allowedFeePaymentContractInstances) { - this.allowedFeePaymentContractInstances = config.allowedFeePaymentContractInstances; + if (config.allowedFunctionsInTeardown) { + this.allowedFunctionsInTeardown = config.allowedFunctionsInTeardown; } } @@ -178,14 +178,15 @@ export class Sequencer { this._feeRecipient, ); - const txValidator = this.txValidatorFactory.buildTxValidator( - newGlobalVariables, - this.allowedFeePaymentContractClasses, - this.allowedFeePaymentContractInstances, - ); - // TODO: It should be responsibility of the P2P layer to validate txs before passing them on here - const validTxs = await this.takeValidTxs(pendingTxs, txValidator); + const validTxs = await this.takeValidTxs( + pendingTxs, + this.txValidatorFactory.validatorForNewTxs( + newGlobalVariables, + this.allowedFunctionsInSetup, + this.allowedFunctionsInTeardown, + ), + ); if (validTxs.length < this.minTxsPerBLock) { return; } @@ -213,7 +214,7 @@ export class Sequencer { const blockTicket = await this.prover.startNewBlock(blockSize, newGlobalVariables, l1ToL2Messages, emptyTx); const [publicProcessorDuration, [processedTxs, failedTxs]] = await elapsed(() => - processor.process(validTxs, blockSize, this.prover, txValidator), + processor.process(validTxs, blockSize, this.prover, this.txValidatorFactory.validatorForProcessedTxs()), ); if (failedTxs.length > 0) { const failedTxData = failedTxs.map(fail => fail.tx); @@ -281,7 +282,7 @@ export class Sequencer { } } - protected async takeValidTxs(txs: T[], validator: TxValidator): Promise { + protected async takeValidTxs(txs: T[], validator: TxValidator): Promise { const [valid, invalid] = await validator.validateTxs(txs); if (invalid.length > 0) { this.log.debug(`Dropping invalid txs from the p2p pool ${Tx.getHashes(invalid).join(', ')}`); diff --git a/yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts b/yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts deleted file mode 100644 index ee8bd7c42123..000000000000 --- a/yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { mockTx, mockTxForRollup } from '@aztec/circuit-types'; -import { - type AztecAddress, - CallContext, - EthAddress, - Fr, - FunctionData, - FunctionSelector, - type GlobalVariables, - PublicCallRequest, -} from '@aztec/circuits.js'; -import { makeAztecAddress, makeGlobalVariables } from '@aztec/circuits.js/testing'; -import { pedersenHash } from '@aztec/foundation/crypto'; -import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; -import { type ContractDataSource } from '@aztec/types/contracts'; - -import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; - -import { type NullifierSource, type PublicStateSource, TxValidator } from './tx_validator.js'; - -describe('TxValidator', () => { - let validator: TxValidator; - let globalVariables: GlobalVariables; - let nullifierSource: MockProxy; - let publicStateSource: MockProxy; - let contractDataSource: MockProxy; - let allowedFPCClass: Fr; - let allowedFPC: AztecAddress; - let gasPortalAddress: EthAddress; - let gasTokenAddress: AztecAddress; - - beforeEach(() => { - gasPortalAddress = EthAddress.random(); - gasTokenAddress = getCanonicalGasTokenAddress(gasPortalAddress); - allowedFPCClass = Fr.random(); - allowedFPC = makeAztecAddress(100); - - nullifierSource = mock({ - getNullifierIndex: mockFn().mockImplementation(() => { - return Promise.resolve(undefined); - }), - }); - publicStateSource = mock({ - storageRead: mockFn().mockImplementation((contractAddress: AztecAddress, _slot: Fr) => { - if (contractAddress.equals(gasTokenAddress)) { - return Promise.resolve(new Fr(1)); - } else { - return Promise.reject(Fr.ZERO); - } - }), - }); - contractDataSource = mock({ - getContract: mockFn().mockImplementation(() => { - return Promise.resolve({ - contractClassId: allowedFPCClass, - }); - }), - }); - - globalVariables = makeGlobalVariables(); - validator = new TxValidator(nullifierSource, publicStateSource, contractDataSource, globalVariables, { - allowedFeePaymentContractClasses: [allowedFPCClass], - allowedFeePaymentContractInstances: [allowedFPC], - gasPortalAddress, - }); - }); - - describe('inspects tx metadata', () => { - it('allows only transactions for the right chain', async () => { - const goodTx = nonFeePayingTx(); - const badTx = nonFeePayingTx(); - badTx.data.constants.txContext.chainId = Fr.random(); - - await expect(validator.validateTxs([goodTx, badTx])).resolves.toEqual([[goodTx], [badTx]]); - }); - }); - - describe('inspects tx nullifiers', () => { - it('rejects duplicates in non revertible data', async () => { - const badTx = nonFeePayingTx(); - badTx.data.forRollup!.end.newNullifiers[1] = badTx.data.forRollup!.end.newNullifiers[0]; - await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); - }); - - it('rejects duplicates in revertible data', async () => { - const badTx = nonFeePayingTx(); - badTx.data.forRollup!.end.newNullifiers[1] = badTx.data.forRollup!.end.newNullifiers[0]; - await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); - }); - - it('rejects duplicates across phases', async () => { - const badTx = nativeFeePayingTx(makeAztecAddress()); - badTx.data.forPublic!.end.newNullifiers[0] = badTx.data.forPublic!.endNonRevertibleData.newNullifiers[0]; - await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); - }); - - it('rejects duplicates across txs', async () => { - const firstTx = nonFeePayingTx(); - const secondTx = nonFeePayingTx(); - secondTx.data.forRollup!.end.newNullifiers[0] = firstTx.data.forRollup!.end.newNullifiers[0]; - await expect(validator.validateTxs([firstTx, secondTx])).resolves.toEqual([[firstTx], [secondTx]]); - }); - - it('rejects duplicates against history', async () => { - const badTx = nonFeePayingTx(); - nullifierSource.getNullifierIndex.mockReturnValueOnce(Promise.resolve(1n)); - await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); - }); - }); - - describe('inspects how fee is paid', () => { - it('allows native gas', async () => { - const tx = nativeFeePayingTx(makeAztecAddress()); - // check that the whitelist on contract address won't shadow this check - contractDataSource.getContract.mockImplementationOnce(() => { - return Promise.resolve({ contractClassId: Fr.random() } as any); - }); - await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]); - }); - - it('allows correct contract class', async () => { - const fpc = makeAztecAddress(); - const tx = fxFeePayingTx(fpc); - - contractDataSource.getContract.mockImplementationOnce(address => { - if (fpc.equals(address)) { - return Promise.resolve({ contractClassId: allowedFPCClass } as any); - } else { - return Promise.resolve({ contractClassId: Fr.random() }); - } - }); - - await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]); - }); - - it('allows correct contract', async () => { - const tx = fxFeePayingTx(allowedFPC); - // check that the whitelist on contract address works and won't get shadowed by the class whitelist - contractDataSource.getContract.mockImplementationOnce(() => { - return Promise.resolve({ contractClassId: Fr.random() } as any); - }); - await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]); - }); - - it('rejects incorrect contract and class', async () => { - const fpc = makeAztecAddress(); - const tx = fxFeePayingTx(fpc); - - contractDataSource.getContract.mockImplementationOnce(() => { - return Promise.resolve({ contractClassId: Fr.random() } as any); - }); - - await expect(validator.validateTxs([tx])).resolves.toEqual([[], [tx]]); - }); - }); - - describe('inspects tx gas', () => { - it('allows native fee paying txs', async () => { - const sender = makeAztecAddress(); - const expectedBalanceSlot = pedersenHash([new Fr(1), sender]); - const tx = nativeFeePayingTx(sender); - - publicStateSource.storageRead.mockImplementation((address, slot) => { - if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) { - return Promise.resolve(new Fr(1)); - } else { - return Promise.resolve(Fr.ZERO); - } - }); - - await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]); - }); - - it('rejects native fee paying txs if out of balance', async () => { - const sender = makeAztecAddress(); - const expectedBalanceSlot = pedersenHash([new Fr(1), sender]); - const tx = nativeFeePayingTx(sender); - - publicStateSource.storageRead.mockImplementation((address, slot) => { - if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) { - return Promise.resolve(Fr.ZERO); - } else { - return Promise.resolve(new Fr(1)); - } - }); - - await expect(validator.validateTxs([tx])).resolves.toEqual([[], [tx]]); - }); - - it('allows txs paying through a fee payment contract', async () => { - const fpcAddress = makeAztecAddress(); - const expectedBalanceSlot = pedersenHash([new Fr(1), fpcAddress]); - const tx = fxFeePayingTx(fpcAddress); - - publicStateSource.storageRead.mockImplementation((address, slot) => { - if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) { - return Promise.resolve(new Fr(1)); - } else { - return Promise.resolve(Fr.ZERO); - } - }); - - await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]); - }); - - it('rejects txs paying through a fee payment contract out of balance', async () => { - const fpcAddress = makeAztecAddress(); - const expectedBalanceSlot = pedersenHash([new Fr(1), fpcAddress]); - const tx = nativeFeePayingTx(fpcAddress); - - publicStateSource.storageRead.mockImplementation((address, slot) => { - if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) { - return Promise.resolve(Fr.ZERO); - } else { - return Promise.resolve(new Fr(1)); - } - }); - - await expect(validator.validateTxs([tx])).resolves.toEqual([[], [tx]]); - }); - }); - - describe('inspects tx max block number', () => { - it('rejects tx with lower max block number', async () => { - const badTx = maxBlockNumberTx(globalVariables.blockNumber.sub(new Fr(1))); - - await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); - }); - - it('allows tx with larger max block number', async () => { - const goodTx = maxBlockNumberTx(globalVariables.blockNumber.add(new Fr(1))); - - await expect(validator.validateTxs([goodTx])).resolves.toEqual([[goodTx], []]); - }); - - it('allows tx with equal max block number', async () => { - const goodTx = maxBlockNumberTx(globalVariables.blockNumber); - - await expect(validator.validateTxs([goodTx])).resolves.toEqual([[goodTx], []]); - }); - - it('allows tx with unset max block number', async () => { - const goodTx = nonFeePayingTx(); - - await expect(validator.validateTxs([goodTx])).resolves.toEqual([[goodTx], []]); - }); - }); - - // get unique txs that are also stable across test runs - let txSeed = 1; - - function mockValidTx(forRollup = true, numberOfNonRevertiblePublicCallRequests = 0) { - const tx = forRollup - ? mockTxForRollup(txSeed++) - : mockTx(txSeed++, { numberOfNonRevertiblePublicCallRequests, numberOfRevertiblePublicCallRequests: 0 }); - tx.data.constants.txContext.chainId = globalVariables.chainId; - tx.data.constants.txContext.version = globalVariables.version; - return tx; - } - - /** Creates a mock tx for the current chain */ - function nonFeePayingTx() { - return mockValidTx(); - } - - /** Create a tx that pays for its cost natively */ - function nativeFeePayingTx(feePayer: AztecAddress) { - const tx = mockValidTx(false, 1); - const gasTokenAddress = getCanonicalGasTokenAddress(gasPortalAddress); - const signature = FunctionSelector.random(); - - const feeExecutionFn = new PublicCallRequest( - gasTokenAddress, - new FunctionData(signature, false), - new CallContext(feePayer, gasTokenAddress, gasPortalAddress, signature, false, false, 1), - CallContext.empty(), - [], - ); - - tx.data.forPublic!.endNonRevertibleData.publicCallStack[0] = feeExecutionFn.toCallRequest(); - tx.enqueuedPublicFunctionCalls[0] = feeExecutionFn; - - return tx; - } - - /** Create a tx that uses fee abstraction to pay for its cost */ - function fxFeePayingTx(feePaymentContract: AztecAddress) { - const tx = mockValidTx(false, 2); - - // the contract calls itself. Both functions are internal - const feeSetupSelector = FunctionSelector.random(); - const feeSetupFn = new PublicCallRequest( - feePaymentContract, - new FunctionData(feeSetupSelector, true), - new CallContext(feePaymentContract, feePaymentContract, EthAddress.ZERO, feeSetupSelector, false, false, 1), - CallContext.empty(), - [], - ); - tx.data.forPublic!.endNonRevertibleData.publicCallStack[0] = feeSetupFn.toCallRequest(); - tx.enqueuedPublicFunctionCalls[0] = feeSetupFn; - - const feeExecutionSelector = FunctionSelector.random(); - const feeExecutionFn = new PublicCallRequest( - feePaymentContract, - new FunctionData(feeExecutionSelector, true), - new CallContext(feePaymentContract, feePaymentContract, EthAddress.ZERO, feeExecutionSelector, false, false, 2), - CallContext.empty(), - [], - ); - tx.data.forPublic!.endNonRevertibleData.publicCallStack[1] = feeExecutionFn.toCallRequest(); - tx.enqueuedPublicFunctionCalls[1] = feeExecutionFn; - - return tx; - } - - /** Create a tx that constraints its max block number */ - function maxBlockNumberTx(maxBlockNumber: Fr) { - const tx = nonFeePayingTx(); - - tx.data.forRollup!.rollupValidationRequests.maxBlockNumber.isSome = true; - tx.data.forRollup!.rollupValidationRequests.maxBlockNumber.value = maxBlockNumber; - - return tx; - } -}); diff --git a/yarn-project/sequencer-client/src/sequencer/tx_validator.ts b/yarn-project/sequencer-client/src/sequencer/tx_validator.ts deleted file mode 100644 index 59be5a8a99a8..000000000000 --- a/yarn-project/sequencer-client/src/sequencer/tx_validator.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { type ProcessedTx, Tx } from '@aztec/circuit-types'; -import { - type AztecAddress, - type EthAddress, - Fr, - type GlobalVariables, - type PublicCallRequest, -} from '@aztec/circuits.js'; -import { pedersenHash } from '@aztec/foundation/crypto'; -import { type Logger, createDebugLogger } from '@aztec/foundation/log'; -import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; -import { type ContractDataSource } from '@aztec/types/contracts'; - -import { AbstractPhaseManager, PublicKernelPhase } from './abstract_phase_manager.js'; - -/** A source of what nullifiers have been committed to the state trees */ -export interface NullifierSource { - getNullifierIndex: (nullifier: Fr) => Promise; -} - -/** Provides a view into public contract state */ -export interface PublicStateSource { - storageRead: (contractAddress: AztecAddress, slot: Fr) => Promise; -} - -// prefer symbols over booleans so it's clear what the intention is -// vs returning true/false is tied to the function name -// eg. isDoubleSpend vs isValidChain assign different meanings to booleans -const VALID_TX = Symbol('valid_tx'); -const INVALID_TX = Symbol('invalid_tx'); - -type TxValidationStatus = typeof VALID_TX | typeof INVALID_TX; - -// the storage slot associated with "storage.balances" -const GAS_TOKEN_BALANCES_SLOT = new Fr(1); - -type FeeValidationConfig = { - gasPortalAddress: EthAddress; - allowedFeePaymentContractClasses: Fr[]; - allowedFeePaymentContractInstances: AztecAddress[]; -}; - -export class TxValidator { - #log: Logger; - #globalVariables: GlobalVariables; - #nullifierSource: NullifierSource; - #publicStateSource: PublicStateSource; - #contractDataSource: ContractDataSource; - #feeValidationConfig: FeeValidationConfig; - - constructor( - nullifierSource: NullifierSource, - publicStateSource: PublicStateSource, - contractDataSource: ContractDataSource, - globalVariables: GlobalVariables, - feeValidationConfig: FeeValidationConfig, - log = createDebugLogger('aztec:sequencer:tx_validator'), - ) { - this.#nullifierSource = nullifierSource; - this.#publicStateSource = publicStateSource; - this.#contractDataSource = contractDataSource; - this.#globalVariables = globalVariables; - this.#feeValidationConfig = feeValidationConfig; - this.#log = log; - } - - /** - * Validates a list of transactions. - * @param txs - The transactions to validate. - * @returns A tuple of valid and invalid transactions. - */ - public async validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[]]> { - const validTxs: T[] = []; - const invalidTxs: T[] = []; - const thisBlockNullifiers = new Set(); - - for (const tx of txs) { - if (this.#validateMetadata(tx) === INVALID_TX) { - invalidTxs.push(tx); - continue; - } - - if ((await this.#validateNullifiers(tx, thisBlockNullifiers)) === INVALID_TX) { - invalidTxs.push(tx); - continue; - } - - // skip already processed transactions - if (tx instanceof Tx) { - if ((await this.#validateFee(tx)) === INVALID_TX) { - invalidTxs.push(tx); - continue; - } - if ((await this.#validateGasBalance(tx)) === INVALID_TX) { - invalidTxs.push(tx); - continue; - } - } - - if (this.#validateMaxBlockNumber(tx) === INVALID_TX) { - invalidTxs.push(tx); - continue; - } - - validTxs.push(tx); - } - - return [validTxs, invalidTxs]; - } - - /** - * It rejects transactions with the wrong chain id. - * @param tx - The transaction. - * @returns Whether the transaction is valid. - */ - #validateMetadata(tx: Tx | ProcessedTx): TxValidationStatus { - if (!tx.data.constants.txContext.chainId.equals(this.#globalVariables.chainId)) { - this.#log.warn( - `Rejecting tx ${Tx.getHash( - tx, - )} because of incorrect chain ${tx.data.constants.txContext.chainId.toString()} != ${this.#globalVariables.chainId.toString()}`, - ); - return INVALID_TX; - } - - return VALID_TX; - } - - /** - * It looks for duplicate nullifiers: - * - in the same transaction - * - in the same block - * - in the nullifier tree - * - * Nullifiers prevent double spends in a private context. - * - * @param tx - The transaction. - * @returns Whether this is a problematic double spend that the L1 contract would reject. - */ - async #validateNullifiers(tx: Tx | ProcessedTx, thisBlockNullifiers: Set): Promise { - const newNullifiers = tx.data.getNonEmptyNullifiers().map(x => x.toBigInt()); - - // Ditch this tx if it has repeated nullifiers - const uniqueNullifiers = new Set(newNullifiers); - if (uniqueNullifiers.size !== newNullifiers.length) { - this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for emitting duplicate nullifiers`); - return INVALID_TX; - } - - for (const nullifier of newNullifiers) { - if (thisBlockNullifiers.has(nullifier)) { - this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for repeating a nullifier in the same block`); - return INVALID_TX; - } - - thisBlockNullifiers.add(nullifier); - } - - const nullifierIndexes = await Promise.all( - newNullifiers.map(n => this.#nullifierSource.getNullifierIndex(new Fr(n))), - ); - - const hasDuplicates = nullifierIndexes.some(index => index !== undefined); - if (hasDuplicates) { - this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for repeating nullifiers present in state trees`); - return INVALID_TX; - } - - return VALID_TX; - } - - async #validateGasBalance(tx: Tx): Promise { - if (!tx.data.forPublic || !tx.data.forPublic.needsTeardown) { - return VALID_TX; - } - - const teardownFn = TxValidator.#extractFeeExecutionCall(tx)!; - - // TODO(#1204) if a generator index is used for the derived storage slot of a map, update it here as well - const slot = pedersenHash([GAS_TOKEN_BALANCES_SLOT, teardownFn.callContext.msgSender]); - const gasBalance = await this.#publicStateSource.storageRead( - getCanonicalGasTokenAddress(this.#feeValidationConfig.gasPortalAddress), - slot, - ); - - // TODO(#5004) calculate fee needed based on tx limits and gas prices - const gasAmountNeeded = new Fr(1); - if (gasBalance.lt(gasAmountNeeded)) { - this.#log.warn( - `Rejecting tx ${Tx.getHash( - tx, - )} because it should pay for gas but has insufficient balance ${gasBalance.toShortString()} < ${gasAmountNeeded.toShortString()}`, - ); - return INVALID_TX; - } - - return VALID_TX; - } - - #validateMaxBlockNumber(tx: Tx | ProcessedTx): TxValidationStatus { - const target = - tx instanceof Tx - ? tx.data.forRollup?.rollupValidationRequests || tx.data.forPublic!.validationRequests.forRollup - : tx.data.rollupValidationRequests; - const maxBlockNumber = target.maxBlockNumber; - - if (maxBlockNumber.isSome && maxBlockNumber.value < this.#globalVariables.blockNumber) { - this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for low max block number`); - return INVALID_TX; - } else { - return VALID_TX; - } - } - - async #validateFee(tx: Tx): Promise { - if (!tx.data.forPublic || !tx.data.forPublic.needsTeardown) { - // TODO check if fees are mandatory and reject this tx - this.#log.debug(`Tx ${Tx.getHash(tx)} doesn't pay for gas`); - return VALID_TX; - } - - const teardownFn = TxValidator.#extractFeeExecutionCall(tx); - if (!teardownFn) { - this.#log.warn( - `Rejecting tx ${Tx.getHash(tx)} because it should pay for gas but has no enqueued teardown function call`, - ); - return INVALID_TX; - } - - const fpcAddress = teardownFn.contractAddress; - const contractClass = await this.#contractDataSource.getContract(fpcAddress); - - if (!contractClass) { - return INVALID_TX; - } - - if (fpcAddress.equals(getCanonicalGasTokenAddress(this.#feeValidationConfig.gasPortalAddress))) { - return VALID_TX; - } - - for (const allowedContract of this.#feeValidationConfig.allowedFeePaymentContractInstances) { - if (fpcAddress.equals(allowedContract)) { - return VALID_TX; - } - } - - for (const allowedContractClass of this.#feeValidationConfig.allowedFeePaymentContractClasses) { - if (contractClass.contractClassId.equals(allowedContractClass)) { - return VALID_TX; - } - } - - return INVALID_TX; - } - - static #extractFeeExecutionCall(tx: Tx): PublicCallRequest | undefined { - const { - // TODO what if there's more than one function call? - // if we're to enshrine that teardown = 1 function call, then we should turn this into a single function call - [PublicKernelPhase.TEARDOWN]: [teardownFn], - } = AbstractPhaseManager.extractEnqueuedPublicCallsByPhase(tx.data, tx.enqueuedPublicFunctionCalls); - - return teardownFn; - } -} diff --git a/yarn-project/sequencer-client/src/sequencer/tx_validator_factory.ts b/yarn-project/sequencer-client/src/sequencer/tx_validator_factory.ts deleted file mode 100644 index 6608071edb91..000000000000 --- a/yarn-project/sequencer-client/src/sequencer/tx_validator_factory.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { type AztecAddress, type EthAddress, type Fr, type GlobalVariables } from '@aztec/circuits.js'; -import { type ContractDataSource } from '@aztec/types/contracts'; -import { type MerkleTreeOperations } from '@aztec/world-state'; - -import { WorldStateDB, WorldStatePublicDB } from '../simulator/public_executor.js'; -import { TxValidator } from './tx_validator.js'; - -export class TxValidatorFactory { - constructor( - private merkleTreeDb: MerkleTreeOperations, - private contractDataSource: ContractDataSource, - private gasPortalAddress: EthAddress, - ) {} - - buildTxValidator( - globalVariables: GlobalVariables, - allowedFeePaymentContractClasses: Fr[], - allowedFeePaymentContractInstances: AztecAddress[], - ): TxValidator { - return new TxValidator( - new WorldStateDB(this.merkleTreeDb), - new WorldStatePublicDB(this.merkleTreeDb), - this.contractDataSource, - globalVariables, - { - allowedFeePaymentContractClasses, - allowedFeePaymentContractInstances, - gasPortalAddress: this.gasPortalAddress, - }, - ); - } -} diff --git a/yarn-project/sequencer-client/src/tx_validator/aggregate_tx_validator.test.ts b/yarn-project/sequencer-client/src/tx_validator/aggregate_tx_validator.test.ts new file mode 100644 index 000000000000..520bca760bf7 --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/aggregate_tx_validator.test.ts @@ -0,0 +1,30 @@ +import { Tx, type TxHash, mockTx } from '@aztec/circuit-types'; + +import { AggregateTxValidator } from './aggregate_tx_validator.js'; +import { type AnyTx, type TxValidator } from './tx_validator.js'; + +describe('AggregateTxValidator', () => { + it('allows txs that pass all validation', async () => { + const txs = [mockTx(0), mockTx(1), mockTx(2), mockTx(3), mockTx(4)]; + const agg = new AggregateTxValidator( + new TxDenyList(txs[0].getTxHash(), txs[1].getTxHash()), + new TxDenyList(txs[2].getTxHash(), txs[3].getTxHash()), + ); + + await expect(agg.validateTxs(txs)).resolves.toEqual([[txs[4]], [txs[0], txs[1], txs[2], txs[3]]]); + }); + + class TxDenyList implements TxValidator { + denyList: Set; + constructor(...txHashes: TxHash[]) { + this.denyList = new Set(txHashes.map(hash => hash.toString())); + } + + validateTxs(txs: AnyTx[]): Promise<[AnyTx[], AnyTx[]]> { + return Promise.resolve([ + txs.filter(tx => !this.denyList.has(Tx.getHash(tx).toString())), + txs.filter(tx => this.denyList.has(Tx.getHash(tx).toString())), + ]); + } + } +}); diff --git a/yarn-project/sequencer-client/src/tx_validator/aggregate_tx_validator.ts b/yarn-project/sequencer-client/src/tx_validator/aggregate_tx_validator.ts new file mode 100644 index 000000000000..5c829322f6f3 --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/aggregate_tx_validator.ts @@ -0,0 +1,26 @@ +import { type ProcessedTx, type Tx } from '@aztec/circuit-types'; + +import { type TxValidator } from './tx_validator.js'; + +export class AggregateTxValidator implements TxValidator { + #validators: TxValidator[]; + constructor(...validators: TxValidator[]) { + if (validators.length === 0) { + throw new Error('At least one validator must be provided'); + } + + this.#validators = validators; + } + + async validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[]]> { + const invalidTxs: T[] = []; + let txPool = txs; + for (const validator of this.#validators) { + const [valid, invalid] = await validator.validateTxs(txPool); + invalidTxs.push(...invalid); + txPool = valid; + } + + return [txPool, invalidTxs]; + } +} diff --git a/yarn-project/sequencer-client/src/tx_validator/double_spend_validator.test.ts b/yarn-project/sequencer-client/src/tx_validator/double_spend_validator.test.ts new file mode 100644 index 000000000000..da88e4ce396b --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/double_spend_validator.test.ts @@ -0,0 +1,54 @@ +import { mockTx, mockTxForRollup } from '@aztec/circuit-types'; + +import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; + +import { DoubleSpendTxValidator, type NullifierSource } from './double_spend_validator.js'; +import { type AnyTx } from './tx_validator.js'; + +describe('DoubleSpendTxValidator', () => { + let txValidator: DoubleSpendTxValidator; + let nullifierSource: MockProxy; + + beforeEach(() => { + nullifierSource = mock({ + getNullifierIndex: mockFn().mockImplementation(() => { + return Promise.resolve(undefined); + }), + }); + txValidator = new DoubleSpendTxValidator(nullifierSource); + }); + + it('rejects duplicates in non revertible data', async () => { + const badTx = mockTxForRollup(); + badTx.data.forRollup!.end.newNullifiers[1] = badTx.data.forRollup!.end.newNullifiers[0]; + await expect(txValidator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); + }); + + it('rejects duplicates in revertible data', async () => { + const badTx = mockTxForRollup(); + badTx.data.forRollup!.end.newNullifiers[1] = badTx.data.forRollup!.end.newNullifiers[0]; + await expect(txValidator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); + }); + + it('rejects duplicates across phases', async () => { + const badTx = mockTx(1, { + numberOfNonRevertiblePublicCallRequests: 1, + numberOfRevertiblePublicCallRequests: 1, + }); + badTx.data.forPublic!.end.newNullifiers[0] = badTx.data.forPublic!.endNonRevertibleData.newNullifiers[0]; + await expect(txValidator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); + }); + + it('rejects duplicates across txs', async () => { + const firstTx = mockTxForRollup(1); + const secondTx = mockTxForRollup(2); + secondTx.data.forRollup!.end.newNullifiers[0] = firstTx.data.forRollup!.end.newNullifiers[0]; + await expect(txValidator.validateTxs([firstTx, secondTx])).resolves.toEqual([[firstTx], [secondTx]]); + }); + + it('rejects duplicates against history', async () => { + const badTx = mockTx(); + nullifierSource.getNullifierIndex.mockReturnValueOnce(Promise.resolve(1n)); + await expect(txValidator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); + }); +}); diff --git a/yarn-project/sequencer-client/src/tx_validator/double_spend_validator.ts b/yarn-project/sequencer-client/src/tx_validator/double_spend_validator.ts new file mode 100644 index 000000000000..695e7b7b6f53 --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/double_spend_validator.ts @@ -0,0 +1,67 @@ +import { Tx } from '@aztec/circuit-types'; +import { Fr } from '@aztec/circuits.js'; +import { createDebugLogger } from '@aztec/foundation/log'; + +import { type AnyTx, type TxValidator } from './tx_validator.js'; + +export interface NullifierSource { + getNullifierIndex: (nullifier: Fr) => Promise; +} + +export class DoubleSpendTxValidator implements TxValidator { + #log = createDebugLogger('aztec:sequencer:tx_validator:tx_double_spend'); + #nullifierSource: NullifierSource; + + constructor(nullifierSource: NullifierSource) { + this.#nullifierSource = nullifierSource; + } + + async validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[]]> { + const validTxs: T[] = []; + const invalidTxs: T[] = []; + const thisBlockNullifiers = new Set(); + + for (const tx of txs) { + if (!(await this.#uniqueNullifiers(tx, thisBlockNullifiers))) { + invalidTxs.push(tx); + continue; + } + + validTxs.push(tx); + } + + return [validTxs, invalidTxs]; + } + + async #uniqueNullifiers(tx: AnyTx, thisBlockNullifiers: Set): Promise { + const newNullifiers = tx.data.getNonEmptyNullifiers().map(x => x.toBigInt()); + + // Ditch this tx if it has repeated nullifiers + const uniqueNullifiers = new Set(newNullifiers); + if (uniqueNullifiers.size !== newNullifiers.length) { + this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for emitting duplicate nullifiers`); + return false; + } + + for (const nullifier of newNullifiers) { + if (thisBlockNullifiers.has(nullifier)) { + this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for repeating a nullifier in the same block`); + return false; + } + + thisBlockNullifiers.add(nullifier); + } + + const nullifierIndexes = await Promise.all( + newNullifiers.map(n => this.#nullifierSource.getNullifierIndex(new Fr(n))), + ); + + const hasDuplicates = nullifierIndexes.some(index => index !== undefined); + if (hasDuplicates) { + this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for repeating nullifiers present in state trees`); + return false; + } + + return true; + } +} diff --git a/yarn-project/sequencer-client/src/tx_validator/gas_validator.test.ts b/yarn-project/sequencer-client/src/tx_validator/gas_validator.test.ts new file mode 100644 index 000000000000..8fab60f3a62f --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/gas_validator.test.ts @@ -0,0 +1,69 @@ +import { type Tx, mockTx } from '@aztec/circuit-types'; +import { AztecAddress, Fr } from '@aztec/circuits.js'; +import { pedersenHash } from '@aztec/foundation/crypto'; + +import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; + +import { GasTxValidator, type PublicStateSource } from './gas_validator.js'; + +describe('GasTxValidator', () => { + let validator: GasTxValidator; + let publicStateSource: MockProxy; + let gasTokenAddress: AztecAddress; + + beforeEach(() => { + gasTokenAddress = AztecAddress.random(); + publicStateSource = mock({ + storageRead: mockFn().mockImplementation((_address: AztecAddress, _slot: Fr) => { + return 0n; + }), + }); + + validator = new GasTxValidator(publicStateSource, gasTokenAddress, true); + }); + + let tx: Tx; + let payer: AztecAddress; + let expectedBalanceSlot: Fr; + + beforeEach(() => { + tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 1 }); + const teardownFn = tx.enqueuedPublicFunctionCalls.at(-1)!; + payer = teardownFn.callContext.msgSender; + expectedBalanceSlot = pedersenHash([new Fr(1), payer]); + }); + + it('allows fee paying txs if teardown caller has enough balance', async () => { + publicStateSource.storageRead.mockImplementation((address, slot) => { + if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) { + return Promise.resolve(new Fr(1)); + } else { + return Promise.resolve(Fr.ZERO); + } + }); + + await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]); + }); + + it('rejects txs if fee payer is out of balance', async () => { + publicStateSource.storageRead.mockImplementation((address, slot) => { + if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) { + return Promise.resolve(Fr.ZERO); + } else { + return Promise.resolve(new Fr(1)); + } + }); + await expect(validator.validateTxs([tx])).resolves.toEqual([[], [tx]]); + }); + + it('rejects txs with no teardown call', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 0 }); + await expect(validator.validateTxs([tx])).resolves.toEqual([[], [tx]]); + }); + + it('allows txs without a teardown call enqueued if fee not mandatory', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 0 }); + const lenientTxValidator = new GasTxValidator(publicStateSource, gasTokenAddress, false); + await expect(lenientTxValidator.validateTxs([tx])).resolves.toEqual([[tx], []]); + }); +}); diff --git a/yarn-project/sequencer-client/src/tx_validator/gas_validator.ts b/yarn-project/sequencer-client/src/tx_validator/gas_validator.ts new file mode 100644 index 000000000000..0e759813df4f --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/gas_validator.ts @@ -0,0 +1,85 @@ +import { Tx } from '@aztec/circuit-types'; +import { type AztecAddress, Fr } from '@aztec/circuits.js'; +import { pedersenHash } from '@aztec/foundation/crypto'; +import { createDebugLogger } from '@aztec/foundation/log'; + +import { AbstractPhaseManager, PublicKernelPhase } from '../sequencer/abstract_phase_manager.js'; +import { type TxValidator } from './tx_validator.js'; + +// the storage slot associated with "storage.balances" +const GAS_TOKEN_BALANCES_SLOT = new Fr(1); + +/** Provides a view into public contract state */ +export interface PublicStateSource { + storageRead: (contractAddress: AztecAddress, slot: Fr) => Promise; +} + +export class GasTxValidator implements TxValidator { + #log = createDebugLogger('aztec:sequencer:tx_validator:tx_gas'); + #publicDataSource: PublicStateSource; + #gasTokenAddress: AztecAddress; + #requireFees: boolean; + + constructor(publicDataSource: PublicStateSource, gasTokenAddress: AztecAddress, requireFees = false) { + this.#publicDataSource = publicDataSource; + this.#gasTokenAddress = gasTokenAddress; + this.#requireFees = requireFees; + } + + async validateTxs(txs: Tx[]): Promise<[validTxs: Tx[], invalidTxs: Tx[]]> { + const validTxs: Tx[] = []; + const invalidTxs: Tx[] = []; + + for (const tx of txs) { + if (await this.#validateTxFee(tx)) { + validTxs.push(tx); + } else { + invalidTxs.push(tx); + } + } + + return [validTxs, invalidTxs]; + } + + async #validateTxFee(tx: Tx): Promise { + const { [PublicKernelPhase.TEARDOWN]: teardownFns } = AbstractPhaseManager.extractEnqueuedPublicCallsByPhase( + tx.data, + tx.enqueuedPublicFunctionCalls, + ); + + if (teardownFns.length === 0) { + if (this.#requireFees) { + this.#log.warn( + `Rejecting tx ${Tx.getHash(tx)} because it should pay for gas but has no enqueued teardown functions`, + ); + return false; + } else { + this.#log.debug(`Tx ${Tx.getHash(tx)} does not pay fees. Skipping balance check.`); + return true; + } + } + + if (teardownFns.length > 1) { + this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} because it has multiple teardown functions`); + return false; + } + + // check that the caller of the teardown function has enough balance to pay for tx costs + const teardownFn = teardownFns[0]; + const slot = pedersenHash([GAS_TOKEN_BALANCES_SLOT, teardownFn.callContext.msgSender]); + const gasBalance = await this.#publicDataSource.storageRead(this.#gasTokenAddress, slot); + + // TODO(#5004) calculate fee needed based on tx limits and gas prices + const gasAmountNeeded = new Fr(1); + if (gasBalance.lt(gasAmountNeeded)) { + this.#log.warn( + `Rejecting tx ${Tx.getHash( + tx, + )} because it should pay for gas but has insufficient balance ${gasBalance.toShortString()} < ${gasAmountNeeded.toShortString()}`, + ); + return false; + } + + return true; + } +} diff --git a/yarn-project/sequencer-client/src/tx_validator/metadata_validator.test.ts b/yarn-project/sequencer-client/src/tx_validator/metadata_validator.test.ts new file mode 100644 index 000000000000..b73c1442562a --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/metadata_validator.test.ts @@ -0,0 +1,57 @@ +import { mockTx, mockTxForRollup } from '@aztec/circuit-types'; +import { Fr, type GlobalVariables, MaxBlockNumber } from '@aztec/circuits.js'; +import { makeGlobalVariables } from '@aztec/circuits.js/testing'; + +import { MetadataTxValidator } from './metadata_validator.js'; +import { type AnyTx } from './tx_validator.js'; + +describe('MetadataTxValidator', () => { + let globalVariables: GlobalVariables; + let validator: MetadataTxValidator; + + beforeEach(() => { + globalVariables = makeGlobalVariables(1, 42); + validator = new MetadataTxValidator(globalVariables); + }); + + it('allows only transactions for the right chain', async () => { + const goodTxs = [mockTx(1), mockTxForRollup(2)]; + const badTxs = [mockTx(3), mockTxForRollup(4)]; + + goodTxs.forEach(tx => { + tx.data.constants.txContext.chainId = globalVariables.chainId; + }); + + badTxs.forEach(tx => { + tx.data.constants.txContext.chainId = globalVariables.chainId.add(new Fr(1)); + }); + + await expect(validator.validateTxs([...goodTxs, ...badTxs])).resolves.toEqual([goodTxs, badTxs]); + }); + + it.each([42, 43])('allows txs with valid max block number', async maxBlockNumber => { + const goodTx = mockTxForRollup(1); + goodTx.data.constants.txContext.chainId = globalVariables.chainId; + goodTx.data.forRollup!.rollupValidationRequests.maxBlockNumber = new MaxBlockNumber(true, new Fr(maxBlockNumber)); + + await expect(validator.validateTxs([goodTx])).resolves.toEqual([[goodTx], []]); + }); + + it('allows txs with unset max block number', async () => { + const goodTx = mockTxForRollup(1); + goodTx.data.constants.txContext.chainId = globalVariables.chainId; + goodTx.data.forRollup!.rollupValidationRequests.maxBlockNumber = new MaxBlockNumber(false, Fr.ZERO); + + await expect(validator.validateTxs([goodTx])).resolves.toEqual([[goodTx], []]); + }); + + it('rejects txs with lower max block number', async () => { + const badTx = mockTxForRollup(1); + badTx.data.constants.txContext.chainId = globalVariables.chainId; + badTx.data.forRollup!.rollupValidationRequests.maxBlockNumber = new MaxBlockNumber( + true, + globalVariables.blockNumber.sub(new Fr(1)), + ); + await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]); + }); +}); diff --git a/yarn-project/sequencer-client/src/tx_validator/metadata_validator.ts b/yarn-project/sequencer-client/src/tx_validator/metadata_validator.ts new file mode 100644 index 000000000000..38f0c0714dcc --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/metadata_validator.ts @@ -0,0 +1,62 @@ +import { Tx } from '@aztec/circuit-types'; +import { type GlobalVariables } from '@aztec/circuits.js'; +import { createDebugLogger } from '@aztec/foundation/log'; + +import { type AnyTx, type TxValidator } from './tx_validator.js'; + +export class MetadataTxValidator implements TxValidator { + #log = createDebugLogger('aztec:sequencer:tx_validator:tx_metadata'); + #globalVariables: GlobalVariables; + + constructor(globalVariables: GlobalVariables) { + this.#globalVariables = globalVariables; + } + + validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[]]> { + const validTxs: T[] = []; + const invalidTxs: T[] = []; + for (const tx of txs) { + if (!this.#hasCorrectChainId(tx)) { + invalidTxs.push(tx); + continue; + } + + if (!this.#isValidForBlockNumber(tx)) { + invalidTxs.push(tx); + continue; + } + + validTxs.push(tx); + } + + return Promise.resolve([validTxs, invalidTxs]); + } + + #hasCorrectChainId(tx: T): boolean { + if (!tx.data.constants.txContext.chainId.equals(this.#globalVariables.chainId)) { + this.#log.warn( + `Rejecting tx ${Tx.getHash( + tx, + )} because of incorrect chain ${tx.data.constants.txContext.chainId.toNumber()} != ${this.#globalVariables.chainId.toNumber()}`, + ); + return false; + } else { + return true; + } + } + + #isValidForBlockNumber(tx: T): boolean { + const target = + tx instanceof Tx + ? tx.data.forRollup?.rollupValidationRequests || tx.data.forPublic!.validationRequests.forRollup + : tx.data.rollupValidationRequests; + const maxBlockNumber = target.maxBlockNumber; + + if (maxBlockNumber.isSome && maxBlockNumber.value < this.#globalVariables.blockNumber) { + this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for low max block number`); + return false; + } else { + return true; + } + } +} diff --git a/yarn-project/sequencer-client/src/tx_validator/phases_validator.test.ts b/yarn-project/sequencer-client/src/tx_validator/phases_validator.test.ts new file mode 100644 index 000000000000..3af8e3746f90 --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/phases_validator.test.ts @@ -0,0 +1,194 @@ +import { type Tx, mockTx } from '@aztec/circuit-types'; +import { type AztecAddress, Fr, type FunctionSelector } from '@aztec/circuits.js'; +import { makeAztecAddress, makeSelector } from '@aztec/circuits.js/testing'; +import { type ContractDataSource } from '@aztec/types/contracts'; + +import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; + +import { PhasesTxValidator } from './phases_validator.js'; + +describe('PhasesTxValidator', () => { + let contractDataSource: MockProxy; + let txValidator: PhasesTxValidator; + let allowedContractClass: Fr; + let allowedContract: AztecAddress; + let allowedSetupSelector1: FunctionSelector; + let allowedSetupSelector2: FunctionSelector; + let allowedTeardownSelector: FunctionSelector; + + beforeEach(() => { + allowedContractClass = Fr.random(); + allowedContract = makeAztecAddress(); + allowedSetupSelector1 = makeSelector(1); + allowedSetupSelector2 = makeSelector(2); + allowedTeardownSelector = makeSelector(3); + + contractDataSource = mock({ + getContract: mockFn().mockImplementation(() => { + return { + contractClassId: Fr.random(), + }; + }), + }); + + txValidator = new PhasesTxValidator( + contractDataSource, + [ + { + classId: allowedContractClass, + selector: allowedSetupSelector1, + }, + { + address: allowedContract, + selector: allowedSetupSelector1, + }, + { + classId: allowedContractClass, + selector: allowedSetupSelector2, + }, + { + address: allowedContract, + selector: allowedSetupSelector2, + }, + ], + [ + { + classId: allowedContractClass, + selector: allowedTeardownSelector, + }, + { + address: allowedContract, + selector: allowedTeardownSelector, + }, + ], + ); + }); + + it('allows teardown functions on the contracts allow list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 1 }); + patchNonRevertibleFn(tx, 0, { address: allowedContract, selector: allowedTeardownSelector }); + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[tx], []]); + }); + + it('allows teardown functions on the contracts class allow list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 1 }); + const { address } = patchNonRevertibleFn(tx, 0, { selector: allowedTeardownSelector }); + contractDataSource.getContract.mockImplementationOnce(contractAddress => { + if (address.equals(contractAddress)) { + return Promise.resolve({ + contractClassId: allowedContractClass, + } as any); + } else { + return Promise.resolve(undefined); + } + }); + + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[tx], []]); + }); + + it('rejects teardown functions not on the contracts class list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 1 }); + // good selector, bad contract class + const { address } = patchNonRevertibleFn(tx, 0, { selector: allowedTeardownSelector }); + contractDataSource.getContract.mockImplementationOnce(contractAddress => { + if (address.equals(contractAddress)) { + return Promise.resolve({ + contractClassId: Fr.random(), + } as any); + } else { + return Promise.resolve(undefined); + } + }); + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[], [tx]]); + }); + + it('rejects teardown functions not on the selector allow list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 1 }); + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[], [tx]]); + }); + + it('allows setup functions on the contracts allow list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + patchNonRevertibleFn(tx, 0, { address: allowedContract, selector: allowedSetupSelector1 }); + patchNonRevertibleFn(tx, 1, { address: allowedContract, selector: allowedTeardownSelector }); + + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[tx], []]); + }); + + it('allows setup functions on the contracts class allow list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + const { address } = patchNonRevertibleFn(tx, 0, { selector: allowedSetupSelector1 }); + patchNonRevertibleFn(tx, 1, { address: allowedContract, selector: allowedTeardownSelector }); + + contractDataSource.getContract.mockImplementationOnce(contractAddress => { + if (address.equals(contractAddress)) { + return Promise.resolve({ + contractClassId: allowedContractClass, + } as any); + } else { + return Promise.resolve(undefined); + } + }); + + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[tx], []]); + }); + + it('rejects txs with setup functions not on the allow list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + // only patch teardown + patchNonRevertibleFn(tx, 1, { address: allowedContract, selector: allowedTeardownSelector }); + + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[], [tx]]); + }); + + it('rejects setup functions not on the contracts class list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + // good selector, bad contract class + const { address } = patchNonRevertibleFn(tx, 0, { selector: allowedSetupSelector1 }); + patchNonRevertibleFn(tx, 1, { address: allowedContract, selector: allowedTeardownSelector }); + contractDataSource.getContract.mockImplementationOnce(contractAddress => { + if (address.equals(contractAddress)) { + return Promise.resolve({ + contractClassId: Fr.random(), + } as any); + } else { + return Promise.resolve(undefined); + } + }); + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[], [tx]]); + }); + + it('allows multiple setup functions on the allow list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 3 }); + patchNonRevertibleFn(tx, 0, { address: allowedContract, selector: allowedSetupSelector1 }); + patchNonRevertibleFn(tx, 1, { address: allowedContract, selector: allowedSetupSelector2 }); + patchNonRevertibleFn(tx, 2, { address: allowedContract, selector: allowedTeardownSelector }); + + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[tx], []]); + }); + + it('rejects if one setup functions is not on the allow list', async () => { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 3 }); + patchNonRevertibleFn(tx, 0, { address: allowedContract, selector: allowedSetupSelector1 }); + // don't patch index 1 + patchNonRevertibleFn(tx, 2, { address: allowedContract, selector: allowedTeardownSelector }); + + await expect(txValidator.validateTxs([tx])).resolves.toEqual([[], [tx]]); + }); + + function patchNonRevertibleFn( + tx: Tx, + index: number, + { address, selector }: { address?: AztecAddress; selector: FunctionSelector }, + ): { address: AztecAddress; selector: FunctionSelector } { + const fn = tx.enqueuedPublicFunctionCalls.at(-1 * index - 1)!; + fn.contractAddress = address ?? fn.contractAddress; + fn.functionData.selector = selector; + tx.data.forPublic!.endNonRevertibleData.publicCallStack[index] = fn.toCallRequest(); + + return { + address: fn.contractAddress, + selector: fn.functionData.selector, + }; + } +}); diff --git a/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts b/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts new file mode 100644 index 000000000000..b4d62d5ed947 --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/phases_validator.ts @@ -0,0 +1,103 @@ +import { type AllowedFunction, Tx } from '@aztec/circuit-types'; +import { type PublicCallRequest } from '@aztec/circuits.js'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { type ContractDataSource } from '@aztec/types/contracts'; + +import { AbstractPhaseManager, PublicKernelPhase } from '../sequencer/abstract_phase_manager.js'; +import { type TxValidator } from './tx_validator.js'; + +export class PhasesTxValidator implements TxValidator { + #log = createDebugLogger('aztec:sequencer:tx_validator:tx_phases'); + + constructor( + private contractDataSource: ContractDataSource, + private setupAllowList: AllowedFunction[], + private teardownAllowList: AllowedFunction[], + ) {} + + async validateTxs(txs: Tx[]): Promise<[validTxs: Tx[], invalidTxs: Tx[]]> { + const validTxs: Tx[] = []; + const invalidTxs: Tx[] = []; + + for (const tx of txs) { + if (await this.#validateTx(tx)) { + validTxs.push(tx); + } else { + invalidTxs.push(tx); + } + } + + return Promise.resolve([validTxs, invalidTxs]); + } + + async #validateTx(tx: Tx): Promise { + if (!tx.data.forPublic) { + this.#log.debug(`Tx ${Tx.getHash(tx)} does not contain enqueued public functions. Skipping phases validation.`); + return true; + } + + const { [PublicKernelPhase.SETUP]: setupFns, [PublicKernelPhase.TEARDOWN]: teardownFns } = + AbstractPhaseManager.extractEnqueuedPublicCallsByPhase(tx.data, tx.enqueuedPublicFunctionCalls); + + for (const setupFn of setupFns) { + if (!(await this.isOnAllowList(setupFn, this.setupAllowList))) { + this.#log.warn( + `Rejecting tx ${Tx.getHash(tx)} because it calls setup function not on allow list: ${ + setupFn.contractAddress + }:${setupFn.functionData.selector}`, + ); + + return false; + } + } + + for (const teardownFn of teardownFns) { + if (!(await this.isOnAllowList(teardownFn, this.teardownAllowList))) { + this.#log.warn( + `Rejecting tx ${Tx.getHash(tx)} because it calls teardown function not on allowlist: ${ + teardownFn.contractAddress + }:${teardownFn.functionData.selector}`, + ); + + return false; + } + } + + return true; + } + + async isOnAllowList(publicCall: PublicCallRequest, allowList: AllowedFunction[]): Promise { + const { + contractAddress, + functionData: { selector }, + } = publicCall; + + // do these checks first since they don't require the contract class + for (const entry of allowList) { + if (!('address' in entry)) { + continue; + } + + if (contractAddress.equals(entry.address) && entry.selector.equals(selector)) { + return true; + } + } + + const contractClass = await this.contractDataSource.getContract(contractAddress); + if (!contractClass) { + throw new Error(`Contract not found: ${publicCall.contractAddress.toString()}`); + } + + for (const entry of allowList) { + if (!('classId' in entry)) { + continue; + } + + if (contractClass.contractClassId.equals(entry.classId) && entry.selector.equals(selector)) { + return true; + } + } + + return false; + } +} diff --git a/yarn-project/sequencer-client/src/tx_validator/tx_validator.ts b/yarn-project/sequencer-client/src/tx_validator/tx_validator.ts new file mode 100644 index 000000000000..c5bb16b561f4 --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/tx_validator.ts @@ -0,0 +1,7 @@ +import { type ProcessedTx, type Tx } from '@aztec/circuit-types'; + +export type AnyTx = Tx | ProcessedTx; + +export interface TxValidator { + validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[]]>; +} diff --git a/yarn-project/sequencer-client/src/tx_validator/tx_validator_factory.ts b/yarn-project/sequencer-client/src/tx_validator/tx_validator_factory.ts new file mode 100644 index 000000000000..6162d502f0ef --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/tx_validator_factory.ts @@ -0,0 +1,38 @@ +import { type AllowedFunction, type ProcessedTx, type Tx } from '@aztec/circuit-types'; +import { type EthAddress, type GlobalVariables } from '@aztec/circuits.js'; +import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; +import { type ContractDataSource } from '@aztec/types/contracts'; +import { type MerkleTreeOperations } from '@aztec/world-state'; + +import { WorldStateDB, WorldStatePublicDB } from '../simulator/public_executor.js'; +import { AggregateTxValidator } from './aggregate_tx_validator.js'; +import { DoubleSpendTxValidator } from './double_spend_validator.js'; +import { GasTxValidator } from './gas_validator.js'; +import { MetadataTxValidator } from './metadata_validator.js'; +import { PhasesTxValidator } from './phases_validator.js'; +import { type TxValidator } from './tx_validator.js'; + +export class TxValidatorFactory { + constructor( + private merkleTreeDb: MerkleTreeOperations, + private contractDataSource: ContractDataSource, + private gasPortalAddress: EthAddress, + ) {} + + validatorForNewTxs( + globalVariables: GlobalVariables, + setupAllowList: AllowedFunction[], + teardownAllowList: AllowedFunction[], + ): TxValidator { + return new AggregateTxValidator( + new MetadataTxValidator(globalVariables), + new DoubleSpendTxValidator(new WorldStateDB(this.merkleTreeDb)), + new PhasesTxValidator(this.contractDataSource, setupAllowList, teardownAllowList), + new GasTxValidator(new WorldStatePublicDB(this.merkleTreeDb), getCanonicalGasTokenAddress(this.gasPortalAddress)), + ); + } + + validatorForProcessedTxs(): TxValidator { + return new DoubleSpendTxValidator(new WorldStateDB(this.merkleTreeDb)); + } +}