From adf20dc4974707255daffdaf3526dc48dc035873 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Tue, 26 Mar 2024 15:09:35 +0000 Subject: [PATCH] feat: sequencer checks list of allowed FPCs (#5310) The sequencer now checks the FPC being used by a tx is on its allowlist. This can be specified either by whitelisting contract classes or individual instances. Fix #5000 --- .../aztec-node/src/aztec-node/server.ts | 1 - yarn-project/aztec/src/cli/texts.ts | 2 + .../circuit-types/src/interfaces/configs.ts | 8 +- .../src/e2e_dapp_subscription.test.ts | 5 + yarn-project/end-to-end/src/e2e_fees.test.ts | 7 +- .../src/client/sequencer-client.ts | 10 +- yarn-project/sequencer-client/src/config.ts | 8 +- .../src/sequencer/public_processor.ts | 4 +- .../src/sequencer/sequencer.test.ts | 12 ++ .../src/sequencer/sequencer.ts | 27 ++--- .../src/sequencer/tx_validator.test.ts | 67 +++++++++++- .../src/sequencer/tx_validator.ts | 103 +++++++++++++----- .../src/sequencer/tx_validator_factory.ts | 32 ++++++ .../src/simulator/public_executor.ts | 10 +- 14 files changed, 234 insertions(+), 62 deletions(-) create mode 100644 yarn-project/sequencer-client/src/sequencer/tx_validator_factory.ts diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 93317645c7c..21e1d4ab3f1 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -651,7 +651,6 @@ export class AztecNodeService implements AztecNode { const publicProcessorFactory = new PublicProcessorFactory( merkleTrees.asLatest(), this.contractDataSource, - this.l1ToL2MessageSource, new WASMSimulator(), ); const processor = await publicProcessorFactory.create(prevHeader, newGlobalVariables); diff --git a/yarn-project/aztec/src/cli/texts.ts b/yarn-project/aztec/src/cli/texts.ts index 0ee661350c3..effb6912fe5 100644 --- a/yarn-project/aztec/src/cli/texts.ts +++ b/yarn-project/aztec/src/cli/texts.ts @@ -65,6 +65,8 @@ export const cliTexts = { 'transactionPollingIntervalMS:SEQ_TX_POLLING_INTERVAL_MS - number - The interval in ms to wait before polling for new transactions. Default: 1000\n' + 'acvmBinaryPath:ACVM_BINARY_PATH - string - The full path to an instance of the acvm cli application. If not provided will fallback to WASM circuit simulation\n' + 'acvmWorkingDirectory:ACVM_WORKING_DIRECTORY - string - A directory to use for temporary files used by the acvm application. If not provided WASM circuit simulation will be used\n' + + 'allowedFeePaymentContractClasses:SEQ_FPC_CLASSES - string[] - Which fee payment contract classes the sequencer allows' + + 'allowedFeePaymentContractInstances:SEQ_FPC_INSTANCES - string[] - Which fee payment contracts the sequencer allows.' + contractAddresses, prover: 'Starts a Prover with options. If started additionally to --node, the Prover will attach to that node.\n' + diff --git a/yarn-project/circuit-types/src/interfaces/configs.ts b/yarn-project/circuit-types/src/interfaces/configs.ts index 19dd07ac493..80198bfca29 100644 --- a/yarn-project/circuit-types/src/interfaces/configs.ts +++ b/yarn-project/circuit-types/src/interfaces/configs.ts @@ -1,4 +1,4 @@ -import { AztecAddress, EthAddress } from '@aztec/circuits.js'; +import { AztecAddress, EthAddress, Fr } from '@aztec/circuits.js'; /** * The sequencer configuration. @@ -18,4 +18,10 @@ export interface SequencerConfig { acvmWorkingDirectory?: string; /** The path to the ACVM binary */ acvmBinaryPath?: string; + + /** The list of permitted fee payment contract classes */ + allowedFeePaymentContractClasses?: Fr[]; + + /** The list of permitted fee payment contract instances. Takes precedence over contract classes */ + allowedFeePaymentContractInstances?: AztecAddress[]; } 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 000b96e116a..2dfd5ea8a10 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 { PrivateFeePaymentMethod, PublicFeePaymentMethod, SentTx, + getContractClassFromArtifact, } from '@aztec/aztec.js'; import { DefaultDappEntrypoint } from '@aztec/entrypoints/dapp'; import { @@ -67,6 +68,10 @@ describe('e2e_dapp_subscription', () => { const { wallets, accounts, aztecNode, deployL1ContractsValues } = e2eContext; + 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 d4cdfccab7c..b03a1d67052 100644 --- a/yarn-project/end-to-end/src/e2e_fees.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees.test.ts @@ -15,7 +15,7 @@ import { computeAuthWitMessageHash, computeMessageSecretHash, } from '@aztec/aztec.js'; -import { FunctionData } from '@aztec/circuits.js'; +import { FunctionData, getContractClassFromArtifact } from '@aztec/circuits.js'; import { ContractArtifact, decodeFunctionSignature } from '@aztec/foundation/abi'; import { TokenContract as BananaCoin, @@ -41,7 +41,7 @@ const TOKEN_SYMBOL = 'BC'; const TOKEN_DECIMALS = 18n; const BRIDGED_FPC_GAS = 500n; -jest.setTimeout(100_000); +jest.setTimeout(1_000_000_000); describe('e2e_fees', () => { let aliceWallet: Wallet; @@ -63,6 +63,9 @@ describe('e2e_fees', () => { e2eContext = await setup(3); const { accounts, logger, aztecNode, pxe, deployL1ContractsValues, wallets } = e2eContext; + await aztecNode.setConfig({ + allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], + }); logFunctionSignatures(BananaCoin.artifact, logger); logFunctionSignatures(FPCContract.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 ba7ab1ec918..79a96f8fe14 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -10,6 +10,7 @@ import { getGlobalVariableBuilder } from '../global_variable_builder/index.js'; import { getL1Publisher } from '../publisher/index.js'; import { Sequencer, SequencerConfig } from '../sequencer/index.js'; import { PublicProcessorFactory } from '../sequencer/public_processor.js'; +import { TxValidatorFactory } from '../sequencer/tx_validator_factory.js'; /** * Encapsulates the full sequencer and publisher. @@ -43,12 +44,7 @@ export class SequencerClient { const globalsBuilder = getGlobalVariableBuilder(config); const merkleTreeDb = worldStateSynchronizer.getLatest(); - const publicProcessorFactory = new PublicProcessorFactory( - merkleTreeDb, - contractDataSource, - l1ToL2MessageSource, - simulationProvider, - ); + const publicProcessorFactory = new PublicProcessorFactory(merkleTreeDb, contractDataSource, simulationProvider); const sequencer = new Sequencer( publisher, @@ -59,8 +55,8 @@ export class SequencerClient { l2BlockSource, l1ToL2MessageSource, publicProcessorFactory, + new TxValidatorFactory(merkleTreeDb, contractDataSource, config.l1Contracts.gasPortalAddress), config, - config.l1Contracts.gasPortalAddress, ); await sequencer.start(); diff --git a/yarn-project/sequencer-client/src/config.ts b/yarn-project/sequencer-client/src/config.ts index a1d60103099..f54dabbe3a9 100644 --- a/yarn-project/sequencer-client/src/config.ts +++ b/yarn-project/sequencer-client/src/config.ts @@ -1,4 +1,4 @@ -import { AztecAddress } from '@aztec/circuits.js'; +import { AztecAddress, Fr } from '@aztec/circuits.js'; import { L1ContractAddresses, NULL_KEY } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -40,6 +40,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, AVAILABILITY_ORACLE_CONTRACT_ADDRESS, ROLLUP_CONTRACT_ADDRESS, REGISTRY_CONTRACT_ADDRESS, @@ -88,5 +90,9 @@ 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) + : [], }; } diff --git a/yarn-project/sequencer-client/src/sequencer/public_processor.ts b/yarn-project/sequencer-client/src/sequencer/public_processor.ts index 50a6272df53..bb0b3dfe93a 100644 --- a/yarn-project/sequencer-client/src/sequencer/public_processor.ts +++ b/yarn-project/sequencer-client/src/sequencer/public_processor.ts @@ -1,6 +1,5 @@ import { FailedTx, - L1ToL2MessageSource, ProcessedTx, SimulationError, Tx, @@ -31,7 +30,6 @@ export class PublicProcessorFactory { constructor( private merkleTree: MerkleTreeOperations, private contractDataSource: ContractDataSource, - private l1Tol2MessagesDataSource: L1ToL2MessageSource, private simulator: SimulationProvider, ) {} @@ -50,7 +48,7 @@ export class PublicProcessorFactory { const publicContractsDB = new ContractsDataSourcePublicDB(this.contractDataSource); const worldStatePublicDB = new WorldStatePublicDB(this.merkleTree); - const worldStateDB = new WorldStateDB(this.merkleTree, this.l1Tol2MessagesDataSource); + const worldStateDB = new WorldStateDB(this.merkleTree); const publicExecutor = new PublicExecutor(worldStatePublicDB, publicContractsDB, worldStateDB, historicalHeader); return new PublicProcessor( this.merkleTree, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index d00a57a7fef..ed46187cd94 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -21,6 +21,7 @@ import { makeEmptyProof, } from '@aztec/circuits.js'; import { P2P, P2PClientState } from '@aztec/p2p'; +import { ContractDataSource } from '@aztec/types/contracts'; import { MerkleTreeOperations, WorldStateRunningState, WorldStateSynchronizer } from '@aztec/world-state'; import { MockProxy, mock, mockFn } from 'jest-mock-extended'; @@ -29,6 +30,7 @@ import { GlobalVariableBuilder } from '../global_variable_builder/global_builder import { L1Publisher } from '../index.js'; import { PublicProcessor, PublicProcessorFactory } from './public_processor.js'; import { Sequencer } from './sequencer.js'; +import { TxValidatorFactory } from './tx_validator_factory.js'; describe('sequencer', () => { let publisher: MockProxy; @@ -86,6 +88,12 @@ describe('sequencer', () => { getBlockNumber: () => Promise.resolve(lastBlockNumber), }); + // all txs use the same allowed FPC class + const fpcClassId = Fr.random(); + const contractSource = mock({ + getContractClass: mockFn().mockResolvedValue(fpcClassId), + }); + sequencer = new TestSubject( publisher, globalVariableBuilder, @@ -95,6 +103,10 @@ describe('sequencer', () => { l2BlockSource, 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 f4c3481a272..4823f48d3fa 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -1,4 +1,4 @@ -import { L1ToL2MessageSource, L2Block, L2BlockSource, MerkleTreeId, ProcessedTx, Tx } from '@aztec/circuit-types'; +import { L1ToL2MessageSource, L2Block, L2BlockSource, ProcessedTx, Tx } from '@aztec/circuit-types'; import { BlockProver, PROVING_STATUS } from '@aztec/circuit-types/interfaces'; import { L2BlockBuiltStats } from '@aztec/circuit-types/stats'; import { AztecAddress, EthAddress, GlobalVariables } from '@aztec/circuits.js'; @@ -11,10 +11,10 @@ import { WorldStateStatus, WorldStateSynchronizer } from '@aztec/world-state'; import { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js'; import { L1Publisher } from '../publisher/l1-publisher.js'; -import { WorldStatePublicDB } from '../simulator/public_executor.js'; import { SequencerConfig } from './config.js'; import { PublicProcessorFactory } from './public_processor.js'; import { TxValidator } from './tx_validator.js'; +import { TxValidatorFactory } from './tx_validator_factory.js'; /** * Sequencer client @@ -35,6 +35,8 @@ export class Sequencer { private _feeRecipient = AztecAddress.ZERO; private lastPublishedBlock = 0; private state = SequencerState.STOPPED; + private allowedFeePaymentContractClasses: Fr[] = []; + private allowedFeePaymentContractInstances: AztecAddress[] = []; constructor( private publisher: L1Publisher, @@ -45,8 +47,8 @@ export class Sequencer { private l2BlockSource: L2BlockSource, private l1ToL2MessageSource: L1ToL2MessageSource, private publicProcessorFactory: PublicProcessorFactory, + private txValidatorFactory: TxValidatorFactory, config: SequencerConfig = {}, - private gasPortalAddress = EthAddress.ZERO, private log = createDebugLogger('aztec:sequencer'), ) { this.updateConfig(config); @@ -73,6 +75,12 @@ export class Sequencer { if (config.feeRecipient) { this._feeRecipient = config.feeRecipient; } + if (config.allowedFeePaymentContractClasses) { + this.allowedFeePaymentContractClasses = config.allowedFeePaymentContractClasses; + } + if (config.allowedFeePaymentContractInstances) { + this.allowedFeePaymentContractInstances = config.allowedFeePaymentContractInstances; + } } /** @@ -170,17 +178,10 @@ export class Sequencer { this._feeRecipient, ); - // Filter out invalid txs - const trees = this.worldState.getLatest(); - const txValidator = new TxValidator( - { - getNullifierIndex(nullifier: Fr): Promise { - return trees.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer()); - }, - }, - new WorldStatePublicDB(trees), - this.gasPortalAddress, + 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 diff --git a/yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts b/yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts index 08f73968242..a9f13a43079 100644 --- a/yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts @@ -16,6 +16,7 @@ import { makeAztecAddress, makeGlobalVariables } from '@aztec/circuits.js/testin import { makeTuple } from '@aztec/foundation/array'; import { pedersenHash } from '@aztec/foundation/crypto'; import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; +import { ContractDataSource } from '@aztec/types/contracts'; import { MockProxy, mock, mockFn } from 'jest-mock-extended'; @@ -26,12 +27,18 @@ describe('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); @@ -46,8 +53,20 @@ describe('TxValidator', () => { } }), }); + contractDataSource = mock({ + getContract: mockFn().mockImplementation(() => { + return Promise.resolve({ + contractClassId: allowedFPCClass, + }); + }), + }); + globalVariables = makeGlobalVariables(); - validator = new TxValidator(nullifierSource, publicStateSource, gasPortalAddress, globalVariables); + validator = new TxValidator(nullifierSource, publicStateSource, contractDataSource, globalVariables, { + allowedFeePaymentContractClasses: [allowedFPCClass], + allowedFeePaymentContractInstances: [allowedFPC], + gasPortalAddress, + }); }); describe('inspects tx metadata', () => { @@ -93,6 +112,52 @@ describe('TxValidator', () => { }); }); + 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(); diff --git a/yarn-project/sequencer-client/src/sequencer/tx_validator.ts b/yarn-project/sequencer-client/src/sequencer/tx_validator.ts index 45146da70bf..ac586965586 100644 --- a/yarn-project/sequencer-client/src/sequencer/tx_validator.ts +++ b/yarn-project/sequencer-client/src/sequencer/tx_validator.ts @@ -1,8 +1,9 @@ import { ProcessedTx, Tx } from '@aztec/circuit-types'; -import { AztecAddress, EthAddress, Fr, GlobalVariables } from '@aztec/circuits.js'; +import { AztecAddress, EthAddress, Fr, GlobalVariables, PublicCallRequest } from '@aztec/circuits.js'; import { pedersenHash } from '@aztec/foundation/crypto'; import { Logger, createDebugLogger } from '@aztec/foundation/log'; import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; +import { ContractDataSource } from '@aztec/types/contracts'; import { AbstractPhaseManager, PublicKernelPhase } from './abstract_phase_manager.js'; @@ -27,25 +28,33 @@ 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; - #gasPortalAddress: EthAddress; + #contractDataSource: ContractDataSource; + #feeValidationConfig: FeeValidationConfig; constructor( nullifierSource: NullifierSource, publicStateSource: PublicStateSource, - gasPortalAddress: EthAddress, + contractDataSource: ContractDataSource, globalVariables: GlobalVariables, + feeValidationConfig: FeeValidationConfig, log = createDebugLogger('aztec:sequencer:tx_validator'), ) { this.#nullifierSource = nullifierSource; - this.#globalVariables = globalVariables; this.#publicStateSource = publicStateSource; - this.#gasPortalAddress = gasPortalAddress; - + this.#contractDataSource = contractDataSource; + this.#globalVariables = globalVariables; + this.#feeValidationConfig = feeValidationConfig; this.#log = log; } @@ -71,9 +80,15 @@ export class TxValidator { } // skip already processed transactions - if (tx instanceof Tx && (await this.#validateFee(tx)) === INVALID_TX) { - invalidTxs.push(tx); - continue; + 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) { @@ -150,30 +165,17 @@ export class TxValidator { return VALID_TX; } - async #validateFee(tx: Tx): Promise { + async #validateGasBalance(tx: Tx): Promise { if (!tx.data.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 { - // 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); - - 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 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.#gasPortalAddress), + getCanonicalGasTokenAddress(this.#feeValidationConfig.gasPortalAddress), slot, ); @@ -201,4 +203,55 @@ export class TxValidator { return VALID_TX; } } + + async #validateFee(tx: Tx): Promise { + if (!tx.data.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 new file mode 100644 index 00000000000..75a7a7b2ce3 --- /dev/null +++ b/yarn-project/sequencer-client/src/sequencer/tx_validator_factory.ts @@ -0,0 +1,32 @@ +import { AztecAddress, EthAddress, Fr, GlobalVariables } from '@aztec/circuits.js'; +import { ContractDataSource } from '@aztec/types/contracts'; +import { 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/simulator/public_executor.ts b/yarn-project/sequencer-client/src/simulator/public_executor.ts index 0e83ad98ab0..f6a42947704 100644 --- a/yarn-project/sequencer-client/src/simulator/public_executor.ts +++ b/yarn-project/sequencer-client/src/simulator/public_executor.ts @@ -1,10 +1,4 @@ -import { - L1ToL2MessageSource, - MerkleTreeId, - NullifierMembershipWitness, - Tx, - UnencryptedL2Log, -} from '@aztec/circuit-types'; +import { MerkleTreeId, NullifierMembershipWitness, Tx, UnencryptedL2Log } from '@aztec/circuit-types'; import { AztecAddress, ContractClassRegisteredEvent, @@ -198,7 +192,7 @@ export class WorldStatePublicDB implements PublicStateDB { * Implements WorldState db using a world state database. */ export class WorldStateDB implements CommitmentsDB { - constructor(private db: MerkleTreeOperations, private l1ToL2MessageSource: L1ToL2MessageSource) {} + constructor(private db: MerkleTreeOperations) {} public async getNullifierMembershipWitnessAtLatestBlock( nullifier: Fr,