diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index cd5b8b8b0975..fa07a23ced6f 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -63,11 +63,12 @@ import { createProverClient } from '@aztec/prover-client'; import { AggregateTxValidator, DataTxValidator, + DoubleSpendTxValidator, type GlobalVariableBuilder, SequencerClient, getGlobalVariableBuilder, } from '@aztec/sequencer-client'; -import { PublicProcessorFactory, WASMSimulator, createSimulationProvider } from '@aztec/simulator'; +import { PublicProcessorFactory, WASMSimulator, WorldStateDB, createSimulationProvider } from '@aztec/simulator'; import { type TelemetryClient } from '@aztec/telemetry-client'; import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; import { @@ -165,6 +166,7 @@ export class AztecNodeService implements AztecNode { new DataTxValidator(), new MetadataTxValidator(config.l1ChainId), new TxProofValidator(proofVerifier), + new DoubleSpendTxValidator(new WorldStateDB(worldStateSynchronizer.getLatest())), ); const simulationProvider = await createSimulationProvider(config, log); @@ -339,9 +341,7 @@ export class AztecNodeService implements AztecNode { const timer = new Timer(); this.log.info(`Received tx ${tx.getTxHash()}`); - const [_, invalidTxs] = await this.txValidator.validateTxs([tx]); - if (invalidTxs.length > 0) { - this.log.warn(`Rejecting tx ${tx.getTxHash()} because of validation errors`); + if ((await this.validateTx(tx)) === false) { this.metrics.receivedTx(timer.ms(), false); return; } @@ -768,6 +768,17 @@ export class AztecNodeService implements AztecNode { ); } + public async validateTx(tx: Tx): Promise { + const [_, invalidTxs] = await this.txValidator.validateTxs([tx]); + if (invalidTxs.length > 0) { + this.log.warn(`Rejecting tx ${tx.getTxHash()} because of validation errors`); + + return false; + } + + return true; + } + public async setConfig(config: Partial): Promise { const newConfig = { ...this.config, ...config }; this.sequencer?.updateSequencerConfig(config); diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index 7d31f4f74f49..3ade1883c3f5 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -23,6 +23,8 @@ export type SimulateMethodOptions = { from?: AztecAddress; /** Gas settings for the simulation. */ gasSettings?: GasSettings; + /** Simulate without validating tx against current state. */ + offline?: boolean; }; /** @@ -93,7 +95,7 @@ export class ContractFunctionInteraction extends BaseContractInteraction { } const txRequest = await this.create(); - const simulatedTx = await this.wallet.simulateTx(txRequest, true, options?.from); + const simulatedTx = await this.wallet.simulateTx(txRequest, true, options?.from, options?.offline); // As account entrypoints are private, for private functions we retrieve the return values from the first nested call // since we're interested in the first set of values AFTER the account entrypoint diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index 7a6d4ec81de7..6f9379db3a7c 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -108,8 +108,8 @@ export abstract class BaseWallet implements Wallet { proveTx(txRequest: TxExecutionRequest, simulatePublic: boolean): Promise { return this.pxe.proveTx(txRequest, simulatePublic, this.scopes); } - simulateTx(txRequest: TxExecutionRequest, simulatePublic: boolean, msgSender?: AztecAddress): Promise { - return this.pxe.simulateTx(txRequest, simulatePublic, msgSender, this.scopes); + simulateTx(txRequest: TxExecutionRequest, simulatePublic: boolean, msgSender?: AztecAddress, offline?: boolean): Promise { + return this.pxe.simulateTx(txRequest, simulatePublic, msgSender, offline, this.scopes); } sendTx(tx: Tx): Promise { return this.pxe.sendTx(tx); diff --git a/yarn-project/circuit-types/src/interfaces/aztec-node.ts b/yarn-project/circuit-types/src/interfaces/aztec-node.ts index 4973715724f7..f20195d0fe04 100644 --- a/yarn-project/circuit-types/src/interfaces/aztec-node.ts +++ b/yarn-project/circuit-types/src/interfaces/aztec-node.ts @@ -316,6 +316,13 @@ export interface AztecNode { **/ simulatePublicCalls(tx: Tx): Promise; + /** + * Validates the correctness of the execution, namely that a transaction is valid if and + * only if the transaction can be added to a valid block at the current state. + * @param tx - The transaction to validate for correctness. + */ + validateTx(tx: Tx): Promise; + /** * Updates the configuration of this node. * @param config - Updated configuration to be merged with the current one. diff --git a/yarn-project/circuit-types/src/interfaces/pxe.ts b/yarn-project/circuit-types/src/interfaces/pxe.ts index efa100eac0d1..c083d8728828 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.ts @@ -180,6 +180,7 @@ export interface PXE { * @param txRequest - An authenticated tx request ready for simulation * @param simulatePublic - Whether to simulate the public part of the transaction. * @param msgSender - (Optional) The message sender to use for the simulation. + * @param offline - (Optional) Whether to simulate without validating tx against current state. * @param scopes - (Optional) The accounts whose notes we can access in this call. Currently optional and will default to all. * @returns A simulated transaction object that includes a transaction that is potentially ready * to be sent to the network for execution, along with public and private return values. @@ -190,6 +191,7 @@ export interface PXE { txRequest: TxExecutionRequest, simulatePublic: boolean, msgSender?: AztecAddress, + offline?: boolean, scopes?: AztecAddress[], ): Promise; diff --git a/yarn-project/end-to-end/src/e2e_private_voting_contract.test.ts b/yarn-project/end-to-end/src/e2e_private_voting_contract.test.ts index a47a22da5984..b24bb91ef43b 100644 --- a/yarn-project/end-to-end/src/e2e_private_voting_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_private_voting_contract.test.ts @@ -47,7 +47,9 @@ describe('e2e_voting_contract', () => { await crossDelay(); // We try simulating voting again, but our TX is invalid because it will emit duplicate nullifiers - await expect(votingContract.methods.cast_vote(candidate).simulate()).rejects.toThrow( + await expect(votingContract.methods.cast_vote(candidate).simulate({ + offline: false, + })).rejects.toThrow( 'The simulated transaction is unable to be added to state and is invalid.', ); diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index 1b8708d95825..f848543edfab 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -497,6 +497,7 @@ export class PXEService implements PXE { txRequest: TxExecutionRequest, simulatePublic: boolean, msgSender: AztecAddress | undefined = undefined, + offline: boolean = true, scopes?: AztecAddress[], ): Promise { return await this.jobQueue.put(async () => { @@ -505,6 +506,14 @@ export class PXEService implements PXE { simulatedTx.publicOutput = await this.#simulatePublicCalls(simulatedTx.tx); } + if (!offline) { + const isValidTx = await this.node.validateTx(simulatedTx.tx); + + if (!isValidTx) { + throw new Error('The simulated transaction is unable to be added to state and is invalid.'); + } + } + // We log only if the msgSender is undefined, as simulating with a different msgSender // is unlikely to be a real transaction, and likely to be only used to read data. // Meaning that it will not necessarily have produced a nullifier (and thus have no TxHash) diff --git a/yarn-project/sequencer-client/src/index.ts b/yarn-project/sequencer-client/src/index.ts index 8dd0cf238897..73e9838889d8 100644 --- a/yarn-project/sequencer-client/src/index.ts +++ b/yarn-project/sequencer-client/src/index.ts @@ -4,6 +4,7 @@ export * from './publisher/index.js'; export * from './sequencer/index.js'; export * from './tx_validator/aggregate_tx_validator.js'; export * from './tx_validator/data_validator.js'; +export * from './tx_validator/double_spend_validator.js'; // Used by the node to simulate public parts of transactions. Should these be moved to a shared library? export * from './global_variable_builder/index.js';