diff --git a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr index 37c2edf31b7..999f07771bd 100644 --- a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr @@ -44,13 +44,6 @@ contract AppSubscription { note.remaining_txs -= 1; storage.subscriptions.at(user_address).replace(&mut note, true); - context.call_public_function( - storage.gas_token_address.read_private(), - FunctionSelector::from_signature("check_balance(Field)"), - // max gas limit in Gas tokens - [42] - ); - context.call_public_function( storage.gas_token_address.read_private(), FunctionSelector::from_signature("pay_fee(Field)"), diff --git a/yarn-project/circuit-types/src/interfaces/configs.ts b/yarn-project/circuit-types/src/interfaces/configs.ts index ee90be579d2..2698d7aeb78 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 6b742f38a8e..3f6c48c134f 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 c79c504aa45..727c1125f70 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 @@ -7,7 +7,6 @@ import { PrivateFeePaymentMethod, PublicFeePaymentMethod, TxStatus, - getContractClassFromArtifact, } from '@aztec/aztec.js'; import { FPCContract, GasTokenContract, TokenContract } from '@aztec/noir-contracts.js'; import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; @@ -39,7 +38,6 @@ describe('benchmarks/tx_size_fees', () => { await aztecNode.setConfig({ feeRecipient: sequencerAddress, - allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], }); await publicDeployAccounts(aliceWallet, wallets); diff --git a/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts b/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts index f5552190e01..a6c74dbbf1c 100644 --- a/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts +++ b/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts @@ -15,7 +15,7 @@ import { computeMessageSecretHash, generatePublicKey, } from '@aztec/aztec.js'; -import { type AztecAddress, CompleteAddress, Fq, getContractClassFromArtifact } from '@aztec/circuits.js'; +import { type AztecAddress, CompleteAddress, Fq } from '@aztec/circuits.js'; import { TokenContract as BananaCoin, FPCContract, @@ -89,7 +89,6 @@ describe('e2e_fees_account_init', () => { sequencersAddress = sequencer.getAddress(); await ctx.aztecNode.setConfig({ - allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], feeRecipient: sequencersAddress, }); 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 3c6406ae255..df3cf484e45 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 @@ -10,7 +10,6 @@ import { PrivateFeePaymentMethod, PublicFeePaymentMethod, SentTx, - getContractClassFromArtifact, } from '@aztec/aztec.js'; import { DefaultDappEntrypoint } from '@aztec/entrypoints/dapp'; import { @@ -70,10 +69,6 @@ describe('e2e_dapp_subscription', () => { await publicDeployAccounts(wallets[0], wallets); - await aztecNode.setConfig({ - allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], - }); - // this should be a SignerlessWallet but that can't call public functions directly gasTokenContract = await GasTokenContract.at( getCanonicalGasTokenAddress(deployL1ContractsValues.l1ContractAddresses.gasPortalAddress), 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 7aed8e282eb..9e0248b16f1 100644 --- a/yarn-project/end-to-end/src/e2e_fees.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees.test.ts @@ -16,7 +16,7 @@ import { computeAuthWitMessageHash, computeMessageSecretHash, } from '@aztec/aztec.js'; -import { FunctionData, getContractClassFromArtifact } from '@aztec/circuits.js'; +import { FunctionData } from '@aztec/circuits.js'; import { type ContractArtifact, decodeFunctionSignature } from '@aztec/foundation/abi'; import { TokenContract as BananaCoin, @@ -57,10 +57,6 @@ describe('e2e_fees', () => { const { wallets: _wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3); wallets = _wallets; - await aztecNode.setConfig({ - allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], - }); - logFunctionSignatures(BananaCoin.artifact, logger); logFunctionSignatures(FPCContract.artifact, logger); logFunctionSignatures(GasTokenContract.artifact, logger); diff --git a/yarn-project/sequencer-client/package.json b/yarn-project/sequencer-client/package.json index cda27227c2c..47336f4a507 100644 --- a/yarn-project/sequencer-client/package.json +++ b/yarn-project/sequencer-client/package.json @@ -30,6 +30,7 @@ "@aztec/foundation": "workspace:^", "@aztec/l1-artifacts": "workspace:^", "@aztec/merkle-tree": "workspace:^", + "@aztec/noir-contracts.js": "workspace:^", "@aztec/noir-protocol-circuits-types": "workspace:^", "@aztec/p2p": "workspace:^", "@aztec/protocol-contracts": "workspace:^", diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index 3cbf3656784..24d9cbd99f6 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 b86863b9393..92384b6d882 100644 --- a/yarn-project/sequencer-client/src/config.ts +++ b/yarn-project/sequencer-client/src/config.ts @@ -1,6 +1,14 @@ -import { AztecAddress, Fr } from '@aztec/circuits.js'; +import { type AllowedFunction } from '@aztec/circuit-types'; +import { AztecAddress, Fr, FunctionSelector, getContractClassFromArtifact } from '@aztec/circuits.js'; import { type L1ContractAddresses, NULL_KEY } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; +import { EcdsaAccountContractArtifact } from '@aztec/noir-contracts.js/EcdsaAccount'; +import { FPCContract } from '@aztec/noir-contracts.js/FPC'; +import { GasTokenContract } from '@aztec/noir-contracts.js/GasToken'; +import { SchnorrAccountContractArtifact } from '@aztec/noir-contracts.js/SchnorrAccount'; +import { SchnorrHardcodedAccountContractArtifact } from '@aztec/noir-contracts.js/SchnorrHardcodedAccount'; +import { SchnorrSingleKeyAccountContractArtifact } from '@aztec/noir-contracts.js/SchnorrSingleKeyAccount'; +import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; import { type Hex } from 'viem'; @@ -40,8 +48,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 +98,87 @@ 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: SEQ_ALLOWED_SETUP_FN + ? parseSequencerAllowList(SEQ_ALLOWED_SETUP_FN) + : getDefaultAllowedSetupFunctions(), + allowedFunctionsInTeardown: SEQ_ALLOWED_TEARDOWN_FN + ? parseSequencerAllowList(SEQ_ALLOWED_TEARDOWN_FN) + : getDefaultAllowedTeardownFunctions(), }; } + +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); + + if (identifierString.startsWith('0x')) { + entries.push({ + address: AztecAddress.fromString(identifierString), + selector, + }); + } else { + entries.push({ + classId: Fr.fromString(identifierString), + selector, + }); + } + } + + return entries; +} + +function getDefaultAllowedSetupFunctions(): AllowedFunction[] { + return [ + { + classId: getContractClassFromArtifact(SchnorrAccountContractArtifact).id, + selector: FunctionSelector.fromSignature('approve_public_authwit(Field)'), + }, + { + classId: getContractClassFromArtifact(SchnorrHardcodedAccountContractArtifact).id, + selector: FunctionSelector.fromSignature('approve_public_authwit(Field)'), + }, + { + classId: getContractClassFromArtifact(SchnorrSingleKeyAccountContractArtifact).id, + selector: FunctionSelector.fromSignature('approve_public_authwit(Field)'), + }, + { + classId: getContractClassFromArtifact(EcdsaAccountContractArtifact).id, + selector: FunctionSelector.fromSignature('approve_public_authwit(Field)'), + }, + + // needed for private transfers via FPC + { + classId: getContractClassFromArtifact(TokenContractArtifact).id, + selector: FunctionSelector.fromSignature('_increase_public_balance((Field),Field)'), + }, + + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('prepare_fee((Field),Field,(Field),Field)'), + }, + ]; +} + +function getDefaultAllowedTeardownFunctions(): AllowedFunction[] { + return [ + { + classId: getContractClassFromArtifact(GasTokenContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee(Field)'), + }, + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee((Field),Field,(Field))'), + }, + { + classId: getContractClassFromArtifact(FPCContract.artifact).id, + selector: FunctionSelector.fromSignature('pay_fee_with_shielded_rebate(Field,(Field),Field)'), + }, + ]; +} 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 fa1cbebee4c..fe266b476f8 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 0e572d0a1f9..d85bf63a05c 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 132b69d476f..ce68916309b 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 76d6fb6a050..447a07ec248 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 2efab40df55..0c7ffdd01de 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 ee8bd7c4212..00000000000 --- 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 59be5a8a99a..00000000000 --- 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 6608071edb9..00000000000 --- 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 00000000000..520bca760bf --- /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 00000000000..5c829322f6f --- /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 00000000000..da88e4ce396 --- /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 00000000000..695e7b7b6f5 --- /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 00000000000..86b5a98c355 --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/gas_validator.test.ts @@ -0,0 +1,70 @@ +import { type Tx, mockTx } from '@aztec/circuit-types'; +import { AztecAddress, Fr } from '@aztec/circuits.js'; +import { pedersenHash } from '@aztec/foundation/crypto'; +import { GasTokenContract } from '@aztec/noir-contracts.js'; + +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([GasTokenContract.storage.balances.slot, 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 00000000000..5775fb7c623 --- /dev/null +++ b/yarn-project/sequencer-client/src/tx_validator/gas_validator.ts @@ -0,0 +1,83 @@ +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 { GasTokenContract } from '@aztec/noir-contracts.js'; + +import { AbstractPhaseManager, PublicKernelPhase } from '../sequencer/abstract_phase_manager.js'; +import { type TxValidator } from './tx_validator.js'; + +/** 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([GasTokenContract.storage.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 00000000000..b73c1442562 --- /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 00000000000..38f0c0714dc --- /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 00000000000..3af8e3746f9 --- /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 00000000000..b4d62d5ed94 --- /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 00000000000..c5bb16b561f --- /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 00000000000..6162d502f0e --- /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)); + } +} diff --git a/yarn-project/sequencer-client/tsconfig.json b/yarn-project/sequencer-client/tsconfig.json index 501b195fd41..4ec1ceda867 100644 --- a/yarn-project/sequencer-client/tsconfig.json +++ b/yarn-project/sequencer-client/tsconfig.json @@ -24,6 +24,9 @@ { "path": "../merkle-tree" }, + { + "path": "../noir-contracts.js" + }, { "path": "../noir-protocol-circuits-types" }, diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index b251c25c6b7..4eba358d479 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -834,6 +834,7 @@ __metadata: "@aztec/kv-store": "workspace:^" "@aztec/l1-artifacts": "workspace:^" "@aztec/merkle-tree": "workspace:^" + "@aztec/noir-contracts.js": "workspace:^" "@aztec/noir-protocol-circuits-types": "workspace:^" "@aztec/p2p": "workspace:^" "@aztec/protocol-contracts": "workspace:^"