diff --git a/yarn-project/aztec.js/src/contract/batch_call.ts b/yarn-project/aztec.js/src/contract/batch_call.ts index 13b748a3baf2..a00de90544da 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.ts @@ -1,5 +1,4 @@ -import { ExecutionPayload } from '@aztec/entrypoints/payload'; -import { mergeExecutionPayloads } from '@aztec/entrypoints/payload'; +import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/entrypoints/payload'; import { type FunctionCall, FunctionType, decodeFromAbi } from '@aztec/stdlib/abi'; import type { TxExecutionRequest } from '@aztec/stdlib/tx'; diff --git a/yarn-project/bb-prover/src/prover/bb_private_kernel_prover.ts b/yarn-project/bb-prover/src/prover/bb_private_kernel_prover.ts index 5eb576813221..7b23f62a2c62 100644 --- a/yarn-project/bb-prover/src/prover/bb_private_kernel_prover.ts +++ b/yarn-project/bb-prover/src/prover/bb_private_kernel_prover.ts @@ -11,6 +11,7 @@ import { convertPrivateKernelTailInputsToWitnessMapWithAbi, convertPrivateKernelTailOutputsFromWitnessMapWithAbi, convertPrivateKernelTailToPublicInputsToWitnessMapWithAbi, + foreignCallHandler, getPrivateKernelResetArtifactName, updateResetCircuitSampleInputs, } from '@aztec/noir-protocol-circuits-types/client'; @@ -28,7 +29,7 @@ import type { PrivateKernelTailCircuitPrivateInputs, PrivateKernelTailCircuitPublicInputs, } from '@aztec/stdlib/kernel'; -import type { NoirCompiledCircuit } from '@aztec/stdlib/noir'; +import type { NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; import type { ClientIvcProof } from '@aztec/stdlib/proofs'; import type { CircuitSimulationStats, CircuitWitnessGenerationStats } from '@aztec/stdlib/stats'; @@ -159,15 +160,14 @@ export abstract class BBPrivateKernelProver implements PrivateKernelProver { convertInputs: (inputs: I, abi: Abi) => WitnessMap, convertOutputs: (outputs: WitnessMap, abi: Abi) => O, ): Promise> { - const compiledCircuit: NoirCompiledCircuit = await this.artifactProvider.getSimulatedClientCircuitArtifactByName( - circuitType, - ); + const compiledCircuit: NoirCompiledCircuitWithName = + await this.artifactProvider.getSimulatedClientCircuitArtifactByName(circuitType); const witnessMap = convertInputs(inputs, compiledCircuit.abi); const timer = new Timer(); const outputWitness = await this.simulationProvider - .executeProtocolCircuit(witnessMap, compiledCircuit) + .executeProtocolCircuit(witnessMap, compiledCircuit, foreignCallHandler) .catch((err: Error) => { this.log.debug(`Failed to simulate ${circuitType}`, { circuitName: mapProtocolArtifactNameToCircuitName(circuitType), @@ -198,13 +198,17 @@ export abstract class BBPrivateKernelProver implements PrivateKernelProver { convertOutputs: (outputs: WitnessMap, abi: Abi) => O, ): Promise> { this.log.debug(`Generating witness for ${circuitType}`); - const compiledCircuit: NoirCompiledCircuit = await this.artifactProvider.getClientCircuitArtifactByName( + const compiledCircuit: NoirCompiledCircuitWithName = await this.artifactProvider.getClientCircuitArtifactByName( circuitType, ); const witnessMap = convertInputs(inputs, compiledCircuit.abi); const timer = new Timer(); - const outputWitness = await this.simulationProvider.executeProtocolCircuit(witnessMap, compiledCircuit); + const outputWitness = await this.simulationProvider.executeProtocolCircuit( + witnessMap, + compiledCircuit, + foreignCallHandler, + ); const output = convertOutputs(outputWitness, compiledCircuit.abi); this.log.debug(`Generated witness for ${circuitType}`, { diff --git a/yarn-project/bb-prover/src/prover/bb_prover.ts b/yarn-project/bb-prover/src/prover/bb_prover.ts index 500662bd165a..bc8af867d63d 100644 --- a/yarn-project/bb-prover/src/prover/bb_prover.ts +++ b/yarn-project/bb-prover/src/prover/bb_prover.ts @@ -14,7 +14,6 @@ import { createLogger } from '@aztec/foundation/log'; import { BufferReader } from '@aztec/foundation/serialize'; import { Timer } from '@aztec/foundation/timer'; import { - ServerCircuitArtifacts, type ServerProtocolArtifact, convertBaseParityInputsToWitnessMap, convertBaseParityOutputsFromWitnessMap, @@ -36,6 +35,7 @@ import { convertRootRollupOutputsFromWitnessMap, convertSingleTxBlockRootRollupInputsToWitnessMap, convertSingleTxBlockRootRollupOutputsFromWitnessMap, + getServerCircuitArtifact, } from '@aztec/noir-protocol-circuits-types/server'; import { ServerCircuitVks } from '@aztec/noir-protocol-circuits-types/server/vks'; import type { WitnessMap } from '@aztec/noir-types'; @@ -411,13 +411,14 @@ export class BBNativeRollupProver implements ServerCircuitProver { outputWitnessFile, ); - const artifact = ServerCircuitArtifacts[circuitType]; + const artifact = getServerCircuitArtifact(circuitType); logger.debug(`Generating witness data for ${circuitType}`); const inputWitness = convertInput(input); const timer = new Timer(); - const outputWitness = await simulator.executeProtocolCircuit(inputWitness, artifact); + const foreignCallHandler = undefined; // We don't handle foreign calls in the native ACVM simulator + const outputWitness = await simulator.executeProtocolCircuit(inputWitness, artifact, foreignCallHandler); const output = convertOutput(outputWitness); const circuitName = mapProtocolArtifactNameToCircuitName(circuitType); diff --git a/yarn-project/bb-prover/src/test/test_circuit_prover.ts b/yarn-project/bb-prover/src/test/test_circuit_prover.ts index 9601a9e533ea..3e4b61fd756c 100644 --- a/yarn-project/bb-prover/src/test/test_circuit_prover.ts +++ b/yarn-project/bb-prover/src/test/test_circuit_prover.ts @@ -11,7 +11,6 @@ import { sleep } from '@aztec/foundation/sleep'; import { Timer } from '@aztec/foundation/timer'; import { type ServerProtocolArtifact, - SimulatedServerCircuitArtifacts, convertBaseParityInputsToWitnessMap, convertBaseParityOutputsFromWitnessMap, convertBlockMergeRollupInputsToWitnessMap, @@ -32,6 +31,8 @@ import { convertSimulatedPublicBaseRollupOutputsFromWitnessMap, convertSimulatedSingleTxBlockRootRollupInputsToWitnessMap, convertSimulatedSingleTxBlockRootRollupOutputsFromWitnessMap, + foreignCallHandler, + getSimulatedServerCircuitArtifact, } from '@aztec/noir-protocol-circuits-types/server'; import { ProtocolCircuitVks } from '@aztec/noir-protocol-circuits-types/server/vks'; import type { WitnessMap } from '@aztec/noir-types'; @@ -347,16 +348,26 @@ export class TestCircuitProver implements ServerCircuitProver { const witnessMap = convertInput(input); const circuitName = mapProtocolArtifactNameToCircuitName(artifactName); - let simulationProvider = this.simulationProvider ?? this.wasmSimulator; - if (['BlockRootRollupArtifact', 'SingleTxBlockRootRollupArtifact'].includes(artifactName)) { - // TODO(#10323): temporarily force block root to use wasm while we simulate - // the blob operations with an oracle. Appears to be no way to provide nativeACVM with a foreign call hander. - simulationProvider = this.wasmSimulator; + let witness: WitnessMap; + if ( + ['BlockRootRollupArtifact', 'SingleTxBlockRootRollupArtifact'].includes(artifactName) || + this.simulationProvider == undefined + ) { + // TODO(#10323): Native ACVM simulator does not support foreign call handler so we use the wasm simulator + // when simulating block root rollup and single tx block root rollup circuits or when the native ACVM simulator + // is not provided. + witness = await this.wasmSimulator.executeProtocolCircuit( + witnessMap, + getSimulatedServerCircuitArtifact(artifactName), + foreignCallHandler, + ); + } else { + witness = await this.simulationProvider.executeProtocolCircuit( + witnessMap, + getSimulatedServerCircuitArtifact(artifactName), + undefined, // Native ACM simulator does not support foreign call handler + ); } - const witness = await simulationProvider.executeProtocolCircuit( - witnessMap, - SimulatedServerCircuitArtifacts[artifactName], - ); const result = convertOutput(witness); diff --git a/yarn-project/end-to-end/bootstrap.sh b/yarn-project/end-to-end/bootstrap.sh index 0aca7cecd169..1996eddd802d 100755 --- a/yarn-project/end-to-end/bootstrap.sh +++ b/yarn-project/end-to-end/bootstrap.sh @@ -113,6 +113,9 @@ function test_cmds { echo "$prefix simple e2e_token_contract/transfer_to_public" echo "$prefix simple e2e_token_contract/transfer.test" + # circuit_recorder sub-tests + echo "$prefix simple e2e_circuit_recorder" + # compose-based tests (use running sandbox) echo "$prefix compose composed/docs_examples" echo "$prefix compose composed/e2e_pxe" diff --git a/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts b/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts new file mode 100644 index 000000000000..f0f70e9b8c7c --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts @@ -0,0 +1,69 @@ +import fs from 'fs/promises'; +import path from 'path'; + +import { setup } from './fixtures/utils.js'; + +/** + * Tests the circuit recorder is working as expected. To read more about it, check JSDoc of CircuitRecorder class. + */ +describe('Circuit Recorder', () => { + const RECORD_DIR = './circuit_recordings'; + + it('records circuit execution', async () => { + // Set recording directory env var - this will activate the circuit recorder + process.env.CIRCUIT_RECORD_DIR = RECORD_DIR; + + // Run setup which deploys an account contract and runs kernels + const { teardown } = await setup(1); + + // Check recording directory exists + const dirExists = await fs.stat(RECORD_DIR).then( + stats => stats.isDirectory(), + () => false, + ); + expect(dirExists).toBe(true); + + // Check recording file of a user circuit (contract circuit) exists and has expected content + { + const files = await fs.readdir(RECORD_DIR); + expect(files.length).toBeGreaterThan(0); + + const recordingFile = files.find(f => f.startsWith('SchnorrAccount_constructor')); + expect(recordingFile).toBeDefined(); + + const recordingContent = await fs.readFile(path.join(RECORD_DIR, recordingFile!), 'utf8'); + const recording = JSON.parse(recordingContent); + + expect(recording).toMatchObject({ + circuitName: 'SchnorrAccount', + functionName: 'constructor', + inputs: expect.any(Object), + oracleCalls: expect.any(Array), + }); + } + + // Then we'll do the same for a protocol circuit + { + const files = await fs.readdir(RECORD_DIR); + expect(files.length).toBeGreaterThan(0); + + const recordingFile = files.find(f => f.startsWith('PrivateKernelInit_main')); + expect(recordingFile).toBeDefined(); + + const recordingContent = await fs.readFile(path.join(RECORD_DIR, recordingFile!), 'utf8'); + const recording = JSON.parse(recordingContent); + + expect(recording).toMatchObject({ + circuitName: 'PrivateKernelInit', + functionName: 'main', + inputs: expect.any(Object), + oracleCalls: expect.any(Array), + }); + } + + // Cleanup + await fs.rm(RECORD_DIR, { recursive: true, force: true }); + delete process.env.CIRCUIT_RECORD_DIR; + await teardown(); + }, 20_000); +}); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index f2cfacde9633..4036ef958064 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -27,7 +27,6 @@ import { } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; import { AnvilTestWatcher, CheatCodes } from '@aztec/aztec.js/testing'; -import type { BBNativePrivateKernelProver } from '@aztec/bb-prover'; import { createBlobSinkClient } from '@aztec/blob-sink/client'; import { type BlobSinkServer, createBlobSinkServer } from '@aztec/blob-sink/server'; import { FEE_JUICE_INITIAL_MINT, GENESIS_ARCHIVE_ROOT, GENESIS_BLOCK_HASH } from '@aztec/constants'; @@ -52,9 +51,16 @@ import { FeeJuiceContract } from '@aztec/noir-contracts.js/FeeJuice'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import { ProtocolContractAddress, protocolContractTreeRoot } from '@aztec/protocol-contracts'; import { type ProverNode, type ProverNodeConfig, createProverNode } from '@aztec/prover-node'; -import { type PXEService, type PXEServiceConfig, createPXEService, getPXEServiceConfig } from '@aztec/pxe/server'; +import { + type PXEService, + type PXEServiceConfig, + createPXEServiceWithSimulationProvider, + getPXEServiceConfig, +} from '@aztec/pxe/server'; import type { SequencerClient } from '@aztec/sequencer-client'; import type { TestSequencerClient } from '@aztec/sequencer-client/test'; +import { WASMSimulator } from '@aztec/simulator/client'; +import { SimulationProviderRecorderWrapper } from '@aztec/simulator/testing'; import { getContractClassFromArtifact } from '@aztec/stdlib/contract'; import { Gas } from '@aztec/stdlib/gas'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; @@ -135,18 +141,15 @@ export const setupL1Contracts = async ( * Sets up Private eXecution Environment (PXE). * @param aztecNode - An instance of Aztec Node. * @param opts - Partial configuration for the PXE service. - * @param firstPrivKey - The private key of the first account to be created. * @param logger - The logger to be used. * @param useLogSuffix - Whether to add a randomly generated suffix to the PXE debug logs. - * @param proofCreator - An optional proof creator to use - * @returns Private eXecution Environment (PXE), accounts, wallets and logger. + * @returns Private eXecution Environment (PXE), logger and teardown function. */ export async function setupPXEService( aztecNode: AztecNode, opts: Partial = {}, logger = getLogger(), useLogSuffix = false, - proofCreator?: BBNativePrivateKernelProver, ): Promise<{ /** * The PXE instance. @@ -169,7 +172,14 @@ export async function setupPXEService( pxeServiceConfig.dataDirectory = path.join(tmpdir(), randomBytes(8).toString('hex')); } - const pxe = await createPXEService(aztecNode, pxeServiceConfig, useLogSuffix, proofCreator); + const simulationProvider = new WASMSimulator(); + const simulationProviderWithRecorder = new SimulationProviderRecorderWrapper(simulationProvider); + const pxe = await createPXEServiceWithSimulationProvider( + aztecNode, + simulationProviderWithRecorder, + pxeServiceConfig, + useLogSuffix, + ); const teardown = async () => { if (!configuredDataDirectory) { diff --git a/yarn-project/noir-protocol-circuits-types/src/artifacts/client/bundle.ts b/yarn-project/noir-protocol-circuits-types/src/artifacts/client/bundle.ts index ea2c0f56e7b2..db359dc59ac7 100644 --- a/yarn-project/noir-protocol-circuits-types/src/artifacts/client/bundle.ts +++ b/yarn-project/noir-protocol-circuits-types/src/artifacts/client/bundle.ts @@ -1,4 +1,4 @@ -import type { NoirCompiledCircuit } from '@aztec/stdlib/noir'; +import type { NoirCompiledCircuit, NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; import type { VerificationKeyData } from '@aztec/stdlib/vks'; import PrivateKernelInitJson from '../../../artifacts/private_kernel_init.json' assert { type: 'json' }; @@ -30,12 +30,12 @@ export const SimulatedClientCircuitArtifacts: Record { - return Promise.resolve(ClientCircuitArtifacts[artifact]); + getClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise { + return Promise.resolve({ ...ClientCircuitArtifacts[artifact], name: artifact.replace('Artifact', '') }); } - getSimulatedClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise { - return Promise.resolve(SimulatedClientCircuitArtifacts[artifact]); + getSimulatedClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise { + return Promise.resolve({ ...SimulatedClientCircuitArtifacts[artifact], name: artifact.replace('Artifact', '') }); } getCircuitVkByName(artifact: ClientProtocolArtifact): Promise { diff --git a/yarn-project/noir-protocol-circuits-types/src/artifacts/client/lazy.ts b/yarn-project/noir-protocol-circuits-types/src/artifacts/client/lazy.ts index ee0e7869485e..2a758b11e644 100644 --- a/yarn-project/noir-protocol-circuits-types/src/artifacts/client/lazy.ts +++ b/yarn-project/noir-protocol-circuits-types/src/artifacts/client/lazy.ts @@ -1,4 +1,4 @@ -import type { NoirCompiledCircuit } from '@aztec/stdlib/noir'; +import type { NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; import type { VerificationKeyData } from '@aztec/stdlib/vks'; import { @@ -9,11 +9,11 @@ import { import type { ArtifactProvider, ClientProtocolArtifact } from '../types.js'; export class LazyArtifactProvider implements ArtifactProvider { - getClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise { + getClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise { return getClientCircuitArtifact(ClientCircuitArtifactNames[artifact], false); } - getSimulatedClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise { + getSimulatedClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise { return getClientCircuitArtifact(ClientCircuitArtifactNames[artifact], true); } diff --git a/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts b/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts index 1f39846c1ca4..d633c357f3d0 100644 --- a/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts +++ b/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts @@ -1,4 +1,4 @@ -import type { NoirCompiledCircuit } from '@aztec/stdlib/noir'; +import type { NoirCompiledCircuit, NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; import BaseParityJson from '../../artifacts/parity_base.json' assert { type: 'json' }; import RootParityJson from '../../artifacts/parity_root.json' assert { type: 'json' }; @@ -41,3 +41,17 @@ export const SimulatedServerCircuitArtifacts: Record; - getSimulatedClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise; + getClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise; + getSimulatedClientCircuitArtifactByName(artifact: ClientProtocolArtifact): Promise; getCircuitVkByName(artifact: ClientProtocolArtifact): Promise; } diff --git a/yarn-project/noir-protocol-circuits-types/src/scripts/generate_client_artifacts_helper.ts b/yarn-project/noir-protocol-circuits-types/src/scripts/generate_client_artifacts_helper.ts index 39c7fcb4bb09..be81b35fb34a 100644 --- a/yarn-project/noir-protocol-circuits-types/src/scripts/generate_client_artifacts_helper.ts +++ b/yarn-project/noir-protocol-circuits-types/src/scripts/generate_client_artifacts_helper.ts @@ -19,7 +19,7 @@ const ClientCircuitArtifactNames: Record = { function generateImports() { return ` - import type { NoirCompiledCircuit } from '@aztec/stdlib/noir'; + import type { NoirCompiledCircuit, NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; import type { ClientProtocolArtifact } from './artifacts/types.js'; import { VerificationKeyData } from '@aztec/stdlib/vks'; import { keyJsonToVKData } from './utils/vk_json.js'; @@ -54,12 +54,12 @@ function generateCircuitArtifactImportFunction() { // In the meantime, this lazy import is INCOMPATIBLE WITH NODEJS return `case '${artifactName}': { const { default: compiledCircuit } = await import(\"../artifacts/${artifactName}.json\"); - return compiledCircuit as NoirCompiledCircuit; + return { ...(compiledCircuit as NoirCompiledCircuit), name: '${artifactName}' }; }`; }); return ` - export async function getClientCircuitArtifact(artifactName: string, simulated: boolean): Promise { + export async function getClientCircuitArtifact(artifactName: string, simulated: boolean): Promise { const isReset = artifactName.includes('private_kernel_reset'); const normalizedArtifactName = isReset ? \`\${simulated ? artifactName.replace('private_kernel_reset', 'private_kernel_reset_simulated') : artifactName}\` @@ -98,7 +98,7 @@ function generateVkImportFunction() { const main = async () => { const content = ` /* eslint-disable camelcase */ - // GENERATED FILE - DO NOT EDIT. RUN \`yarn generate\` or \`yarn generate:client-artifacts-helper\` + // GENERATED FILE - DO NOT EDIT. RUN \`yarn generate\` in the noir-protocol-circuits-types package to update. ${generateImports()} diff --git a/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts b/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts index eefd55037115..e34122a56045 100644 --- a/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts +++ b/yarn-project/noir-protocol-circuits-types/src/utils/server/foreign_call_handler.ts @@ -19,7 +19,7 @@ export async function foreignCallHandler(name: string, args: ForeignCallInput[]) // TODO(#10323): this was added to save simulation time (~1min in ACVM, ~3mins in wasm -> 500ms). // The use of bignum adds a lot of unconstrained code which overloads limits when simulating. // If/when simulation times of unconstrained are improved, remove this. - // Create and evaulate our blobs: + // Create and evaluate our blobs: const paddedBlobsAsFr: Fr[] = args[0].map((field: string) => Fr.fromString(field)); const kzgCommitments = args[1].map((field: string) => Fr.fromString(field)); const spongeBlob = SpongeBlob.fromFields( diff --git a/yarn-project/pxe/src/entrypoints/server/utils.ts b/yarn-project/pxe/src/entrypoints/server/utils.ts index 2596e3795db1..1bca882e6cbe 100644 --- a/yarn-project/pxe/src/entrypoints/server/utils.ts +++ b/yarn-project/pxe/src/entrypoints/server/utils.ts @@ -5,28 +5,43 @@ import { createLogger } from '@aztec/foundation/log'; import { createStore } from '@aztec/kv-store/lmdb-v2'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { type SimulationProvider, WASMSimulator } from '@aztec/simulator/client'; -import type { AztecNode, PrivateKernelProver } from '@aztec/stdlib/interfaces/client'; +import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { PXEServiceConfig } from '../../config/index.js'; import { PXEService } from '../../pxe_service/pxe_service.js'; import { PXE_DATA_SCHEMA_VERSION } from '../../storage/index.js'; /** - * Create and start an PXEService instance with the given AztecNode. - * If no keyStore or database is provided, it will use KeyStore and MemoryDB as default values. - * Returns a Promise that resolves to the started PXEService instance. + * Create and start an PXEService instance with the given AztecNode and config. * * @param aztecNode - The AztecNode instance to be used by the server. * @param config - The PXE Service Config to use - * @param useLogSuffix - (Optional) Log suffix for PXE's logger. - * @param proofCreator - An optional proof creator to use in place of any other configuration + * @param useLogSuffix - Whether to add a randomly generated suffix to the PXE debug logs. * @returns A Promise that resolves to the started PXEService instance. */ -export async function createPXEService( +export function createPXEService( aztecNode: AztecNode, config: PXEServiceConfig, useLogSuffix: string | boolean | undefined = undefined, - proofCreator?: PrivateKernelProver, +) { + const simulationProvider = new WASMSimulator(); + return createPXEServiceWithSimulationProvider(aztecNode, simulationProvider, config, useLogSuffix); +} + +/** + * Create and start an PXEService instance with the given AztecNode, SimulationProvider and config. + * + * @param aztecNode - The AztecNode instance to be used by the server. + * @param simulationProvider - The SimulationProvider to use + * @param config - The PXE Service Config to use + * @param useLogSuffix - Whether to add a randomly generated suffix to the PXE debug logs. + * @returns A Promise that resolves to the started PXEService instance. + */ +export async function createPXEServiceWithSimulationProvider( + aztecNode: AztecNode, + simulationProvider: SimulationProvider, + config: PXEServiceConfig, + useLogSuffix: string | boolean | undefined = undefined, ) { const logSuffix = typeof useLogSuffix === 'boolean' ? (useLogSuffix ? randomBytes(3).toString('hex') : undefined) : useLogSuffix; @@ -44,8 +59,7 @@ export async function createPXEService( createLogger('pxe:data:lmdb'), ); - const simulationProvider = new WASMSimulator(); - const prover = proofCreator ?? (await createProver(config, simulationProvider, logSuffix)); + const prover = await createProver(config, simulationProvider, logSuffix); const protocolContractsProvider = new BundledProtocolContractsProvider(); const pxe = await PXEService.create( aztecNode, diff --git a/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.ts b/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.ts index 18382fd8a7b7..cfd3ac12baf9 100644 --- a/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.ts +++ b/yarn-project/pxe/src/pxe_oracle_interface/pxe_oracle_interface.ts @@ -12,6 +12,7 @@ import { } from '@aztec/simulator/client'; import { type FunctionArtifact, + type FunctionArtifactWithContractName, FunctionCall, FunctionSelector, FunctionType, @@ -100,7 +101,10 @@ export class PXEOracleInterface implements ExecutionDataProvider { })); } - async getFunctionArtifact(contractAddress: AztecAddress, selector: FunctionSelector): Promise { + async getFunctionArtifact( + contractAddress: AztecAddress, + selector: FunctionSelector, + ): Promise { const artifact = await this.contractDataProvider.getFunctionArtifact(contractAddress, selector); const debug = await this.contractDataProvider.getFunctionDebugMetadata(contractAddress, selector); return { @@ -112,7 +116,7 @@ export class PXEOracleInterface implements ExecutionDataProvider { async getFunctionArtifactByName( contractAddress: AztecAddress, functionName: string, - ): Promise { + ): Promise { const instance = await this.contractDataProvider.getContractInstance(contractAddress); const artifact = await this.contractDataProvider.getContractArtifact(instance.currentContractClassId); return artifact && getFunctionArtifact(artifact, functionName); diff --git a/yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.ts b/yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.ts index adb494d64ab7..fadb0d6612b9 100644 --- a/yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.ts +++ b/yarn-project/pxe/src/storage/contract_data_provider/contract_data_provider.ts @@ -6,6 +6,7 @@ import { ContractClassNotFoundError, ContractNotFoundError } from '@aztec/simula import { type ContractArtifact, type FunctionArtifact, + type FunctionArtifactWithContractName, type FunctionDebugMetadata, FunctionSelector, FunctionType, @@ -168,9 +169,17 @@ export class ContractDataProvider implements DataProvider { * @param selector - The function selector. * @returns The corresponding function's artifact as an object. */ - public async getFunctionArtifact(contractAddress: AztecAddress, selector: FunctionSelector) { + public async getFunctionArtifact( + contractAddress: AztecAddress, + selector: FunctionSelector, + ): Promise { const tree = await this.getTreeForAddress(contractAddress); - return tree.getFunctionArtifact(selector); + const contractArtifact = tree.getArtifact(); + const functionArtifact = await tree.getFunctionArtifact(selector); + return { + ...functionArtifact, + contractName: contractArtifact.name, + }; } /** diff --git a/yarn-project/simulator/package.json b/yarn-project/simulator/package.json index eb9b280cf94c..92ce8c724c46 100644 --- a/yarn-project/simulator/package.json +++ b/yarn-project/simulator/package.json @@ -5,6 +5,7 @@ "exports": { "./server": "./dest/server.js", "./client": "./dest/client.js", + "./testing": "./dest/testing.js", "./public/fixtures": "./dest/public/fixtures/index.js" }, "typedocOptions": { diff --git a/yarn-project/simulator/src/private/acvm/acvm.ts b/yarn-project/simulator/src/private/acvm/acvm.ts index f06bb5e42cfc..8fed7c024359 100644 --- a/yarn-project/simulator/src/private/acvm/acvm.ts +++ b/yarn-project/simulator/src/private/acvm/acvm.ts @@ -32,6 +32,11 @@ export interface ACIRExecutionResult { /** * The function call that executes an ACIR. + * @param acir - The ACIR circuit bytecode to execute. + * @param initialWitness - The initial witness map defining all of the inputs to `circuit`. + * @param callback - A callback to process any foreign calls from the circuit. + * @returns The solved witness calculated by executing the circuit on the provided inputs, as well as the return + * witness indices as specified by the circuit. */ export async function acvm( acir: Buffer, diff --git a/yarn-project/simulator/src/private/execution_data_provider.ts b/yarn-project/simulator/src/private/execution_data_provider.ts index 21ba493c4b41..d409e3ecc5b0 100644 --- a/yarn-project/simulator/src/private/execution_data_provider.ts +++ b/yarn-project/simulator/src/private/execution_data_provider.ts @@ -1,5 +1,5 @@ import type { Fr, Point } from '@aztec/foundation/fields'; -import type { FunctionArtifact, FunctionSelector } from '@aztec/stdlib/abi'; +import type { FunctionArtifact, FunctionArtifactWithContractName, FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2Block } from '@aztec/stdlib/block'; import type { CompleteAddress, ContractInstance } from '@aztec/stdlib/contract'; @@ -83,7 +83,10 @@ export interface ExecutionDataProvider extends CommitmentsDBInterface { * @param selector - The corresponding function selector. * @returns A Promise that resolves to a FunctionArtifact object. */ - getFunctionArtifact(contractAddress: AztecAddress, selector: FunctionSelector): Promise; + getFunctionArtifact( + contractAddress: AztecAddress, + selector: FunctionSelector, + ): Promise; /** * Generates a stable function name for debug purposes. diff --git a/yarn-project/simulator/src/private/private_execution.ts b/yarn-project/simulator/src/private/private_execution.ts index e15f2cf84803..4fb73487d6e1 100644 --- a/yarn-project/simulator/src/private/private_execution.ts +++ b/yarn-project/simulator/src/private/private_execution.ts @@ -3,7 +3,12 @@ import { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; -import { type FunctionArtifact, type FunctionSelector, countArgumentsSize } from '@aztec/stdlib/abi'; +import { + type FunctionArtifact, + type FunctionArtifactWithContractName, + type FunctionSelector, + countArgumentsSize, +} from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractInstance } from '@aztec/stdlib/contract'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; @@ -25,19 +30,18 @@ import type { SimulationProvider } from './providers/simulation_provider.js'; export async function executePrivateFunction( simulator: SimulationProvider, privateExecutionOracle: PrivateExecutionOracle, - artifact: FunctionArtifact, + artifact: FunctionArtifactWithContractName, contractAddress: AztecAddress, functionSelector: FunctionSelector, log = createLogger('simulator:private_execution'), ): Promise { const functionName = await privateExecutionOracle.getDebugFunctionName(); log.verbose(`Executing private function ${functionName}`, { contract: contractAddress }); - const acir = artifact.bytecode; const initialWitness = privateExecutionOracle.getInitialWitness(artifact); const acvmCallback = new Oracle(privateExecutionOracle); const timer = new Timer(); const acirExecutionResult = await simulator - .executeUserCircuit(acir, initialWitness, acvmCallback) + .executeUserCircuit(initialWitness, artifact, acvmCallback) .catch((err: Error) => { err.message = resolveAssertionMessageFromError(err, artifact); throw new ExecutionError( @@ -79,7 +83,7 @@ export async function executePrivateFunction( log.debug(`Returning from call to ${contractAddress.toString()}:${functionSelector}`); return new PrivateCallExecutionResult( - acir, + artifact.bytecode, Buffer.from(artifact.verificationKey!, 'base64'), partialWitness, publicInputs, diff --git a/yarn-project/simulator/src/private/providers/acvm_native.ts b/yarn-project/simulator/src/private/providers/acvm_native.ts index 9f2b1d23609b..5bb33398d703 100644 --- a/yarn-project/simulator/src/private/providers/acvm_native.ts +++ b/yarn-project/simulator/src/private/providers/acvm_native.ts @@ -1,8 +1,10 @@ import { runInDirectory } from '@aztec/foundation/fs'; import { createLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; -import type { WitnessMap } from '@aztec/noir-types'; -import type { NoirCompiledCircuit } from '@aztec/stdlib/noir'; +import type { WitnessMap } from '@aztec/noir-acvm_js'; +import type { ForeignCallHandler } from '@aztec/noir-protocol-circuits-types/types'; +import type { FunctionArtifactWithContractName } from '@aztec/stdlib/abi'; +import type { NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; import * as proc from 'child_process'; import { promises as fs } from 'fs'; @@ -136,12 +138,21 @@ export async function executeNativeCircuit( export class NativeACVMSimulator implements SimulationProvider { constructor(private workingDirectory: string, private pathToAcvm: string, private witnessFilename?: string) {} - async executeProtocolCircuit(input: WitnessMap, compiledCircuit: NoirCompiledCircuit): Promise { + + async executeProtocolCircuit( + input: ACVMWitness, + artifact: NoirCompiledCircuitWithName, + callback: ForeignCallHandler | undefined, + ): Promise { // Execute the circuit on those initial witness values + if (callback) { + throw new Error('Native ACVM simulator does not support foreign calls. Ignoring callback.'); + } + const operation = async (directory: string) => { // Decode the bytecode from base64 since the acvm does not know about base64 encoding - const decodedBytecode = Buffer.from(compiledCircuit.bytecode, 'base64'); + const decodedBytecode = Buffer.from(artifact.bytecode, 'base64'); // Execute the circuit const result = await executeNativeCircuit( input, @@ -162,8 +173,8 @@ export class NativeACVMSimulator implements SimulationProvider { } executeUserCircuit( - _acir: Buffer, - _initialWitness: ACVMWitness, + _input: ACVMWitness, + _artifact: FunctionArtifactWithContractName, _callback: ACIRCallback, ): Promise { throw new Error('Not implemented'); diff --git a/yarn-project/simulator/src/private/providers/acvm_wasm.ts b/yarn-project/simulator/src/private/providers/acvm_wasm.ts index 5327ebd70115..3b10b8b5d9ab 100644 --- a/yarn-project/simulator/src/private/providers/acvm_wasm.ts +++ b/yarn-project/simulator/src/private/providers/acvm_wasm.ts @@ -1,11 +1,10 @@ import { createLogger } from '@aztec/foundation/log'; -import initACVM, { type ExecutionError, executeCircuit } from '@aztec/noir-acvm_js'; +import initACVM, { type ExecutionError, type ForeignCallHandler, executeCircuit } from '@aztec/noir-acvm_js'; import initAbi from '@aztec/noir-noirc_abi'; -import { foreignCallHandler } from '@aztec/noir-protocol-circuits-types/client'; -import type { WitnessMap } from '@aztec/noir-types'; -import type { NoirCompiledCircuit } from '@aztec/stdlib/noir'; +import type { FunctionArtifactWithContractName } from '@aztec/stdlib/abi'; +import type { NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; -import { type ACIRCallback, acvm } from '../acvm/acvm.js'; +import { type ACIRCallback, type ACIRExecutionResult, acvm } from '../acvm/acvm.js'; import type { ACVMWitness } from '../acvm/acvm_types.js'; import { type SimulationProvider, enrichNoirError } from './simulation_provider.js'; @@ -22,41 +21,49 @@ export class WASMSimulator implements SimulationProvider { } } - async executeProtocolCircuit(input: WitnessMap, compiledCircuit: NoirCompiledCircuit): Promise { - this.log.debug('init', { hash: compiledCircuit.hash }); + async executeProtocolCircuit( + input: ACVMWitness, + artifact: NoirCompiledCircuitWithName, + callback: ForeignCallHandler, + ): Promise { + this.log.debug('init', { hash: artifact.hash }); await this.init(); - // Execute the circuit on those initial witness values - // + // Decode the bytecode from base64 since the acvm does not know about base64 encoding - const decodedBytecode = Buffer.from(compiledCircuit.bytecode, 'base64'); + const decodedBytecode = Buffer.from(artifact.bytecode, 'base64'); // // Execute the circuit try { - const _witnessMap = await executeCircuit( + const result = await executeCircuit( decodedBytecode, input, - foreignCallHandler, // handle calls to debug_log + callback, // handle calls to debug_log ); - this.log.debug('execution successful', { hash: compiledCircuit.hash }); - return _witnessMap; + this.log.debug('execution successful', { hash: artifact.hash }); + return result; } catch (err) { - // Typescript types catched errors as unknown or any, so we need to narrow its type to check if it has raw assertion payload. + // Typescript types caught errors as unknown or any, so we need to narrow its type to check if it has raw + // assertion payload. if (typeof err === 'object' && err !== null && 'rawAssertionPayload' in err) { - const parsed = enrichNoirError(compiledCircuit, err as ExecutionError); + const parsed = enrichNoirError(artifact, err as ExecutionError); this.log.debug('execution failed', { - hash: compiledCircuit.hash, + hash: artifact.hash, error: parsed, message: parsed.message, }); throw parsed; } - this.log.debug('execution failed', { hash: compiledCircuit.hash, error: err }); + this.log.debug('execution failed', { hash: artifact.hash, error: err }); throw new Error(`Circuit execution failed: ${err}`); } } - async executeUserCircuit(acir: Buffer, initialWitness: ACVMWitness, callback: ACIRCallback) { + async executeUserCircuit( + input: ACVMWitness, + artifact: FunctionArtifactWithContractName, + callback: ACIRCallback, + ): Promise { await this.init(); - return acvm(acir, initialWitness, callback); + return acvm(artifact.bytecode, input, callback); } } diff --git a/yarn-project/simulator/src/private/providers/acvm_wasm_with_blobs.ts b/yarn-project/simulator/src/private/providers/acvm_wasm_with_blobs.ts index dd17a8da792c..0506a4824bad 100644 --- a/yarn-project/simulator/src/private/providers/acvm_wasm_with_blobs.ts +++ b/yarn-project/simulator/src/private/providers/acvm_wasm_with_blobs.ts @@ -1,7 +1,7 @@ -import { type ExecutionError, executeCircuit } from '@aztec/noir-acvm_js'; -import { foreignCallHandler } from '@aztec/noir-protocol-circuits-types/server'; +import { type ExecutionError, type ForeignCallHandler, executeCircuit } from '@aztec/noir-acvm_js'; import type { WitnessMap } from '@aztec/noir-types'; -import type { NoirCompiledCircuit } from '@aztec/stdlib/noir'; +import type { FunctionArtifactWithContractName } from '@aztec/stdlib/abi'; +import type { NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; import type { ACIRCallback, ACIRExecutionResult } from '../acvm/acvm.js'; import type { ACVMWitness } from '../acvm/acvm_types.js'; @@ -15,33 +15,36 @@ import { type SimulationProvider, enrichNoirError } from './simulation_provider. * It is only used in the context of server-side code executing simulated protocol circuits. */ export class WASMSimulatorWithBlobs implements SimulationProvider { - async executeProtocolCircuit(input: WitnessMap, compiledCircuit: NoirCompiledCircuit): Promise { - // Execute the circuit on those initial witness values - // + async executeProtocolCircuit( + input: WitnessMap, + artifact: NoirCompiledCircuitWithName, + callback: ForeignCallHandler, + ): Promise { // Decode the bytecode from base64 since the acvm does not know about base64 encoding - const decodedBytecode = Buffer.from(compiledCircuit.bytecode, 'base64'); + const decodedBytecode = Buffer.from(artifact.bytecode, 'base64'); // // Execute the circuit try { const _witnessMap = await executeCircuit( decodedBytecode, input, - foreignCallHandler, // handle calls to debug_log and evaluate_blobs mock + callback, // handle calls to debug_log and evaluate_blobs mock ); return _witnessMap; } catch (err) { - // Typescript types catched errors as unknown or any, so we need to narrow its type to check if it has raw assertion payload. + // Typescript types caught errors as unknown or any, so we need to narrow its type to check if it has raw + // assertion payload. if (typeof err === 'object' && err !== null && 'rawAssertionPayload' in err) { - throw enrichNoirError(compiledCircuit, err as ExecutionError); + throw enrichNoirError(artifact, err as ExecutionError); } throw new Error(`Circuit execution failed: ${err}`); } } executeUserCircuit( - _acir: Buffer, - _initialWitness: ACVMWitness, + _input: ACVMWitness, + _artifact: FunctionArtifactWithContractName, _callback: ACIRCallback, ): Promise { throw new Error('Not implemented'); diff --git a/yarn-project/simulator/src/private/providers/circuit_recording/circuit_recorder.ts b/yarn-project/simulator/src/private/providers/circuit_recording/circuit_recorder.ts new file mode 100644 index 000000000000..b5a531920d38 --- /dev/null +++ b/yarn-project/simulator/src/private/providers/circuit_recording/circuit_recorder.ts @@ -0,0 +1,283 @@ +import { createLogger } from '@aztec/foundation/log'; +import type { ForeignCallHandler, ForeignCallInput, ForeignCallOutput } from '@aztec/noir-acvm_js'; + +import { createHash } from 'crypto'; +import fs from 'fs/promises'; +import path from 'path'; + +import type { ACIRCallback } from '../../acvm/acvm.js'; +import type { ACVMWitness } from '../../acvm/acvm_types.js'; +import { Oracle } from '../../acvm/oracle/oracle.js'; + +/** + * Class responsible for recording circuit inputs necessary to replay the circuit. These inputs are the initial witness + * map and the oracle calls made during the circuit execution/witness generation. + * + * The recording is stored in a JSON file called `circuit_name_circuit_function_name_YYYY-MM-DD_N.json` where N is + * a counter to ensure unique filenames. The file is stored in the `recordDir` directory provided as a parameter to + * CircuitRecorder.start(). + * + * Example recording file: + * ```json + * { + * "circuitName": "AMM", + * "functionName": "add_liquidity", + * "bytecodeMd5Hash": "b46c640ed38f20eac5f61a5e41d8dd1e", + * "timestamp": 1740691464360, + * "inputs": { + * "0": "0x1e89de1f0ad5204263733b7ddf65bec45b8f44714a4da85a46474dad677679ef", + * "1": "0x00f4d59c0ff773427bb0fed5b422557ca4dc5655abe53d31fa9408cb3c5a672f", + * "5": "0x000000000000000000000000000000000000000000000000000000000000000f" + * }, + * "oracleCalls": [ + * { + * "name": "loadCapsule", + * "inputs": [ + * [ + * "0x102422483bad6abd385948435667e144ac4c272576e325e7563608876cd446fd" + * ], + * [ + * "0x000000000000000000000000000000000000000000000000000000000000004d" + * ], + * [ + * "0x0000000000000000000000000000000000000000000000000000000000000001" + * ] + * ], + * "outputs": [ + * "0x0000000000000000000000000000000000000000000000000000000000000000", + * [ + * "0x0000000000000000000000000000000000000000000000000000000000000000" + * ] + * ] + * }, + * { + * "name": "syncNotes", + * "inputs": [] + * } + * ] + * } + * ``` + */ +export class CircuitRecorder { + private readonly logger = createLogger('simulator:acvm:recording'); + private isFirstCall = true; + + private constructor(private readonly filePath: string) {} + + /** + * Initializes a new circuit recording session. + * @param recordDir - Directory to store the recording + * @param input - Circuit input witness + * @param circuitBytecode - Compiled circuit bytecode + * @param circuitName - Name of the circuit + * @param functionName - Name of the circuit function (defaults to 'main'). This is meaningful only for + * contracts as protocol circuits artifacts always contain a single entrypoint function called 'main'. + * @returns A new CircuitRecorder instance + */ + static async start( + recordDir: string, + input: ACVMWitness, + circuitBytecode: Buffer, + circuitName: string, + functionName: string = 'main', + ): Promise { + const recording = { + circuitName: circuitName, + functionName: functionName, + bytecodeMd5Hash: createHash('md5').update(circuitBytecode).digest('hex'), + timestamp: Date.now(), + inputs: Object.fromEntries(input), + }; + + const recordingStringWithoutClosingBracket = JSON.stringify(recording, null, 2).slice(0, -2); + + try { + // Check if the recording directory exists and is a directory + const stats = await fs.stat(recordDir); + if (!stats.isDirectory()) { + throw new Error(`Recording path ${recordDir} exists but is not a directory`); + } + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + // The directory does not exist so we create it + await fs.mkdir(recordDir, { recursive: true }); + } else { + throw err; + } + } + + const filePath = await CircuitRecorder.#computeFilePathAndStoreInitialRecording( + recordDir, + circuitName, + functionName, + recordingStringWithoutClosingBracket, + ); + return new CircuitRecorder(filePath); + } + + /** + * Computes a unique file path for the recording by trying different counter values. + * This is needed because multiple recordings of the same circuit could be happening simultaneously or an older + * recording might be present. + * @param recordDir - Directory to store the recording + * @param circuitName - Name of the circuit + * @param functionName - Name of the circuit function + * @param recordingContent - Initial recording content + * @returns A unique file path for the recording + */ + static async #computeFilePathAndStoreInitialRecording( + recordDir: string, + circuitName: string, + functionName: string, + recordingContent: string, + ): Promise { + let counter = 0; + while (true) { + try { + const filePath = getFilePath(recordDir, circuitName, functionName, counter); + // Write the initial recording content to the file + await fs.writeFile(filePath, recordingContent + ',\n "oracleCalls": [\n', { + flag: 'wx', // wx flag fails if file exists + }); + return filePath; + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'EEXIST') { + counter++; + continue; + } + throw err; + } + } + } + + /** + * Wraps a callback to record all oracle/foreign calls. + * @param callback - The original callback to wrap, either a user circuit callback or protocol circuit callback. + * @returns A wrapped callback that records all oracle interactions. + */ + wrapCallback(callback: ACIRCallback | ForeignCallHandler | undefined): ACIRCallback | ForeignCallHandler | undefined { + if (!callback) { + return undefined; + } + if (this.#isACIRCallback(callback)) { + return this.#wrapUserCircuitCallback(callback); + } + return this.#wrapProtocolCircuitCallback(callback); + } + + /** + * Type guard to check if a callback is an ACIRCallback. + */ + #isACIRCallback(callback: ACIRCallback | ForeignCallHandler): callback is ACIRCallback { + return typeof callback === 'object' && callback !== null && !('call' in callback); + } + + /** + * Wraps a user circuit callback to record all oracle calls. + * @param callback - The original circuit callback. + * @returns A wrapped callback that records all oracle interactions which is to be provided to the ACVM. + */ + #wrapUserCircuitCallback(callback: ACIRCallback): ACIRCallback { + const recordingCallback: ACIRCallback = {} as ACIRCallback; + const oracleMethods = Object.getOwnPropertyNames(Oracle.prototype).filter(name => name !== 'constructor'); + + for (const name of oracleMethods) { + const fn = callback[name as keyof ACIRCallback]; + if (!fn) { + throw new Error(`Oracle method ${name} not found when setting up recording callback`); + } + + recordingCallback[name as keyof ACIRCallback] = (...args: ForeignCallInput[]): ReturnType => { + const result = fn.call(callback, ...args); + if (result instanceof Promise) { + return result.then(async r => { + await this.#recordCall(name, args, r); + return r; + }) as ReturnType; + } + void this.#recordCall(name, args, result); + return result; + }; + } + + return recordingCallback; + } + + /** + * Wraps a protocol circuit callback to record all oracle calls. + * @param callback - The original oracle circuit callback. + * @returns A wrapped handler that records all oracle interactions which is to be provided to the ACVM. + */ + #wrapProtocolCircuitCallback(callback: ForeignCallHandler): ForeignCallHandler { + return async (name: string, inputs: ForeignCallInput[]): Promise => { + const result = await callback(name, inputs); + await this.#recordCall(name, inputs, result); + return result; + }; + } + + /** + * Records a single oracle/foreign call with its inputs and outputs. + * @param name - Name of the call + * @param inputs - Input arguments + * @param outputs - Output results + */ + async #recordCall(name: string, inputs: unknown[], outputs: unknown) { + try { + const entry = { + name, + inputs, + outputs, + }; + const prefix = this.isFirstCall ? ' ' : ' ,'; + this.isFirstCall = false; + await fs.appendFile(this.filePath, prefix + JSON.stringify(entry) + '\n'); + } catch (err) { + this.logger.error('Failed to log circuit call', { error: err }); + } + } + + /** + * Finalizes the recording file by adding closing brackets. Without calling this method, the recording file is + * incomplete and it fails to parse. + */ + async finish(): Promise { + try { + await fs.appendFile(this.filePath, ' ]\n}\n'); + } catch (err) { + this.logger.error('Failed to finalize recording file', { error: err }); + } + } + + /** + * Finalizes the recording file by adding the error and closing brackets. Without calling this method or `finish`, + * the recording file is incomplete and it fails to parse. + * @param error - The error that occurred during circuit execution + */ + async finishWithError(error: unknown): Promise { + try { + await fs.appendFile(this.filePath, ' ],\n'); + await fs.appendFile(this.filePath, ` "error": ${JSON.stringify(error)}\n`); + await fs.appendFile(this.filePath, '}\n'); + } catch (err) { + this.logger.error('Failed to finalize recording file with error', { error: err }); + } + } +} + +/** + * Generates a file path for storing circuit recordings. The format of the filename is: + * `circuit_name_circuit_function_name_YYYY-MM-DD_N.json` where N is a counter to ensure unique filenames. + * @param recordDir - Base directory for recordings + * @param circuitName - Name of the circuit + * @param functionName - Name of the circuit function + * @param counter - Counter to ensure unique filenames. This is expected to be incremented in a loop until there is no + * existing file with the same name. + * @returns A file path for the recording. + */ +function getFilePath(recordDir: string, circuitName: string, functionName: string, counter: number): string { + const date = new Date(); + const formattedDate = date.toISOString().split('T')[0]; + const filename = `${circuitName}_${functionName}_${formattedDate}_${counter}.json`; + return path.join(recordDir, filename); +} diff --git a/yarn-project/simulator/src/private/providers/circuit_recording/simulation_provider_recorder_wrapper.ts b/yarn-project/simulator/src/private/providers/circuit_recording/simulation_provider_recorder_wrapper.ts new file mode 100644 index 000000000000..825c5b56dac6 --- /dev/null +++ b/yarn-project/simulator/src/private/providers/circuit_recording/simulation_provider_recorder_wrapper.ts @@ -0,0 +1,82 @@ +import type { ForeignCallHandler } from '@aztec/noir-protocol-circuits-types/types'; +import type { FunctionArtifactWithContractName } from '@aztec/stdlib/abi'; +import type { NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; + +import type { ACIRCallback, ACIRExecutionResult } from '../../acvm/acvm.js'; +import type { ACVMWitness } from '../../acvm/acvm_types.js'; +import type { SimulationProvider } from '../simulation_provider.js'; +import { CircuitRecorder } from './circuit_recorder.js'; + +/** + * Takes a simulation provider and wraps it in a circuit recorder. See CircuitRecorder for more details on how circuit + * recording works. + */ +export class SimulationProviderRecorderWrapper implements SimulationProvider { + constructor(private simulator: SimulationProvider) {} + + executeProtocolCircuit( + input: ACVMWitness, + artifact: NoirCompiledCircuitWithName, + callback: ForeignCallHandler | undefined, + ): Promise { + const bytecode = Buffer.from(artifact.bytecode, 'base64'); + + return this.#simulate( + wrappedCallback => this.simulator.executeProtocolCircuit(input, artifact, wrappedCallback), + input, + bytecode, + artifact.name, + 'main', + callback, + ); + } + + executeUserCircuit( + input: ACVMWitness, + artifact: FunctionArtifactWithContractName, + callback: ACIRCallback, + ): Promise { + return this.#simulate( + wrappedCallback => this.simulator.executeUserCircuit(input, artifact, wrappedCallback), + input, + artifact.bytecode, + artifact.contractName, + artifact.name, + callback, + ); + } + + async #simulate( + simulateFn: (wrappedCallback: C) => Promise, + input: ACVMWitness, + bytecode: Buffer, + contractName: string, + functionName: string, + callback: C, + ): Promise { + const recordDir = process.env.CIRCUIT_RECORD_DIR; + if (!recordDir) { + // Recording is not enabled so we just execute the circuit + return simulateFn(callback); + } + + // Start recording circuit execution + const recorder = await CircuitRecorder.start(recordDir, input, bytecode, contractName, functionName); + + // If callback was provided, we wrap it in a circuit recorder callback wrapper + const wrappedCallback = recorder.wrapCallback(callback); + let result: T; + try { + result = await simulateFn(wrappedCallback as C); + } catch (error) { + // If an error occurs, we finalize the recording file with the error + await recorder.finishWithError(error); + throw error; + } + + // Witness generation is complete so we finish the circuit recorder + await recorder.finish(); + + return result; + } +} diff --git a/yarn-project/simulator/src/private/providers/simulation_provider.ts b/yarn-project/simulator/src/private/providers/simulation_provider.ts index be5d32f42728..da6589d2f790 100644 --- a/yarn-project/simulator/src/private/providers/simulation_provider.ts +++ b/yarn-project/simulator/src/private/providers/simulation_provider.ts @@ -1,8 +1,8 @@ -import type { ExecutionError } from '@aztec/noir-acvm_js'; +import type { ExecutionError, ForeignCallHandler } from '@aztec/noir-acvm_js'; import { abiDecodeError } from '@aztec/noir-noirc_abi'; -import type { WitnessMap } from '@aztec/noir-types'; import { parseDebugSymbols } from '@aztec/stdlib/abi'; -import type { NoirCompiledCircuit } from '@aztec/stdlib/noir'; +import type { FunctionArtifactWithContractName } from '@aztec/stdlib/abi'; +import type { NoirCompiledCircuit, NoirCompiledCircuitWithName } from '@aztec/stdlib/noir'; import { type ACIRCallback, type ACIRExecutionResult, extractCallStack } from '../acvm/acvm.js'; import type { ACVMWitness } from '../acvm/acvm_types.js'; @@ -11,8 +11,33 @@ import type { ACVMWitness } from '../acvm/acvm_types.js'; * Low level simulation interface */ export interface SimulationProvider { - executeProtocolCircuit(input: WitnessMap, compiledCircuit: NoirCompiledCircuit): Promise; - executeUserCircuit(acir: Buffer, initialWitness: ACVMWitness, callback: ACIRCallback): Promise; + /** + * Execute a protocol circuit/generate a witness + * @param input - The initial witness map defining all of the inputs to `circuit`. + * @param artifact - ACIR circuit bytecode and its metadata. + * @param callback - A callback to process any foreign calls from the circuit. Can be undefined as for native + * ACVM simulator we don't process foreign calls. + * @returns The solved witness calculated by executing the circuit on the provided inputs. + */ + executeProtocolCircuit( + input: ACVMWitness, + artifact: NoirCompiledCircuitWithName, + callback: ForeignCallHandler | undefined, + ): Promise; + + /** + * Execute a user circuit (smart contract function)/generate a witness + * @param input - The initial witness map defining all of the inputs to `circuit`. + * @param artifact - Contract function ACIR circuit bytecode and its metadata. + * @param callback - A callback to process any foreign calls from the circuit. + * @returns The solved witness calculated by executing the circuit on the provided inputs, as well as the return + * witness indices as specified by the circuit. + */ + executeUserCircuit( + input: ACVMWitness, + artifact: FunctionArtifactWithContractName, + callback: ACIRCallback, + ): Promise; } export type DecodedError = ExecutionError & { decodedAssertionPayload?: any; noirCallStack?: string[] }; diff --git a/yarn-project/simulator/src/private/unconstrained_execution.test.ts b/yarn-project/simulator/src/private/unconstrained_execution.test.ts index 0e6c59108c7a..7ca325a5ddcf 100644 --- a/yarn-project/simulator/src/private/unconstrained_execution.test.ts +++ b/yarn-project/simulator/src/private/unconstrained_execution.test.ts @@ -52,7 +52,10 @@ describe('Unconstrained Execution test suite', () => { it('should run the summed_values function', async () => { const contractAddress = await AztecAddress.random(); - const artifact = StatefulTestContractArtifact.functions.find(f => f.name === 'summed_values')!; + const artifact = { + ...StatefulTestContractArtifact.functions.find(f => f.name === 'summed_values')!, + contractName: StatefulTestContractArtifact.name, + }; const notes: Note[] = [...Array(5).fill(buildNote(1n, owner)), ...Array(2).fill(buildNote(2n, owner))]; diff --git a/yarn-project/simulator/src/private/unconstrained_execution.ts b/yarn-project/simulator/src/private/unconstrained_execution.ts index 01574ec7092d..38d3c5797caf 100644 --- a/yarn-project/simulator/src/private/unconstrained_execution.ts +++ b/yarn-project/simulator/src/private/unconstrained_execution.ts @@ -1,6 +1,11 @@ import type { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; -import { type AbiDecoded, type FunctionArtifact, type FunctionSelector, decodeFromAbi } from '@aztec/stdlib/abi'; +import { + type AbiDecoded, + type FunctionArtifactWithContractName, + type FunctionSelector, + decodeFromAbi, +} from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { ExecutionError, resolveAssertionMessageFromError } from '../common/errors.js'; @@ -16,7 +21,7 @@ import type { UnconstrainedExecutionOracle } from './unconstrained_execution_ora export async function executeUnconstrainedFunction( simulatorProvider: SimulationProvider, oracle: UnconstrainedExecutionOracle, - artifact: FunctionArtifact, + artifact: FunctionArtifactWithContractName, contractAddress: AztecAddress, functionSelector: FunctionSelector, args: Fr[], @@ -27,10 +32,9 @@ export async function executeUnconstrainedFunction( selector: functionSelector, }); - const acir = artifact.bytecode; const initialWitness = toACVMWitness(0, args); const acirExecutionResult = await simulatorProvider - .executeUserCircuit(acir, initialWitness, new Oracle(oracle)) + .executeUserCircuit(initialWitness, artifact, new Oracle(oracle)) .catch((err: Error) => { err.message = resolveAssertionMessageFromError(err, artifact); throw new ExecutionError( diff --git a/yarn-project/simulator/src/testing.ts b/yarn-project/simulator/src/testing.ts new file mode 100644 index 000000000000..63f4dee1b954 --- /dev/null +++ b/yarn-project/simulator/src/testing.ts @@ -0,0 +1 @@ +export { SimulationProviderRecorderWrapper } from './private/providers/circuit_recording/simulation_provider_recorder_wrapper.js'; diff --git a/yarn-project/stdlib/src/abi/abi.ts b/yarn-project/stdlib/src/abi/abi.ts index 15f78acf37e0..8f88cc341dbc 100644 --- a/yarn-project/stdlib/src/abi/abi.ts +++ b/yarn-project/stdlib/src/abi/abi.ts @@ -231,6 +231,11 @@ export interface FunctionArtifact extends FunctionAbi { debug?: FunctionDebugMetadata; } +export interface FunctionArtifactWithContractName extends FunctionArtifact { + /** The name of the contract. */ + contractName: string; +} + export const FunctionArtifactSchema = FunctionAbiSchema.and( z.object({ bytecode: schemas.Buffer, @@ -395,7 +400,7 @@ export function getFunctionArtifactByName(artifact: ContractArtifact, functionNa export async function getFunctionArtifact( artifact: ContractArtifact, functionNameOrSelector: string | FunctionSelector, -): Promise { +): Promise { let functionArtifact; if (typeof functionNameOrSelector === 'string') { functionArtifact = artifact.functions.find(f => f.name === functionNameOrSelector); @@ -416,7 +421,7 @@ export async function getFunctionArtifact( const debugMetadata = getFunctionDebugMetadata(artifact, functionArtifact); - return { ...functionArtifact, debug: debugMetadata }; + return { ...functionArtifact, debug: debugMetadata, contractName: artifact.name }; } /** Gets all function abis */ diff --git a/yarn-project/stdlib/src/noir/index.ts b/yarn-project/stdlib/src/noir/index.ts index 1fc99036e187..114108495f9f 100644 --- a/yarn-project/stdlib/src/noir/index.ts +++ b/yarn-project/stdlib/src/noir/index.ts @@ -75,7 +75,7 @@ export interface NoirCompiledContract { } /** - * The compilation result of an Aztec.nr contract. + * The compilation result of a protocol (non-contract) circuit. */ export interface NoirCompiledCircuit { /** The hash of the circuit. */ @@ -92,6 +92,11 @@ export interface NoirCompiledCircuit { file_map: DebugFileMap; } +export interface NoirCompiledCircuitWithName extends NoirCompiledCircuit { + /** The name of the circuit. */ + name: string; +} + /** * The debug metadata of an Aztec.nr contract. */ diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 01caccefb970..da550da30a86 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -826,12 +826,11 @@ export class TXE implements TypedOracle { const artifact = await this.contractDataProvider.getFunctionArtifact(targetContractAddress, functionSelector); - const acir = artifact.bytecode; const initialWitness = await this.getInitialWitness(artifact, argsHash, sideEffectCounter, isStaticCall); const acvmCallback = new Oracle(this); const timer = new Timer(); const acirExecutionResult = await this.simulationProvider - .executeUserCircuit(acir, initialWitness, acvmCallback) + .executeUserCircuit(initialWitness, artifact, acvmCallback) .catch((err: Error) => { err.message = resolveAssertionMessageFromError(err, artifact);