diff --git a/yarn-project/acir-simulator/src/acvm/acvm.ts b/yarn-project/acir-simulator/src/acvm/acvm.ts index 5b07aca3abb..adcbdb736a6 100644 --- a/yarn-project/acir-simulator/src/acvm/acvm.ts +++ b/yarn-project/acir-simulator/src/acvm/acvm.ts @@ -3,6 +3,7 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; +import { NoirCallStack } from '@aztec/types'; import { ForeignCallInput, @@ -77,32 +78,10 @@ function extractOpcodeLocationFromError(err: string): string | undefined { return match?.groups?.opcodeLocation; } -/** - * The data for a call in the call stack. - */ -interface SourceCodeLocation { - /** - * The path to the source file. - */ - filePath: string; - /** - * The line number of the call. - */ - line: number; - /** - * The source code of the file. - */ - fileSource: string; - /** - * The source code text of the failed constraint. - */ - assertionText: string; -} - /** * Extracts the call stack from the location of a failing opcode and the debug metadata. */ -function getCallStackFromOpcodeLocation(opcodeLocation: string, debug: FunctionDebugMetadata): SourceCodeLocation[] { +function getCallStackFromOpcodeLocation(opcodeLocation: string, debug: FunctionDebugMetadata): NoirCallStack { const { debugSymbols, files } = debug; const callStack = debugSymbols.locations[opcodeLocation] || []; @@ -111,7 +90,7 @@ function getCallStackFromOpcodeLocation(opcodeLocation: string, debug: FunctionD const { path, source } = files[fileId]; - const assertionText = source.substring(span.start, span.end + 1); + const locationText = source.substring(span.start, span.end + 1); const precedingText = source.substring(0, span.start); const line = precedingText.split('\n').length; @@ -119,22 +98,39 @@ function getCallStackFromOpcodeLocation(opcodeLocation: string, debug: FunctionD filePath: path, line, fileSource: source, - assertionText, + locationText, }; }); } /** - * Creates a formatted string for an error stack - * @param callStack - The error stack - * @returns - The formatted string + * Extracts source code locations from an ACVM error if possible. + * @param errMessage - The ACVM error. + * @param debug - The debug metadata of the function. + * @returns The source code locations or undefined if they couldn't be extracted from the error. */ -function printErrorStack(callStack: SourceCodeLocation[]): string { - // TODO experiment with formats of reporting this for better error reporting - return [ - 'Error: Assertion failed', - callStack.map(call => ` at ${call.filePath}:${call.line} '${call.assertionText}'`), - ].join('\n'); +export function processAcvmError(errMessage: string, debug: FunctionDebugMetadata): NoirCallStack | undefined { + const opcodeLocation = extractOpcodeLocationFromError(errMessage); + if (!opcodeLocation) { + return undefined; + } + + return getCallStackFromOpcodeLocation(opcodeLocation, debug); +} + +/** + * An error thrown by the ACVM during simulation. Optionally contains a noir call stack. + */ +export class ACVMError extends Error { + constructor( + message: string, + /** + * The noir call stack of the error, if it could be extracted. + */ + public callStack?: NoirCallStack, + ) { + super(message); + } } /** @@ -174,22 +170,27 @@ export async function acvm( typedError = new Error(`Error in oracle callback ${err}`); } oracleError = typedError; - logger.error(`Error in oracle callback ${name}: ${typedError.message}`); + logger.error(`Error in oracle callback ${name}:`, typedError.message, typedError.stack); throw typedError; } }, - ).catch((acvmError: string) => { + ).catch((acvmErrorString: string) => { if (oracleError) { throw oracleError; } - const opcodeLocation = extractOpcodeLocationFromError(acvmError); - if (!opcodeLocation || !debug) { - throw new Error(acvmError); - } - const callStack = getCallStackFromOpcodeLocation(opcodeLocation, debug); - logger(printErrorStack(callStack)); - throw new Error(`Assertion failed: '${callStack.pop()?.assertionText ?? 'Unknown'}'`); + if (debug) { + const callStack = processAcvmError(acvmErrorString, debug); + + if (callStack) { + throw new ACVMError( + `Assertion failed: '${callStack[callStack.length - 1]?.locationText ?? 'Unknown'}'`, + callStack, + ); + } + } + // If we cannot find a callstack, throw the original error. + throw new ACVMError(acvmErrorString); }); return Promise.resolve({ partialWitness }); diff --git a/yarn-project/acir-simulator/src/client/private_execution.ts b/yarn-project/acir-simulator/src/client/private_execution.ts index 93425ffb955..ef002781d71 100644 --- a/yarn-project/acir-simulator/src/client/private_execution.ts +++ b/yarn-project/acir-simulator/src/client/private_execution.ts @@ -11,7 +11,7 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr, Point } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { to2Fields } from '@aztec/foundation/serialize'; -import { FunctionL2Logs, NotePreimage, NoteSpendingInfo } from '@aztec/types'; +import { FunctionL2Logs, NotePreimage, NoteSpendingInfo, SimulationError } from '@aztec/types'; import { extractPrivateCircuitPublicInputs, frToAztecAddress } from '../acvm/deserialize.js'; import { @@ -55,8 +55,8 @@ export class PrivateFunctionExecution { * @returns The execution result. */ public async run(): Promise { - const selector = this.functionData.selector.toString(); - this.log(`Executing external function ${this.contractAddress.toString()}:${selector}`); + const selector = this.functionData.selector; + this.log(`Executing external function ${this.contractAddress}:${selector}`); const acir = Buffer.from(this.abi.bytecode, 'base64'); const initialWitness = this.getInitialWitness(); @@ -197,7 +197,9 @@ export class PrivateFunctionExecution { }, }, this.abi.debug, - ); + ).catch((err: Error) => { + throw SimulationError.fromError(this.contractAddress, selector, err); + }); const publicInputs = extractPrivateCircuitPublicInputs(partialWitness, acir); diff --git a/yarn-project/acir-simulator/src/client/unconstrained_execution.ts b/yarn-project/acir-simulator/src/client/unconstrained_execution.ts index a370240badd..2bba8bea403 100644 --- a/yarn-project/acir-simulator/src/client/unconstrained_execution.ts +++ b/yarn-project/acir-simulator/src/client/unconstrained_execution.ts @@ -3,7 +3,7 @@ import { DecodedReturn, decodeReturnValues } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; -import { AztecNode } from '@aztec/types'; +import { AztecNode, SimulationError } from '@aztec/types'; import { extractReturnWitness, frToAztecAddress } from '../acvm/deserialize.js'; import { ACVMField, ZERO_ACVM_FIELD, acvm, fromACVMField, toACVMField, toACVMWitness } from '../acvm/index.js'; @@ -33,9 +33,7 @@ export class UnconstrainedFunctionExecution { * @returns The return values of the executed function. */ public async run(aztecNode?: AztecNode): Promise { - this.log( - `Executing unconstrained function ${this.contractAddress.toShortString()}:${this.functionData.selector.toString()}`, - ); + this.log(`Executing unconstrained function ${this.contractAddress.toShortString()}:${this.functionData.selector}`); const acir = Buffer.from(this.abi.bytecode, 'base64'); const initialWitness = toACVMWitness(1, this.args); @@ -105,7 +103,9 @@ export class UnconstrainedFunctionExecution { }, }, this.abi.debug, - ); + ).catch((err: Error) => { + throw SimulationError.fromError(this.contractAddress, this.functionData.selector, err); + }); const returnValues: ACVMField[] = extractReturnWitness(acir, partialWitness); diff --git a/yarn-project/acir-simulator/src/public/executor.ts b/yarn-project/acir-simulator/src/public/executor.ts index 2b0081508a9..5ed691cef2a 100644 --- a/yarn-project/acir-simulator/src/public/executor.ts +++ b/yarn-project/acir-simulator/src/public/executor.ts @@ -10,7 +10,7 @@ import { } from '@aztec/circuits.js'; import { padArrayEnd } from '@aztec/foundation/collection'; import { createDebugLogger } from '@aztec/foundation/log'; -import { FunctionL2Logs } from '@aztec/types'; +import { FunctionL2Logs, SimulationError } from '@aztec/types'; import { ZERO_ACVM_FIELD, @@ -67,7 +67,6 @@ export class PublicExecutor { // Functions can request to pack arguments before calling other functions. // We use this cache to hold the packed arguments. const packedArgs = await PackedArgsCache.create([]); - const { partialWitness } = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, { packArguments: async args => { return toACVMField(await packedArgs.pack(args.map(fromACVMField))); @@ -140,6 +139,8 @@ export class PublicExecutor { (await this.contractsDb.getPortalContractAddress(contractAddress)) ?? EthAddress.ZERO; return Promise.resolve(toACVMField(portalContactAddress)); }, + }).catch((err: Error) => { + throw SimulationError.fromError(execution.contractAddress, selector, err); }); const { diff --git a/yarn-project/aztec-node/src/aztec-node/http-node.test.ts b/yarn-project/aztec-node/src/aztec-node/http-node.test.ts index 45de1625658..32f105ccda5 100644 --- a/yarn-project/aztec-node/src/aztec-node/http-node.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/http-node.test.ts @@ -1,4 +1,4 @@ -import { AztecAddress, CircuitsWasm, EthAddress, Fr, HistoricBlockData } from '@aztec/circuits.js'; +import { AztecAddress, CircuitsWasm, EthAddress, Fr, FunctionSelector, HistoricBlockData } from '@aztec/circuits.js'; import { randomBytes } from '@aztec/foundation/crypto'; import { Pedersen } from '@aztec/merkle-tree'; import { @@ -10,6 +10,7 @@ import { LogType, MerkleTreeId, SiblingPath, + SimulationError, TxHash, mockTx, } from '@aztec/types'; @@ -482,4 +483,36 @@ describe('HttpNode', () => { expect(result).toEqual(blockData); }); }); + + describe('simulatePublicCalls', () => { + it('should fetch a successful simulation response', async () => { + const tx = mockTx(); + const response = {}; + setFetchMock(response); + + await httpNode.simulatePublicCalls(tx); + + const init: RequestInit = { + method: 'POST', + body: tx.toBuffer(), + }; + const call = (fetch as jest.Mock).mock.calls[0] as any[]; + expect(call[0].href).toBe(`${TEST_URL}simulate-tx`); + expect(call[1]).toStrictEqual(init); + }); + + it('should fetch a simulation error', async () => { + const tx = mockTx(); + const simulationError = new SimulationError('Failing function', { + contractAddress: AztecAddress.ZERO, + functionSelector: FunctionSelector.empty(), + }); + const response = { + simulationError: simulationError.toJSON(), + }; + setFetchMock(response); + + await expect(httpNode.simulatePublicCalls(tx)).rejects.toThrow(simulationError); + }); + }); }); diff --git a/yarn-project/aztec-node/src/aztec-node/http-node.ts b/yarn-project/aztec-node/src/aztec-node/http-node.ts index a102dc258b4..52f1d898cfa 100644 --- a/yarn-project/aztec-node/src/aztec-node/http-node.ts +++ b/yarn-project/aztec-node/src/aztec-node/http-node.ts @@ -20,6 +20,7 @@ import { LogType, MerkleTreeId, SiblingPath, + SimulationError, Tx, TxHash, } from '@aztec/types'; @@ -369,4 +370,19 @@ export class HttpNode implements AztecNode { const response = await (await fetch(url.toString())).json(); return response.blockData; } + + /** + * Simulates the public part of a transaction with the current state. + * @param tx - The transaction to simulate. + **/ + public async simulatePublicCalls(tx: Tx) { + const url = new URL(`${this.baseUrl}/simulate-tx`); + const init: RequestInit = {}; + init['method'] = 'POST'; + init['body'] = tx.toBuffer(); + const response = await (await fetch(url, init)).json(); + if (response.simulationError) { + throw SimulationError.fromJSON(response.simulationError); + } + } } diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index cccec0aa8d8..b2235f1b31b 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -4,6 +4,7 @@ import { CircuitsWasm, EthAddress, Fr, + GlobalVariables, HistoricBlockData, L1_TO_L2_MSG_TREE_HEIGHT, PRIVATE_DATA_TREE_HEIGHT, @@ -11,7 +12,12 @@ import { import { AztecAddress } from '@aztec/foundation/aztec-address'; import { createDebugLogger } from '@aztec/foundation/log'; import { InMemoryTxPool, P2P, createP2PClient } from '@aztec/p2p'; -import { SequencerClient } from '@aztec/sequencer-client'; +import { + GlobalVariableBuilder, + PublicProcessorFactory, + SequencerClient, + getGlobalVariableBuilder, +} from '@aztec/sequencer-client'; import { AztecNode, ContractData, @@ -61,6 +67,8 @@ export class AztecNodeService implements AztecNode { protected sequencer: SequencerClient, protected chainId: number, protected version: number, + protected globalVariableBuilder: GlobalVariableBuilder, + protected merkleTreesDb: levelup.LevelUp, private log = createDebugLogger('aztec:node'), ) {} @@ -81,9 +89,10 @@ export class AztecNodeService implements AztecNode { const p2pClient = await createP2PClient(config, new InMemoryTxPool(), archiver); // now create the merkle trees and the world state syncher - const merkleTreeDB = await MerkleTrees.new(levelup(createMemDown()), await CircuitsWasm.get()); + const merkleTreesDb = levelup(createMemDown()); + const merkleTrees = await MerkleTrees.new(merkleTreesDb, await CircuitsWasm.get()); const worldStateConfig: WorldStateConfig = getWorldStateConfig(); - const worldStateSynchroniser = new ServerWorldStateSynchroniser(merkleTreeDB, archiver, worldStateConfig); + const worldStateSynchroniser = new ServerWorldStateSynchroniser(merkleTrees, archiver, worldStateConfig); // start both and wait for them to sync from the block source await Promise.all([p2pClient.start(), worldStateSynchroniser.start()]); @@ -108,6 +117,8 @@ export class AztecNodeService implements AztecNode { sequencer, config.chainId, config.version, + getGlobalVariableBuilder(config), + merkleTreesDb, ); } @@ -367,6 +378,37 @@ export class AztecNodeService implements AztecNode { ); } + /** + * Simulates the public part of a transaction with the current state. + * @param tx - The transaction to simulate. + **/ + public async simulatePublicCalls(tx: Tx) { + this.log.info(`Simulating tx ${await tx.getTxHash()}`); + const blockNumber = (await this.blockSource.getBlockNumber()) + 1; + const newGlobalVariables = await this.globalVariableBuilder.buildGlobalVariables(new Fr(blockNumber)); + const prevGlobalVariables = (await this.blockSource.getL2Block(-1))?.globalVariables ?? GlobalVariables.empty(); + + // Instantiate merkle trees so uncommited updates by this simulation are local to it. + // TODO we should be able to remove this after https://github.com/AztecProtocol/aztec-packages/issues/1869 + // So simulation of public functions doesn't affect the merkle trees. + const merkleTrees = new MerkleTrees(this.merkleTreesDb, this.log); + await merkleTrees.init(await CircuitsWasm.get(), { + globalVariables: prevGlobalVariables, + }); + + const publicProcessorFactory = new PublicProcessorFactory( + merkleTrees.asLatest(), + this.contractDataSource, + this.l1ToL2MessageSource, + ); + const processor = await publicProcessorFactory.create(prevGlobalVariables, newGlobalVariables); + const [, failedTxs] = await processor.process([tx]); + if (failedTxs.length) { + throw failedTxs[0].error; + } + this.log.info(`Simulated tx ${await tx.getTxHash()} succeeds`); + } + /** * Returns an instance of MerkleTreeOperations having first ensured the world state is fully synched * @returns An instance of a committed MerkleTreeOperations diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts index bdb66131827..6cdeba9a74b 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts @@ -3,6 +3,7 @@ import { collectEncryptedLogs, collectEnqueuedPublicFunctionCalls, collectUnencryptedLogs, + processAcvmError, } from '@aztec/acir-simulator'; import { AztecAddress, @@ -33,6 +34,7 @@ import { L2BlockL2Logs, LogType, NodeInfo, + SimulationError, Tx, TxExecutionRequest, TxHash, @@ -177,7 +179,7 @@ export class AztecRPCServer implements AztecRPC { return await this.node.getBlock(blockNumber); } - public async simulateTx(txRequest: TxExecutionRequest) { + public async simulateTx(txRequest: TxExecutionRequest, simulatePublic: boolean) { if (!txRequest.functionData.isPrivate) { throw new Error(`Public entrypoints are not allowed`); } @@ -191,6 +193,7 @@ export class AztecRPCServer implements AztecRPC { const newContract = deployedContractAddress ? await this.db.getContract(deployedContractAddress) : undefined; const tx = await this.#simulateAndProve(txRequest, newContract); + if (simulatePublic) await this.#simulatePublicCalls(tx); this.log.info(`Executed local simulation for ${await tx.getTxHash()}`); return tx; @@ -327,10 +330,17 @@ export class AztecRPCServer implements AztecRPC { const simulator = getAcirSimulator(this.db, this.node, this.node, this.node, this.keyStore, contractDataOracle); this.log('Executing simulator...'); - const result = await simulator.run(txRequest, functionAbi, contractAddress, portalContract); - this.log('Simulation completed!'); - - return result; + try { + const result = await simulator.run(txRequest, functionAbi, contractAddress, portalContract); + this.log('Simulation completed!'); + return result; + } catch (err) { + if (err instanceof SimulationError) { + await this.#enrichSimulationError(err); + this.log(err.toString()); + } + throw err; + } } /** @@ -353,17 +363,63 @@ export class AztecRPCServer implements AztecRPC { const simulator = getAcirSimulator(this.db, this.node, this.node, this.node, this.keyStore, contractDataOracle); this.log('Executing unconstrained simulator...'); - const result = await simulator.runUnconstrained( - execRequest, - from ?? AztecAddress.ZERO, - functionAbi, - contractAddress, - portalContract, - this.node, - ); - this.log('Unconstrained simulation completed!'); + try { + const result = await simulator.runUnconstrained( + execRequest, + from ?? AztecAddress.ZERO, + functionAbi, + contractAddress, + portalContract, + this.node, + ); + this.log('Unconstrained simulation completed!'); + + return result; + } catch (err) { + if (err instanceof SimulationError) { + await this.#enrichSimulationError(err); + this.log(err.toString()); + } + throw err; + } + } - return result; + /** + * Simulate the public part of a transaction. + * This allows to catch public execution errors before submitting the transaction. + * It can also be used for estimating gas in the future. + * @param tx - The transaction to be simulated. + */ + async #simulatePublicCalls(tx: Tx) { + try { + await this.node.simulatePublicCalls(tx); + } catch (err) { + // Try to fill in the noir call stack since the RPC server may have access to the debug metadata + if (err instanceof SimulationError) { + const callStack = err.getCallStack(); + const originalFailingFunction = callStack[callStack.length - 1]; + const contractDataOracle = new ContractDataOracle(this.db, this.node); + const debugInfo = await contractDataOracle.getFunctionDebugMetadata( + originalFailingFunction.contractAddress, + originalFailingFunction.functionSelector, + ); + if (debugInfo) { + const noirCallStack = processAcvmError(err.message, debugInfo); + if (noirCallStack) { + err.setNoirCallStack(noirCallStack); + err.updateMessage( + `Assertion failed in public execution: '${ + noirCallStack[noirCallStack.length - 1]?.locationText ?? 'Unknown' + }'`, + ); + } + } + await this.#enrichSimulationError(err); + this.log(err.toString()); + } + + throw err; + } } /** @@ -412,6 +468,39 @@ export class AztecRPCServer implements AztecRPC { return new Tx(publicInputs, proof, encryptedLogs, unencryptedLogs, enqueuedPublicFunctions, [extendedContractData]); } + /** + * Adds contract and function names to a simulation error. + * @param err - The error to enrich. + */ + async #enrichSimulationError(err: SimulationError) { + // Maps contract addresses to the set of functions selectors that were in error. + // Using strings because map and set don't use .equals() + const mentionedFunctions: Map> = new Map(); + + err.getCallStack().forEach(({ contractAddress, functionSelector }) => { + if (!mentionedFunctions.has(contractAddress.toString())) { + mentionedFunctions.set(contractAddress.toString(), new Set()); + } + mentionedFunctions.get(contractAddress.toString())!.add(functionSelector.toString()); + }); + + await Promise.all( + [...mentionedFunctions.entries()].map(async ([contractAddress, selectors]) => { + const parsedContractAddress = AztecAddress.fromString(contractAddress); + const contract = await this.db.getContract(parsedContractAddress); + if (contract) { + err.enrichWithContractName(parsedContractAddress, contract.name); + selectors.forEach(selector => { + const functionAbi = contract.functions.find(f => f.selector.toString() === selector); + if (functionAbi) { + err.enrichWithFunctionName(parsedContractAddress, functionAbi.selector, functionAbi.name); + } + }); + } + }), + ); + } + // HACK(#1639): this is a hack to fix ordering of public calls enqueued in the call stack. Since the private kernel // cannot keep track of side effects that happen after or before a nested call, we override the public call stack // it emits with whatever we got from the simulator collected enqueued calls. As a sanity check, we at least verify diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts index 9571dfd7ce9..20f5969f5bb 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/test/aztec_rpc_test_suite.ts @@ -107,7 +107,7 @@ export const aztecRpcTestSuite = (testName: string, aztecRpcSetup: () => Promise [], ); - await expect(async () => await rpc.simulateTx(txExecutionRequest)).rejects.toThrow( + await expect(async () => await rpc.simulateTx(txExecutionRequest, false)).rejects.toThrow( 'Public entrypoints are not allowed', ); }); diff --git a/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts b/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts index 6efde784367..c9450bbf887 100644 --- a/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts +++ b/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts @@ -55,8 +55,8 @@ export abstract class BaseWallet implements Wallet { getContracts(): Promise { return this.rpc.getContracts(); } - simulateTx(txRequest: TxExecutionRequest): Promise { - return this.rpc.simulateTx(txRequest); + simulateTx(txRequest: TxExecutionRequest, simulatePublic: boolean): Promise { + return this.rpc.simulateTx(txRequest, simulatePublic); } sendTx(tx: Tx): Promise { return this.rpc.sendTx(tx); diff --git a/yarn-project/aztec.js/src/contract/base_contract_interaction.ts b/yarn-project/aztec.js/src/contract/base_contract_interaction.ts index 07db8847daa..2412401f5df 100644 --- a/yarn-project/aztec.js/src/contract/base_contract_interaction.ts +++ b/yarn-project/aztec.js/src/contract/base_contract_interaction.ts @@ -12,6 +12,10 @@ export interface SendMethodOptions { * Sender's address initiating the transaction. */ origin?: AztecAddress; + /** + * Wether to skip the simulation of the public part of the transaction. + */ + skipPublicSimulation?: boolean; } /** @@ -38,7 +42,7 @@ export abstract class BaseContractInteraction { */ public async simulate(options: SendMethodOptions = {}): Promise { const txRequest = this.txRequest ?? (await this.create(options)); - this.tx = await this.rpc.simulateTx(txRequest); + this.tx = await this.rpc.simulateTx(txRequest, !options.skipPublicSimulation); return this.tx; } diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index 1e305ba7688..2eee79c81ee 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -535,7 +535,9 @@ describe('e2e_lending_contract', () => { { // Withdraw more than possible to test the revert. logger('Withdraw: trying to withdraw more than possible'); - const tx = lendingContract.methods.withdraw_public(recipient, 10n ** 9n).send({ origin: recipient }); + const tx = lendingContract.methods + .withdraw_public(recipient, 10n ** 9n) + .send({ origin: recipient, skipPublicSimulation: true }); await tx.isMined({ interval: 0.1 }); const receipt = await tx.getReceipt(); expect(receipt.status).toBe(TxStatus.DROPPED); @@ -601,7 +603,7 @@ describe('e2e_lending_contract', () => { const tx = lendingContract.methods ._deposit(recipient.toField(), 42n, collateralAsset.address) - .send({ origin: recipient }); + .send({ origin: recipient, skipPublicSimulation: true }); await tx.isMined({ interval: 0.1 }); const receipt = await tx.getReceipt(); expect(receipt.status).toBe(TxStatus.DROPPED); diff --git a/yarn-project/end-to-end/src/e2e_nested_contract.test.ts b/yarn-project/end-to-end/src/e2e_nested_contract.test.ts index 73429762f95..a2eb9fe420f 100644 --- a/yarn-project/end-to-end/src/e2e_nested_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_contract.test.ts @@ -71,6 +71,14 @@ describe('e2e_nested_contract', () => { expect(receipt.status).toBe(TxStatus.MINED); }, 100_000); + it('fails simulation if calling a function not allowed to be called externally', async () => { + await expect( + parentContract.methods + .entryPoint(childContract.address, childContract.methods.valueInternal.selector.toField()) + .simulate(), + ).rejects.toThrowError(/Assertion failed: '.*'/); + }, 100_000); + it('performs public nested calls', async () => { const tx = parentContract.methods .pubEntryPoint(childContract.address, childContract.methods.pubGetValue.selector.toField(), 42n) @@ -94,6 +102,14 @@ describe('e2e_nested_contract', () => { expect(await getChildStoredValue(childContract)).toEqual(42n); }, 100_000); + it('fails simulation if calling a public function not allowed to be called externally', async () => { + await expect( + parentContract.methods + .enqueueCallToChild(childContract.address, childContract.methods.pubIncValueInternal.selector.toField(), 42n) + .simulate(), + ).rejects.toThrowError(/Assertion failed in public execution: '.*'/); + }, 100_000); + // Fails with "solver opcode resolution error: cannot solve opcode: expression has too many unknowns %EXPR [ 0 ]%" // See https://github.com/noir-lang/noir/issues/1347 // Task to repair this test: https://github.com/AztecProtocol/aztec-packages/issues/1587 diff --git a/yarn-project/noir-contracts/src/contracts/child_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/child_contract/src/main.nr index e25cb6b1347..286482eac81 100644 --- a/yarn-project/noir-contracts/src/contracts/child_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/child_contract/src/main.nr @@ -24,6 +24,16 @@ contract Child { context.return_values.push(input + context.chain_id() + context.version()); } + // Returns a sum of the input and the chain id and version of the contract in private circuit public input's return_values. + // Can only be called from this contract. + #[aztec(private)] + fn valueInternal( + input: Field, + ) { + assert(inputs.call_context.msg_sender == inputs.call_context.storage_contract_address); + context.return_values.push(input + context.chain_id() + context.version()); + } + // Returns base_value + 42. #[aztec(public)] open fn pubGetValue(base_value: Field) { @@ -51,6 +61,17 @@ contract Child { context.return_values.push(new_value); } + // Increments `current_value` by `new_value`. Can only be called from this contract. + #[aztec(public)] + open fn pubIncValueInternal(new_value: Field) { + let storage = Storage::init(); + assert(inputs.call_context.msg_sender == inputs.call_context.storage_contract_address); + let old_value = storage.current_value.read(); + storage.current_value.write(old_value + new_value); + let _hash = emit_unencrypted_log(new_value); + context.return_values.push(new_value); + } + #[aztec(public)] open fn setValueTwiceWithNestedFirst() { let pubSetValueSelector = 0x5b0f91b0; diff --git a/yarn-project/rollup-provider/src/app.ts b/yarn-project/rollup-provider/src/app.ts index aea93a4aa49..7b1cf70ca74 100644 --- a/yarn-project/rollup-provider/src/app.ts +++ b/yarn-project/rollup-provider/src/app.ts @@ -1,7 +1,7 @@ import { Fr, HistoricBlockData } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { createDebugLogger } from '@aztec/foundation/log'; -import { AztecNode, MerkleTreeId, Tx, TxHash } from '@aztec/types'; +import { AztecNode, MerkleTreeId, SimulationError, Tx, TxHash } from '@aztec/types'; import Koa, { Context, DefaultState } from 'koa'; import Router from 'koa-router'; @@ -269,6 +269,24 @@ export function appFactory(node: AztecNode, prefix: string) { ctx.status = 200; }); + router.post('/tx-simulate', checkReady, async (ctx: Koa.Context) => { + const stream = new PromiseReadable(ctx.req); + const postData = (await stream.readAll()) as Buffer; + const tx = Tx.fromBuffer(postData); + try { + await node.simulatePublicCalls(tx); + } catch (err) { + if (err instanceof SimulationError) { + ctx.body = { + simulationError: err.toJSON(), + }; + ctx.status = 400; + } else { + throw err; + } + } + }); + const app = new Koa(); app.on('error', error => { logger.error(`KOA app-level error. ${JSON.stringify({ error })}`); diff --git a/yarn-project/sequencer-client/src/global_variable_builder/index.ts b/yarn-project/sequencer-client/src/global_variable_builder/index.ts index 8de0248afcc..91ab6caecb8 100644 --- a/yarn-project/sequencer-client/src/global_variable_builder/index.ts +++ b/yarn-project/sequencer-client/src/global_variable_builder/index.ts @@ -4,6 +4,7 @@ import { ViemReader } from './viem-reader.js'; export { SimpleTestGlobalVariableBuilder as SimpleGlobalVariableBuilder } from './global_builder.js'; export { GlobalReaderConfig } from './config.js'; +export { GlobalVariableBuilder } from './global_builder.js'; /** * Returns a new instance of the global variable builder. diff --git a/yarn-project/sequencer-client/src/index.ts b/yarn-project/sequencer-client/src/index.ts index fb6a33a566d..f95eed98741 100644 --- a/yarn-project/sequencer-client/src/index.ts +++ b/yarn-project/sequencer-client/src/index.ts @@ -4,6 +4,10 @@ export * from './publisher/index.js'; export * from './client/index.js'; export * from './mocks/verification_keys.js'; +// Used by the node to simulate public parts of transactions. Should these be moved to a shared library? +export * from './sequencer/public_processor.js'; +export * from './global_variable_builder/index.js'; + // Used by publisher test in e2e export { WasmRollupCircuitSimulator } from './simulator/rollup.js'; export { EmptyRollupProver } from './prover/empty.js'; diff --git a/yarn-project/sequencer-client/src/sequencer/processed_tx.ts b/yarn-project/sequencer-client/src/sequencer/processed_tx.ts index fbf80ff5f03..7c4ce3e64da 100644 --- a/yarn-project/sequencer-client/src/sequencer/processed_tx.ts +++ b/yarn-project/sequencer-client/src/sequencer/processed_tx.ts @@ -16,6 +16,20 @@ export type ProcessedTx = Pick { const [processed, failed] = await processor.process([tx]); expect(processed).toEqual([]); - expect(failed).toEqual([tx]); + expect(failed[0].tx).toEqual(tx); }); }); diff --git a/yarn-project/sequencer-client/src/sequencer/public_processor.ts b/yarn-project/sequencer-client/src/sequencer/public_processor.ts index 8ff1a223ca0..8b00cfa592c 100644 --- a/yarn-project/sequencer-client/src/sequencer/public_processor.ts +++ b/yarn-project/sequencer-client/src/sequencer/public_processor.ts @@ -46,10 +46,9 @@ import { MerkleTreeOperations } from '@aztec/world-state'; import { getVerificationKeys } from '../index.js'; import { EmptyPublicProver } from '../prover/empty.js'; import { PublicProver } from '../prover/index.js'; -import { PublicKernelCircuitSimulator } from '../simulator/index.js'; -import { getPublicExecutor } from '../simulator/public_executor.js'; +import { PublicKernelCircuitSimulator, getPublicExecutor } from '../simulator/index.js'; import { WasmPublicKernelCircuitSimulator } from '../simulator/public_kernel.js'; -import { ProcessedTx, makeEmptyProcessedTx, makeProcessedTx } from './processed_tx.js'; +import { FailedTx, ProcessedTx, makeEmptyProcessedTx, makeProcessedTx } from './processed_tx.js'; import { getHistoricBlockData } from './utils.js'; /** @@ -107,17 +106,22 @@ export class PublicProcessor { * @param txs - Txs to process. * @returns The list of processed txs with their circuit simulation outputs. */ - public async process(txs: Tx[]): Promise<[ProcessedTx[], Tx[]]> { + public async process(txs: Tx[]): Promise<[ProcessedTx[], FailedTx[]]> { + // The processor modifies the tx objects in place, so we need to clone them. + txs = txs.map(tx => Tx.fromJSON(tx.toJSON())); const result: ProcessedTx[] = []; - const failed: Tx[] = []; + const failed: FailedTx[] = []; for (const tx of txs) { this.log(`Processing tx ${await tx.getTxHash()}`); try { result.push(await this.processTx(tx)); } catch (err) { - this.log.error(`Error processing tx ${await tx.getTxHash()}: ${err}`); - failed.push(tx); + this.log.warn(`Error processing tx ${await tx.getTxHash()}: ${err}`); + failed.push({ + tx, + error: err instanceof Error ? err : new Error('Unknown error'), + }); } } return [result, failed]; diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index d473dec8e48..8f4afb9cc23 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -141,8 +141,9 @@ export class Sequencer { const processor = await this.publicProcessorFactory.create(prevGlobalVariables, newGlobalVariables); const [processedTxs, failedTxs] = await processor.process(validTxs); if (failedTxs.length > 0) { - this.log(`Dropping failed txs ${(await Tx.getHashes(failedTxs)).join(', ')}`); - await this.p2pClient.deleteTxs(await Tx.getHashes(failedTxs)); + const failedTxData = failedTxs.map(fail => fail.tx); + this.log(`Dropping failed txs ${(await Tx.getHashes(failedTxData)).join(', ')}`); + await this.p2pClient.deleteTxs(await Tx.getHashes(failedTxData)); } if (processedTxs.length === 0) { diff --git a/yarn-project/sequencer-client/src/simulator/index.ts b/yarn-project/sequencer-client/src/simulator/index.ts index a9ede566376..0cc49389219 100644 --- a/yarn-project/sequencer-client/src/simulator/index.ts +++ b/yarn-project/sequencer-client/src/simulator/index.ts @@ -8,6 +8,8 @@ import { RootRollupPublicInputs, } from '@aztec/circuits.js'; +export { getPublicExecutor } from './public_executor.js'; + /** * Circuit simulator for the rollup circuits. */ diff --git a/yarn-project/types/src/index.ts b/yarn-project/types/src/index.ts index 2dc0f8bddd9..9d0c7f314ea 100644 --- a/yarn-project/types/src/index.ts +++ b/yarn-project/types/src/index.ts @@ -15,6 +15,7 @@ export * from './logs/index.js'; export * from './merkle_tree_id.js'; export * from './mocks.js'; export * from './public_data_write.js'; +export * from './simulation_error.js'; export * from './tx/index.js'; export * from './tx_execution_request.js'; export * from './packed_arguments.js'; diff --git a/yarn-project/types/src/interfaces/aztec-node.ts b/yarn-project/types/src/interfaces/aztec-node.ts index 5d53f1a319a..ed61b46a11d 100644 --- a/yarn-project/types/src/interfaces/aztec-node.ts +++ b/yarn-project/types/src/interfaces/aztec-node.ts @@ -137,4 +137,11 @@ export interface AztecNode extends DataCommitmentProvider, L1ToL2MessageProvider * @returns The current committed block data. */ getHistoricBlockData(): Promise; + + /** + * Simulates the public part of a transaction with the current state. + * This currently just checks that the transaction execution succeeds. + * @param tx - The transaction to simulate. + **/ + simulatePublicCalls(tx: Tx): Promise; } diff --git a/yarn-project/types/src/interfaces/aztec_rpc.ts b/yarn-project/types/src/interfaces/aztec_rpc.ts index b30cbec92c1..5eded121bfe 100644 --- a/yarn-project/types/src/interfaces/aztec_rpc.ts +++ b/yarn-project/types/src/interfaces/aztec_rpc.ts @@ -139,10 +139,10 @@ export interface AztecRPC { * Throws an error if the contract or function is unknown. * * @param txRequest - An authenticated tx request ready for simulation - * @param optionalFromAddress - The address to simulate from + * @param simulatePublic - Whether to simulate the public part of the transaction. * @returns A Tx ready to send to the p2p pool for execution. */ - simulateTx(txRequest: TxExecutionRequest): Promise; + simulateTx(txRequest: TxExecutionRequest, simulatePublic: boolean): Promise; /** * Send a transaction. diff --git a/yarn-project/types/src/simulation_error.ts b/yarn-project/types/src/simulation_error.ts new file mode 100644 index 00000000000..9477e758984 --- /dev/null +++ b/yarn-project/types/src/simulation_error.ts @@ -0,0 +1,199 @@ +import { AztecAddress, FunctionSelector } from '@aztec/circuits.js'; + +/** + * Address and selector of a function that failed during simulation. + */ +export interface FailingFunction { + /** + * The address of the contract that failed. + */ + contractAddress: AztecAddress; + /** + * The name of the contract that failed. + */ + contractName?: string; + /** + * The selector of the function that failed. + */ + functionSelector: FunctionSelector; + /** + * The name of the function that failed. + */ + functionName?: string; +} + +/** + * A pointer to a failing section of the noir source code. + */ +export interface SourceCodeLocation { + /** + * The path to the source file. + */ + filePath: string; + /** + * The line number of the call. + */ + line: number; + /** + * The source code of the file. + */ + fileSource: string; + /** + * The source code text of the failed constraint. + */ + locationText: string; +} + +/** + * A stack of noir source code locations. + */ +export type NoirCallStack = SourceCodeLocation[]; + +/** + * An error during the simulation of a function call. + */ +export class SimulationError extends Error { + private functionErrorStack: FailingFunction[]; + + // We want to maintain a public constructor for proper printing. + constructor( + message: string, + failingFunction: FailingFunction, + private noirErrorStack?: NoirCallStack, + options?: ErrorOptions, + ) { + super(message, options); + this.functionErrorStack = [failingFunction]; + } + + private addCaller(failingFunction: FailingFunction) { + this.functionErrorStack.unshift(failingFunction); + } + + static fromError( + failingContract: AztecAddress, + failingselector: FunctionSelector, + err: Error & { + /** + * The noir call stack. + */ + callStack?: NoirCallStack; + }, + ) { + const failingFunction = { contractAddress: failingContract, functionSelector: failingselector }; + if (err instanceof SimulationError) { + return SimulationError.extendPreviousSimulationError(failingFunction, err); + } + return new SimulationError(err.message, failingFunction, err?.callStack, { + cause: err, + }); + } + + static extendPreviousSimulationError(failingFunction: FailingFunction, previousError: SimulationError) { + previousError.addCaller(failingFunction); + return previousError; + } + + /** + * Enriches the error with the name of a contract that failed. + * @param contractAddress - The address of the contract + * @param contractName - The corresponding name + */ + enrichWithContractName(contractAddress: AztecAddress, contractName: string) { + this.functionErrorStack.forEach(failingFunction => { + if (failingFunction.contractAddress.equals(contractAddress)) { + failingFunction.contractName = contractName; + } + }); + } + + /** + * Enriches the error with the name of a function that failed. + * @param contractAddress - The address of the contract + * @param functionSelector - The selector of the function + * @param functionName - The corresponding name + */ + enrichWithFunctionName(contractAddress: AztecAddress, functionSelector: FunctionSelector, functionName: string) { + this.functionErrorStack.forEach(failingFunction => { + if ( + failingFunction.contractAddress.equals(contractAddress) && + failingFunction.functionSelector.equals(functionSelector) + ) { + failingFunction.functionName = functionName; + } + }); + } + + /** + * Returns a string representation of the error. + * @returns The string. + */ + toString() { + const functionCallStack = this.getCallStack(); + const noirCallStack = this.getNoirCallStack(); + + // Try to resolve the contract and function names of the stack of failing functions. + const stackLines: string[] = [ + ...functionCallStack.map(failingFunction => { + return ` at ${failingFunction.contractName ?? failingFunction.contractAddress.toString()}.${ + failingFunction.functionName ?? failingFunction.functionSelector.toString() + }`; + }), + ...noirCallStack.map( + sourceCodeLocation => + ` at ${sourceCodeLocation.filePath}:${sourceCodeLocation.line} '${sourceCodeLocation.locationText}'`, + ), + ]; + + return [`Simulation error: ${this.message}`, ...stackLines.reverse()].join('\n'); + } + + /** + * Updates the error message. This is needed because in some engines the stack also contains the message. + * @param newMessage - The new message of this error. + */ + updateMessage(newMessage: string) { + const oldMessage = this.message; + this.message = newMessage; + if (this.stack?.startsWith(`Error: ${oldMessage}`)) { + this.stack = this.stack?.replace(`Error: ${oldMessage}`, `Error: ${newMessage}`); + } + } + + /** + * The aztec function stack that failed during simulation. + */ + getCallStack(): FailingFunction[] { + return this.functionErrorStack; + } + + /** + * Returns the noir call stack inside the first function that failed during simulation. + * @returns The noir call stack. + */ + getNoirCallStack(): NoirCallStack { + return this.noirErrorStack || []; + } + + /** + * Sets the noir call stack. + * @param callStack - The noir call stack. + */ + setNoirCallStack(callStack: NoirCallStack) { + this.noirErrorStack = callStack; + } + + toJSON() { + return { + message: this.message, + functionErrorStack: this.functionErrorStack, + noirErrorStack: this.noirErrorStack, + }; + } + + static fromJSON(obj: any) { + const error = new SimulationError(obj.message, obj.functionErrorStack[0], obj.noirErrorStack); + error.functionErrorStack = obj.functionErrorStack; + return error; + } +} diff --git a/yarn-project/world-state/src/world-state-db/merkle_trees.ts b/yarn-project/world-state/src/world-state-db/merkle_trees.ts index 1b502f5ba02..23166d50787 100644 --- a/yarn-project/world-state/src/world-state-db/merkle_trees.ts +++ b/yarn-project/world-state/src/world-state-db/merkle_trees.ts @@ -25,6 +25,7 @@ import { StandardIndexedTree, StandardTree, UpdateOnlyTree, + loadTree, newTree, } from '@aztec/merkle-tree'; import { L2Block, MerkleTreeId, SiblingPath, merkleTreeIds } from '@aztec/types'; @@ -43,6 +44,16 @@ import { TreeInfo, } from './index.js'; +/** + * Data necessary to reinitialise the merkle trees from Db. + */ +interface FromDbOptions { + /** + * The global variables from the last block. + */ + globalVariables: GlobalVariables; +} + /** * A convenience class for managing multiple merkle trees. */ @@ -58,18 +69,22 @@ export class MerkleTrees implements MerkleTreeDb { /** * Initialises the collection of Merkle Trees. * @param optionalWasm - WASM instance to use for hashing (if not provided PrimitivesWasm will be used). + * @param fromDbOptions - Options to initialise the trees from the database. */ - public async init(optionalWasm?: IWasmModule) { + public async init(optionalWasm?: IWasmModule, fromDbOptions?: FromDbOptions) { + const fromDb = fromDbOptions !== undefined; + const initialiseTree = fromDb ? loadTree : newTree; + const wasm = optionalWasm ?? (await CircuitsWasm.get()); const hasher = new Pedersen(wasm); - const contractTree: AppendOnlyTree = await newTree( + const contractTree: AppendOnlyTree = await initialiseTree( StandardTree, this.db, hasher, `${MerkleTreeId[MerkleTreeId.CONTRACT_TREE]}`, CONTRACT_TREE_HEIGHT, ); - const nullifierTree = await newTree( + const nullifierTree = await initialiseTree( StandardIndexedTree, this.db, hasher, @@ -77,28 +92,28 @@ export class MerkleTrees implements MerkleTreeDb { NULLIFIER_TREE_HEIGHT, INITIAL_NULLIFIER_TREE_SIZE, ); - const privateDataTree: AppendOnlyTree = await newTree( + const privateDataTree: AppendOnlyTree = await initialiseTree( StandardTree, this.db, hasher, `${MerkleTreeId[MerkleTreeId.PRIVATE_DATA_TREE]}`, PRIVATE_DATA_TREE_HEIGHT, ); - const publicDataTree: UpdateOnlyTree = await newTree( + const publicDataTree: UpdateOnlyTree = await initialiseTree( SparseTree, this.db, hasher, `${MerkleTreeId[MerkleTreeId.PUBLIC_DATA_TREE]}`, PUBLIC_DATA_TREE_HEIGHT, ); - const l1Tol2MessagesTree: AppendOnlyTree = await newTree( + const l1Tol2MessagesTree: AppendOnlyTree = await initialiseTree( StandardTree, this.db, hasher, `${MerkleTreeId[MerkleTreeId.L1_TO_L2_MESSAGES_TREE]}`, L1_TO_L2_MSG_TREE_HEIGHT, ); - const historicBlocksTree: AppendOnlyTree = await newTree( + const historicBlocksTree: AppendOnlyTree = await initialiseTree( StandardTree, this.db, hasher, @@ -110,10 +125,14 @@ export class MerkleTrees implements MerkleTreeDb { this.jobQueue.start(); // The first leaf in the blocks tree contains the empty roots of the other trees and empty global variables. - const initialGlobalVariablesHash = await computeGlobalVariablesHash(GlobalVariables.empty()); - await this._updateLatestGlobalVariablesHash(initialGlobalVariablesHash); - await this._updateHistoricBlocksTree(initialGlobalVariablesHash, true); - await this._commit(); + if (!fromDb) { + const initialGlobalVariablesHash = await computeGlobalVariablesHash(GlobalVariables.empty()); + await this._updateLatestGlobalVariablesHash(initialGlobalVariablesHash); + await this._updateHistoricBlocksTree(initialGlobalVariablesHash, true); + await this._commit(); + } else { + await this._updateLatestGlobalVariablesHash(await computeGlobalVariablesHash(fromDbOptions.globalVariables)); + } } /**