diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit1.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit1.test.ts index 2e9a203845ee..7d23c59a9c6d 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit1.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit1.test.ts @@ -19,7 +19,7 @@ describe('AVM WitGen & Circuit – check circuit', () => { let tester: AvmProvingTester; beforeEach(async () => { - tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], /*deployer=*/ AztecAddress.fromNumber(420), diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit2.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit2.test.ts index f7691ae1c2ea..a229bba37257 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit2.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit2.test.ts @@ -13,7 +13,7 @@ describe('AVM WitGen & Circuit – check circuit', () => { let tester: AvmProvingTester; beforeEach(async () => { - tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], /*deployer=*/ AztecAddress.fromNumber(420), @@ -44,7 +44,7 @@ describe('AVM WitGen & Circuit – check circuit', () => { // FIXME(dbanks12): fails with "Lookup PERM_MAIN_ALU failed." it.skip('top-level exceptional halts due to a non-existent contract in app-logic and teardown', async () => { // don't insert contracts into trees, and make sure retrieval fails - const tester = await AvmProvingTester.create(/*checkCircuitOnly=*/ true); + const tester = await AvmProvingTester.new(/*checkCircuitOnly=*/ true); await tester.simProveVerify( sender, /*setupCalls=*/ [], diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts index 1a55082cd2fb..046f583ad9d1 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts @@ -13,7 +13,7 @@ describe('AVM WitGen & Circuit – check circuit', () => { let tester: AvmProvingTester; beforeEach(async () => { - tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], /*deployer=*/ AztecAddress.fromNumber(420), diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_class_limits.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_class_limits.test.ts index a96adebe7b49..58c6f7d5ebdb 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_class_limits.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_class_limits.test.ts @@ -15,7 +15,7 @@ describe('AVM WitGen & Circuit – check circuit - contract class limits', () => let avmTestContractAddress: AztecAddress; beforeEach(async () => { - tester = await AvmProvingTester.create(/*checkCircuitOnly=*/ true); + tester = await AvmProvingTester.new(/*checkCircuitOnly=*/ true); // create enough unique contract classes to hit the limit instances = []; for (let i = 0; i <= MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS; i++) { diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_updates.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_updates.test.ts index ab48afbc8c39..5be707c975df 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_updates.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_updates.test.ts @@ -1,7 +1,7 @@ import { Fr } from '@aztec/foundation/fields'; import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; -import { DEFAULT_BLOCK_NUMBER } from '@aztec/simulator/public/fixtures'; +import { defaultGlobals } from '@aztec/simulator/public/fixtures'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import { ScheduledDelayChange, ScheduledValueChange, SharedMutableValuesWithHash } from '@aztec/stdlib/shared-mutable'; @@ -43,7 +43,8 @@ describe.skip('AVM WitGen & Circuit - contract updates', () => { async () => { // Contract was not originally the avmTestContract const originalClassId = new Fr(27); - const tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + const globals = defaultGlobals(); + const tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true, globals); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], @@ -59,7 +60,7 @@ describe.skip('AVM WitGen & Circuit - contract updates', () => { avmTestContractInstance.address, avmTestContractInstance.originalContractClassId, avmTestContractInstance.currentContractClassId, - DEFAULT_BLOCK_NUMBER, + globals.blockNumber.toNumber(), ); await tester.simProveVerify( @@ -80,8 +81,8 @@ describe.skip('AVM WitGen & Circuit - contract updates', () => { async () => { // Contract was not originally the avmTestContract const originalClassId = new Fr(27); - - const tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + const globals = defaultGlobals(); + const tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true, globals); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], sender, @@ -96,7 +97,7 @@ describe.skip('AVM WitGen & Circuit - contract updates', () => { avmTestContractInstance.address, avmTestContractInstance.originalContractClassId, avmTestContractInstance.currentContractClassId, - DEFAULT_BLOCK_NUMBER + 1, + globals.blockNumber.toNumber() + 1, ); await expect( @@ -120,7 +121,8 @@ describe.skip('AVM WitGen & Circuit - contract updates', () => { // Contract was not originally the avmTestContract const newClassId = new Fr(27); - const tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + const globals = defaultGlobals(); + const tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true, globals); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], sender, @@ -134,7 +136,7 @@ describe.skip('AVM WitGen & Circuit - contract updates', () => { avmTestContractInstance.address, avmTestContractInstance.currentContractClassId, newClassId, - DEFAULT_BLOCK_NUMBER + 1, + globals.blockNumber.toNumber() + 1, ); await tester.simProveVerify( @@ -156,7 +158,8 @@ describe.skip('AVM WitGen & Circuit - contract updates', () => { // Contract was not originally the avmTestContract const newClassId = new Fr(27); - const tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + const globals = defaultGlobals(); + const tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true, globals); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], sender, @@ -170,7 +173,7 @@ describe.skip('AVM WitGen & Circuit - contract updates', () => { avmTestContractInstance.address, avmTestContractInstance.currentContractClassId, newClassId, - DEFAULT_BLOCK_NUMBER - 1, + globals.blockNumber.toNumber() - 1, ); await expect( diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_amm.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_amm.test.ts index 71654d9bf96c..dfd4e559f53a 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_amm.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_amm.test.ts @@ -15,7 +15,7 @@ describe('AVM Witgen & Circuit apps tests: AMM', () => { jest.setTimeout(TIMEOUT); const admin = AztecAddress.fromNumber(42); const liquidityProvider = AztecAddress.fromNumber(111); - const otherLiqduidityProvider = AztecAddress.fromNumber(222); + const otherLiquidityProvider = AztecAddress.fromNumber(222); const swapper = AztecAddress.fromNumber(333); let token0: ContractInstanceWithAddress; @@ -25,7 +25,7 @@ describe('AVM Witgen & Circuit apps tests: AMM', () => { let tester: AvmProvingTester; beforeEach(async () => { - tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true); }); // TODO(dbanks12): add tester support for authwit and finish implementing this test @@ -52,8 +52,8 @@ describe('AVM Witgen & Circuit apps tests: AMM', () => { await mint(/*to=*/ liquidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token0); await mint(/*to=*/ liquidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token1); - await mint(/*to=*/ otherLiqduidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token0); - await mint(/*to=*/ otherLiqduidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token1); + await mint(/*to=*/ otherLiquidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token0); + await mint(/*to=*/ otherLiquidityProvider, /*amount=*/ INITIAL_TOKEN_BALANCE, token1); await mint(/*to=*/ swapper, /*amount=*/ INITIAL_TOKEN_BALANCE, token0); //const ammBalancesBefore = await getAmmBalances(); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_token.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_token.test.ts index 23ebfe7a04ad..8e4b51d818bb 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_token.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_token.test.ts @@ -20,7 +20,7 @@ describe('AVM Witgen & Circuit apps tests: TokenContract', () => { let tester: AvmProvingTester; beforeEach(async () => { - tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true); const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)]; token = await tester.registerAndDeployContract(constructorArgs, /*deployer=*/ admin, TokenContractArtifact); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_and_verification.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_and_verification.test.ts index 4d11af860bcd..d19fa0242c19 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_and_verification.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_and_verification.test.ts @@ -12,7 +12,7 @@ describe('AVM WitGen & Circuit – proving and verification', () => { let tester: AvmProvingTester; beforeEach(async () => { - tester = await AvmProvingTester.create(/*checkCircuitOnly*/ false); + tester = await AvmProvingTester.new(/*checkCircuitOnly*/ false); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], /*deployer=*/ AztecAddress.fromNumber(420), diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts index b1aa77280b93..76a68198ea00 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts @@ -4,6 +4,7 @@ import type { AvmCircuitInputs } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'; import { makeAvmCircuitInputs } from '@aztec/stdlib/testing'; +import type { GlobalVariables } from '@aztec/stdlib/tx'; import { VerificationKeyData } from '@aztec/stdlib/vks'; import { NativeWorldStateService } from '@aztec/world-state'; @@ -30,16 +31,18 @@ export class AvmProvingTester extends PublicTxSimulationTester { private checkCircuitOnly: boolean, merkleTree: MerkleTreeWriteOperations, contractDataSource: SimpleContractDataSource, + globals?: GlobalVariables, ) { - super(merkleTree, contractDataSource); + super(merkleTree, contractDataSource, globals); } - static override async create(checkCircuitOnly: boolean = false) { + // overriding parent class' create is a pain, so we use a different nam + static async new(checkCircuitOnly: boolean = false, globals?: GlobalVariables) { const bbWorkingDirectory = await fs.mkdtemp(path.join(tmpdir(), 'bb-')); const contractDataSource = new SimpleContractDataSource(); const merkleTrees = await (await NativeWorldStateService.tmp()).fork(); - return new AvmProvingTester(bbWorkingDirectory, checkCircuitOnly, merkleTrees, contractDataSource); + return new AvmProvingTester(bbWorkingDirectory, checkCircuitOnly, merkleTrees, contractDataSource, globals); } async prove(avmCircuitInputs: AvmCircuitInputs): Promise { @@ -108,16 +111,17 @@ export class AvmProvingTesterV2 extends PublicTxSimulationTester { private bbWorkingDirectory: string, contractDataSource: SimpleContractDataSource, merkleTrees: MerkleTreeWriteOperations, + globals?: GlobalVariables, ) { - super(merkleTrees, contractDataSource); + super(merkleTrees, contractDataSource, globals); } - static override async create() { + static async new(globals?: GlobalVariables) { const bbWorkingDirectory = await fs.mkdtemp(path.join(tmpdir(), 'bb-')); const contractDataSource = new SimpleContractDataSource(); const merkleTrees = await (await NativeWorldStateService.tmp()).fork(); - return new AvmProvingTesterV2(bbWorkingDirectory, contractDataSource, merkleTrees); + return new AvmProvingTesterV2(bbWorkingDirectory, contractDataSource, merkleTrees, globals); } async proveV2(avmCircuitInputs: AvmCircuitInputs): Promise { diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_public_fee_payment.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_public_fee_payment.test.ts index e84415f3be3d..1b3f9798363f 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_public_fee_payment.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_public_fee_payment.test.ts @@ -16,7 +16,7 @@ describe('AVM WitGen & Circuit – public fee payment', () => { let tester: AvmProvingTester; beforeEach(async () => { - tester = await AvmProvingTester.create(/*checkCircuitOnly*/ true); + tester = await AvmProvingTester.new(/*checkCircuitOnly*/ true); await tester.registerFeeJuiceContract(); await tester.setFeePayerBalance(feePayer, initialFeeJuiceBalance); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_v2.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_v2.test.ts index dbdca33acd21..f56706614c52 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_v2.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_v2.test.ts @@ -11,7 +11,7 @@ describe('AVM v2', () => { let tester: AvmProvingTesterV2; beforeEach(async () => { - tester = await AvmProvingTesterV2.create(); + tester = await AvmProvingTesterV2.new(); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], /*deployer=*/ AztecAddress.fromNumber(420), diff --git a/yarn-project/end-to-end/src/bench/utils.ts b/yarn-project/end-to-end/src/bench/utils.ts index 5e952caac0bf..d0e1bbce5941 100644 --- a/yarn-project/end-to-end/src/bench/utils.ts +++ b/yarn-project/end-to-end/src/bench/utils.ts @@ -1,16 +1,12 @@ import type { AztecNodeService } from '@aztec/aztec-node'; import { type AztecNode, BatchCall, INITIAL_L2_BLOCK_NUM, type SentTx, type WaitOpts } from '@aztec/aztec.js'; import { mean, stdDev, times } from '@aztec/foundation/collection'; -import { randomInt } from '@aztec/foundation/crypto'; import { BenchmarkingContract } from '@aztec/noir-contracts.js/Benchmarking'; import { type PXEService, type PXEServiceConfig, createPXEService } from '@aztec/pxe/server'; import type { MetricsType } from '@aztec/telemetry-client'; import type { BenchmarkDataPoint, BenchmarkMetricsType, BenchmarkTelemetryClient } from '@aztec/telemetry-client/bench'; import { writeFileSync } from 'fs'; -import { mkdirpSync } from 'fs-extra'; -import { globSync } from 'glob'; -import { join } from 'path'; import { type EndToEndContext, type SetupOptions, setup } from '../fixtures/utils.js'; @@ -94,30 +90,6 @@ function getMetricValues(points: BenchmarkDataPoint[]) { } } -/** - * Creates and returns a directory with the current job name and a random number. - * @param index - Index to merge into the dir path. - * @returns A path to a created dir. - */ -export function makeDataDirectory(index: number) { - const testName = expect.getState().currentTestName!.split(' ')[0].replaceAll('/', '_'); - const db = join('data', testName, index.toString(), `${randomInt(99)}`); - mkdirpSync(db); - return db; -} - -/** - * Returns the size in disk of a folder. - * @param path - Path to the folder. - * @returns Size in bytes. - */ -export function getFolderSize(path: string): number { - return globSync('**', { stat: true, cwd: path, nodir: true, withFileTypes: true }).reduce( - (accum, file) => accum + (file as any as { /** Size */ size: number }).size, - 0, - ); -} - /** * Returns a call to the benchmark contract. Each call has a private execution (account entrypoint), * a nested private call (create_note), a public call (increment_balance), and a nested public diff --git a/yarn-project/end-to-end/src/e2e_block_building.test.ts b/yarn-project/end-to-end/src/e2e_block_building.test.ts index 00b560ec9354..8531bf0d61a5 100644 --- a/yarn-project/end-to-end/src/e2e_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_block_building.test.ts @@ -33,7 +33,7 @@ import { PublicProcessorFactory, type PublicTreesDB, type PublicTxResult, - PublicTxSimulator, + TelemetryPublicTxSimulator, } from '@aztec/simulator/server'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import type { Tx } from '@aztec/stdlib/tx'; @@ -608,7 +608,7 @@ async function sendAndWait(calls: ContractFunctionInteraction[]) { const TEST_PUBLIC_TX_SIMULATION_DELAY_MS = 300; -class TestPublicTxSimulator extends PublicTxSimulator { +class TestPublicTxSimulator extends TelemetryPublicTxSimulator { public override async simulate(tx: Tx): Promise { await sleep(TEST_PUBLIC_TX_SIMULATION_DELAY_MS); return super.simulate(tx); @@ -622,7 +622,7 @@ class TestPublicProcessorFactory extends PublicProcessorFactory { doMerkleOperations: boolean, skipFeeEnforcement: boolean, telemetryClient?: TelemetryClient, - ): PublicTxSimulator { + ) { return new TestPublicTxSimulator( treesDB, contractsDB, diff --git a/yarn-project/simulator/README.md b/yarn-project/simulator/README.md index 2e48cd3848d6..1f29f54f2134 100644 --- a/yarn-project/simulator/README.md +++ b/yarn-project/simulator/README.md @@ -47,3 +47,9 @@ Same steps as any other library. They are detailed [here](../README.md#developme ### Testing Same steps as any other library. They are detailed [here](../README.md#tests) + +### Benchmarking + +``` +LOG_LEVEL=info yarn test src/public/public_tx_simulator/apps_tests/bench.test.ts +``` diff --git a/yarn-project/simulator/src/public/avm/avm_contract_call_result.ts b/yarn-project/simulator/src/public/avm/avm_contract_call_result.ts index 88f98a9d784e..1e864b777923 100644 --- a/yarn-project/simulator/src/public/avm/avm_contract_call_result.ts +++ b/yarn-project/simulator/src/public/avm/avm_contract_call_result.ts @@ -17,10 +17,13 @@ export class AvmContractCallResult { public output: Fr[], public gasLeft: AvmGas, public revertReason?: AvmRevertReason, + public totalInstructions: number = 0, // including nested calls ) {} toString(): string { - let resultsStr = `reverted: ${this.reverted}, output: ${this.output}, gasLeft: ${inspect(this.gasLeft)}`; + let resultsStr = `reverted: ${this.reverted}, output: ${this.output}, gasLeft: ${inspect( + this.gasLeft, + )}, totalInstructions: ${this.totalInstructions}`; if (this.revertReason) { resultsStr += `, revertReason: ${this.revertReason}`; } @@ -29,7 +32,13 @@ export class AvmContractCallResult { finalize(): AvmFinalizedCallResult { const revertReason = this.revertReason ? createSimulationError(this.revertReason, this.output) : undefined; - return new AvmFinalizedCallResult(this.reverted, this.output, Gas.from(this.gasLeft), revertReason); + return new AvmFinalizedCallResult( + this.reverted, + this.output, + Gas.from(this.gasLeft), + revertReason, + this.totalInstructions, + ); } } @@ -43,10 +52,13 @@ export class AvmFinalizedCallResult { public output: Fr[], public gasLeft: Gas, public revertReason?: SimulationError, + public totalInstructions: number = 0, // including nested calls ) {} toString(): string { - let resultsStr = `reverted: ${this.reverted}, output: ${this.output}, gasLeft: ${inspect(this.gasLeft)}`; + let resultsStr = `reverted: ${this.reverted}, output: ${this.output}, gasLeft: ${inspect( + this.gasLeft, + )}, totalInstructions: ${this.totalInstructions}`; if (this.revertReason) { resultsStr += `, revertReason: ${this.revertReason}`; } diff --git a/yarn-project/simulator/src/public/avm/avm_machine_state.ts b/yarn-project/simulator/src/public/avm/avm_machine_state.ts index 33f295b6c975..7be5b54c5db4 100644 --- a/yarn-project/simulator/src/public/avm/avm_machine_state.ts +++ b/yarn-project/simulator/src/public/avm/avm_machine_state.ts @@ -68,6 +68,11 @@ export class AvmMachineState { /** Output data must NOT be modified once it is set */ private output: Fr[] = []; + // Metrics only - not needed for execution + /** instruction counter, including nested calls */ + public instrCounter: number = 0; + // End metrics only + constructor(gasLeft: Gas); constructor(l2GasLeft: number, daGasLeft: number); constructor(gasLeftOrL2GasLeft: Gas | number, daGasLeft?: number) { diff --git a/yarn-project/simulator/src/public/avm/avm_simulator.ts b/yarn-project/simulator/src/public/avm/avm_simulator.ts index db3865e54b25..7bbfd0ed82bd 100644 --- a/yarn-project/simulator/src/public/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/public/avm/avm_simulator.ts @@ -147,7 +147,6 @@ export class AvmSimulator { try { // Execute instruction pointed to by the current program counter // continuing until the machine state signifies a halt - let instrCounter = 0; while (!machineState.getHalted()) { // Get the instruction from cache, or deserialize for the first time let cachedInstruction = this.deserializedInstructionsCache.get(machineState.pc); @@ -163,13 +162,11 @@ export class AvmSimulator { if (this.log.isLevelEnabled('trace')) { // Skip this entirely to avoid toStringing etc if trace is not enabled this.log.trace( - `[PC:${machineState.pc}] [IC:${instrCounter}] ${instruction.toString()} (gasLeft l2=${ + `[PC:${machineState.pc}] [IC:${machineState.instrCounter}] ${instruction.toString()} (gasLeft l2=${ machineState.l2GasLeft } da=${machineState.daGasLeft})`, ); } - instrCounter++; - machineState.nextPc = machineState.pc + bytesRead; // Execute the instruction. @@ -181,6 +178,8 @@ export class AvmSimulator { machineState.pc += bytesRead; } + machineState.instrCounter++; + // gas used by this instruction - used for profiling/tallying const gasUsed: Gas = { l2Gas: instrStartGas.l2Gas - machineState.l2GasLeft, @@ -197,13 +196,19 @@ export class AvmSimulator { const output = machineState.getOutput(); const reverted = machineState.getReverted(); const revertReason = reverted ? await revertReasonFromExplicitRevert(output, this.context) : undefined; - const results = new AvmContractCallResult(reverted, output, machineState.gasLeft, revertReason); + const results = new AvmContractCallResult( + reverted, + output, + machineState.gasLeft, + revertReason, + machineState.instrCounter, + ); this.log.debug(`Context execution results: ${results.toString()}`); const totalGasUsed: Gas = { l2Gas: callStartGas.l2Gas - machineState.l2GasLeft, daGas: callStartGas.daGas - machineState.daGasLeft, }; - this.log.debug(`Executed ${instrCounter} instructions and consumed ${totalGasUsed.l2Gas} L2 Gas`); + this.log.debug(`Executed ${machineState.instrCounter} instructions and consumed ${totalGasUsed.l2Gas} L2 Gas`); this.tallyPrintFunction(); @@ -233,7 +238,13 @@ export class AvmSimulator { // Exceptional halts consume all allocated gas const noGasLeft = { l2Gas: 0, daGas: 0 }; // Note: "exceptional halts" cannot return data, hence []. - const results = new AvmContractCallResult(/*reverted=*/ true, /*output=*/ [], noGasLeft, revertReason); + const results = new AvmContractCallResult( + /*reverted=*/ true, + /*output=*/ [], + noGasLeft, + revertReason, + machineState.instrCounter, + ); this.log.debug(`Context execution results: ${results.toString()}`); this.tallyPrintFunction(); diff --git a/yarn-project/simulator/src/public/avm/fixtures/avm_simulation_tester.ts b/yarn-project/simulator/src/public/avm/fixtures/avm_simulation_tester.ts index f6f3ced11559..cccf1aef8810 100644 --- a/yarn-project/simulator/src/public/avm/fixtures/avm_simulation_tester.ts +++ b/yarn-project/simulator/src/public/avm/fixtures/avm_simulation_tester.ts @@ -9,6 +9,7 @@ import { NativeWorldStateService } from '@aztec/world-state'; import { SideEffectTrace } from '../../../public/side_effect_trace.js'; import type { AvmContractCallResult } from '../../avm/avm_contract_call_result.js'; import { + DEFAULT_BLOCK_NUMBER, getContractFunctionAbi, getFunctionSelector, initContext, @@ -16,7 +17,6 @@ import { resolveContractAssertionMessage, } from '../../avm/fixtures/index.js'; import { AvmPersistableStateManager } from '../../avm/journal/journal.js'; -import { DEFAULT_BLOCK_NUMBER } from '../../fixtures/public_tx_simulation_tester.js'; import { PublicContractsDB, PublicTreesDB } from '../../public_db_sources.js'; import { AvmSimulator } from '../avm_simulator.js'; import { BaseAvmSimulationTester } from './base_avm_simulation_tester.js'; diff --git a/yarn-project/simulator/src/public/avm/fixtures/base_avm_simulation_tester.ts b/yarn-project/simulator/src/public/avm/fixtures/base_avm_simulation_tester.ts index 25c7f0c6680f..6339b2c191cd 100644 --- a/yarn-project/simulator/src/public/avm/fixtures/base_avm_simulation_tester.ts +++ b/yarn-project/simulator/src/public/avm/fixtures/base_avm_simulation_tester.ts @@ -6,7 +6,7 @@ import { computeFeePayerBalanceStorageSlot, getCanonicalFeeJuice } from '@aztec/ import type { ContractArtifact } from '@aztec/stdlib/abi'; import { PublicDataWrite } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import { computePublicDataTreeLeafSlot, siloNullifier } from '@aztec/stdlib/hash'; import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'; import { MerkleTreeId } from '@aztec/stdlib/trees'; @@ -86,12 +86,6 @@ export abstract class BaseAvmSimulationTester { return feeJuice.instance; } - addContractClass(contractClass: ContractClassPublic, contractArtifact: ContractArtifact): Promise { - this.logger.debug(`Adding contract class with Id ${contractClass.id}`); - this.contractDataSource.addContractArtifact(contractClass.id, contractArtifact); - return this.contractDataSource.addContractClass(contractClass); - } - async addContractInstance(contractInstance: ContractInstanceWithAddress, skipNullifierInsertion = false) { if (!skipNullifierInsertion) { await this.insertContractAddressNullifier(contractInstance.address); diff --git a/yarn-project/simulator/src/public/avm/fixtures/index.ts b/yarn-project/simulator/src/public/avm/fixtures/index.ts index d94aa90a8b7d..c577ae0cd7bb 100644 --- a/yarn-project/simulator/src/public/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/public/avm/fixtures/index.ts @@ -28,7 +28,6 @@ import { mock } from 'jest-mock-extended'; import merge from 'lodash.merge'; import { resolveAssertionMessageFromRevertData, traverseCauseChain } from '../../../common/index.js'; -import { DEFAULT_BLOCK_NUMBER } from '../../fixtures/public_tx_simulation_tester.js'; import type { PublicContractsDB, PublicTreesDB } from '../../public_db_sources.js'; import type { PublicSideEffectTraceInterface } from '../../side_effect_trace_interface.js'; import { AvmContext } from '../avm_context.js'; @@ -42,6 +41,7 @@ import { NullifierManager } from '../journal/nullifiers.js'; import { PublicStorage } from '../journal/public_storage.js'; export const PUBLIC_DISPATCH_FN_NAME = 'public_dispatch'; +export const DEFAULT_BLOCK_NUMBER = 42; /** * Create a new AVM context with default values. diff --git a/yarn-project/simulator/src/public/avm/fixtures/simple_contract_data_source.ts b/yarn-project/simulator/src/public/avm/fixtures/simple_contract_data_source.ts index 7aaaee6cb055..7b371ef9ead8 100644 --- a/yarn-project/simulator/src/public/avm/fixtures/simple_contract_data_source.ts +++ b/yarn-project/simulator/src/public/avm/fixtures/simple_contract_data_source.ts @@ -4,7 +4,7 @@ import type { ContractArtifact, FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractClassPublic, ContractDataSource, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { PUBLIC_DISPATCH_FN_NAME } from './index.js'; +import { getFunctionSelector } from './index.js'; /** * This class is used during public/avm testing to function as a database of @@ -22,6 +22,8 @@ export class SimpleContractDataSource implements ContractDataSource { private contractInstances: Map = new Map(); // maps contract instance address to address private contractArtifacts: Map = new Map(); + // maps `${classID}:${fnSelector}` to name + private debugFunctionName: Map = new Map(); ///////////////////////////////////////////////////////////// // Helper functions not in the contract data source interface @@ -34,13 +36,25 @@ export class SimpleContractDataSource implements ContractDataSource { contractClass: ContractClassPublic, contractInstance: ContractInstanceWithAddress, ) { - this.addContractArtifact(contractClass.id, contractArtifact); + await this.addContractArtifact(contractClass.id, contractArtifact); await this.addContractClass(contractClass); await this.addContractInstance(contractInstance); } - addContractArtifact(classId: Fr, artifact: ContractArtifact): void { + async addContractArtifact(classId: Fr, artifact: ContractArtifact) { this.contractArtifacts.set(classId.toString(), artifact); + const classIdStr = classId.toString(); + const publicFns = artifact.nonDispatchPublicFunctions; + if (publicFns.length !== 0) { + for (const fn of publicFns) { + const actualFnName = `${fn.name}`; + const fnSelector = await getFunctionSelector(actualFnName, artifact); + const key = `${classIdStr}:${fnSelector.toString()}`; + + const longFnName = `${artifact.name}.${actualFnName}`; + this.debugFunctionName.set(key, longFnName); + } + } } ///////////////////////////////////////////////////////////// @@ -73,11 +87,24 @@ export class SimpleContractDataSource implements ContractDataSource { } this.logger.debug(`Retrieved contract artifact for address: ${address}`); this.logger.debug(`Contract class ID: ${contractInstance.currentContractClassId}`); - return Promise.resolve(this.contractArtifacts.get(contractInstance!.currentContractClassId.toString())); + return this.contractArtifacts.get(contractInstance!.currentContractClassId.toString()); } - getDebugFunctionName(_address: AztecAddress, _selector: FunctionSelector): Promise { - return Promise.resolve(PUBLIC_DISPATCH_FN_NAME); + async getDebugFunctionName(address: AztecAddress, selector: FunctionSelector): Promise { + const contractInstance = await this.getContract(address); + if (!contractInstance) { + this.logger.warn( + `Couldn't get fn name for debugging. Contract not in tester's ContractDataSource. Using selector:${selector} instead...`, + ); + return `selector:${selector.toString()}`; + } + const key = `${contractInstance.currentContractClassId.toString()}:${selector.toString()}`; + const fnName = this.debugFunctionName.get(key); + if (!fnName) { + this.logger.warn(`Couldn't get fn name for debugging. Using selector:${selector} instead...`); + return selector.toString(); + } + return fnName; } registerContractFunctionSignatures(_address: AztecAddress, _signatures: string[]): Promise { diff --git a/yarn-project/simulator/src/public/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/public/avm/opcodes/external_calls.ts index eb2b812ea6c7..4b66ef711870 100644 --- a/yarn-project/simulator/src/public/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/public/avm/opcodes/external_calls.ts @@ -70,6 +70,9 @@ abstract class ExternalCall extends Instruction { // Track the success status directly context.machineState.nestedCallSuccess = success; + // Account for all instructions executed in the nested call + context.machineState.instrCounter += nestedCallResults.totalInstructions; + // If the nested call reverted, we try to save the reason and the revert data. // This will be used by the caller to try to reconstruct the call stack. // This is only a heuristic and may not always work. It is intended to work diff --git a/yarn-project/simulator/src/public/executor_metrics.ts b/yarn-project/simulator/src/public/executor_metrics.ts index 7fe14a34e34a..aa1bb17a72c0 100644 --- a/yarn-project/simulator/src/public/executor_metrics.ts +++ b/yarn-project/simulator/src/public/executor_metrics.ts @@ -1,3 +1,4 @@ +import type { RevertCode } from '@aztec/stdlib/avm'; import { Attributes, type Histogram, @@ -8,11 +9,16 @@ import { ValueType, } from '@aztec/telemetry-client'; -export class ExecutorMetrics { +import type { ExecutorMetricsInterface } from './executor_metrics_interface.js'; + +export class ExecutorMetrics implements ExecutorMetricsInterface { public readonly tracer: Tracer; private fnCount: UpDownCounter; private fnDuration: Histogram; private manaPerSecond: Histogram; + private manaUsed: Histogram; + private totalInstructions: Histogram; + private txHashing: Histogram; private privateEffectsInsertions: Histogram; constructor(client: TelemetryClient, name = 'PublicExecutor') { @@ -35,20 +41,53 @@ export class ExecutorMetrics { valueType: ValueType.INT, }); - this.privateEffectsInsertions = meter.createHistogram(Metrics.PUBLIC_EXECUTION_PRIVATE_EFFECTS_INSERTION, { + this.manaUsed = meter.createHistogram(Metrics.PUBLIC_EXECUTOR_SIMULATION_MANA_USED, { + description: 'Total mana used', + unit: 'mana', + valueType: ValueType.INT, + }); + + this.totalInstructions = meter.createHistogram(Metrics.PUBLIC_EXECUTOR_SIMULATION_TOTAL_INSTRUCTIONS, { + description: 'Total number of instructions executed', + unit: 'instructions', + valueType: ValueType.INT, + }); + + this.txHashing = meter.createHistogram(Metrics.PUBLIC_EXECUTOR_TX_HASHING, { + description: 'Tx hashing time', + unit: 'ms', + valueType: ValueType.INT, + }); + + this.privateEffectsInsertions = meter.createHistogram(Metrics.PUBLIC_EXECUTOR_PRIVATE_EFFECTS_INSERTION, { description: 'Private effects insertion time', unit: 'us', valueType: ValueType.INT, }); } - recordFunctionSimulation(durationMs: number, manaUsed: number, fnName: string) { + startRecordingTxSimulation(_txLabel: string) { + // do nothing (unimplemented) + } + + stopRecordingTxSimulation(_txLabel: string, _revertedCode?: RevertCode) { + // do nothing (unimplemented) + } + + recordEnqueuedCallSimulation(fnName: string, durationMs: number, manaUsed: number, totalInstructions: number) { this.fnCount.add(1, { [Attributes.OK]: true, [Attributes.APP_CIRCUIT_NAME]: fnName, - [Attributes.MANA_USED]: manaUsed, }); - this.fnDuration.record(Math.ceil(durationMs)); + this.manaUsed.record(Math.ceil(manaUsed), { + [Attributes.APP_CIRCUIT_NAME]: fnName, + }); + this.totalInstructions.record(Math.ceil(totalInstructions), { + [Attributes.APP_CIRCUIT_NAME]: fnName, + }); + this.fnDuration.record(Math.ceil(durationMs), { + [Attributes.APP_CIRCUIT_NAME]: fnName, + }); if (durationMs > 0 && manaUsed > 0) { const manaPerSecond = Math.round((manaUsed * 1000) / durationMs); this.manaPerSecond.record(manaPerSecond, { @@ -57,12 +96,21 @@ export class ExecutorMetrics { } } - recordFunctionSimulationFailure() { + recordEnqueuedCallSimulationFailure( + _fnName: string, + _durationMs: number, + _manaUsed: number, + _totalInstructions: number, + ) { this.fnCount.add(1, { [Attributes.OK]: false, }); } + recordTxHashComputation(durationMs: number) { + this.txHashing.record(Math.ceil(durationMs)); + } + recordPrivateEffectsInsertion(durationUs: number, type: 'revertible' | 'non-revertible') { this.privateEffectsInsertions.record(Math.ceil(durationUs), { [Attributes.REVERTIBILITY]: type, diff --git a/yarn-project/simulator/src/public/executor_metrics_interface.ts b/yarn-project/simulator/src/public/executor_metrics_interface.ts new file mode 100644 index 000000000000..e3488d05a494 --- /dev/null +++ b/yarn-project/simulator/src/public/executor_metrics_interface.ts @@ -0,0 +1,15 @@ +import type { RevertCode } from '@aztec/stdlib/avm'; + +export interface ExecutorMetricsInterface { + startRecordingTxSimulation(txLabel: string): void; + stopRecordingTxSimulation(txLabel: string, revertedCode?: RevertCode): void; + recordEnqueuedCallSimulation(fnName: string, durationMs: number, manaUsed: number, totalInstructions: number): void; + recordEnqueuedCallSimulationFailure( + fnName: string, + durationMs: number, + manaUsed: number, + totalInstructions: number, + ): void; + recordTxHashComputation(durationMs: number): void; + recordPrivateEffectsInsertion(durationUs: number, type: 'revertible' | 'non-revertible'): void; +} diff --git a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts index 96afbe604045..ad6c8fea2ed5 100644 --- a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts +++ b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts @@ -9,17 +9,19 @@ import { GlobalVariables, PublicCallRequestWithCalldata, type Tx } from '@aztec/ import { NativeWorldStateService } from '@aztec/world-state'; import { BaseAvmSimulationTester } from '../avm/fixtures/base_avm_simulation_tester.js'; -import { getContractFunctionAbi, getFunctionSelector } from '../avm/fixtures/index.js'; +import { DEFAULT_BLOCK_NUMBER, getContractFunctionAbi, getFunctionSelector } from '../avm/fixtures/index.js'; import { SimpleContractDataSource } from '../avm/fixtures/simple_contract_data_source.js'; import { PublicContractsDB, PublicTreesDB } from '../public_db_sources.js'; -import { type PublicTxResult, PublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; +import { MeasuredPublicTxSimulator } from '../public_tx_simulator/measured_public_tx_simulator.js'; +import type { PublicTxResult } from '../public_tx_simulator/public_tx_simulator.js'; +import { TestExecutorMetrics } from '../test_executor_metrics.js'; import { createTxForPublicCalls } from './utils.js'; const TIMESTAMP = new Fr(99833); const DEFAULT_GAS_FEES = new GasFees(2, 3); -export const DEFAULT_BLOCK_NUMBER = 42; export type TestEnqueuedCall = { + sender?: AztecAddress; address: AztecAddress; fnName: string; args: any[]; @@ -34,15 +36,41 @@ export type TestEnqueuedCall = { */ export class PublicTxSimulationTester extends BaseAvmSimulationTester { private txCount = 0; - - constructor(private merkleTree: MerkleTreeWriteOperations, contractDataSource: SimpleContractDataSource) { + private simulator: MeasuredPublicTxSimulator; + private metricsPrefix?: string; + + constructor( + merkleTree: MerkleTreeWriteOperations, + contractDataSource: SimpleContractDataSource, + globals: GlobalVariables = defaultGlobals(), + private metrics: TestExecutorMetrics = new TestExecutorMetrics(), + ) { super(contractDataSource, merkleTree); + + const treesDB = new PublicTreesDB(merkleTree); + const contractsDB = new PublicContractsDB(contractDataSource); + + this.simulator = new MeasuredPublicTxSimulator( + treesDB, + contractsDB, + globals, + /*doMerkleOperations=*/ true, + /*skipFeeEnforcement=*/ false, + this.metrics, + ); } - public static async create(): Promise { + public static async create( + globals: GlobalVariables = defaultGlobals(), + metrics: TestExecutorMetrics = new TestExecutorMetrics(), + ): Promise { const contractDataSource = new SimpleContractDataSource(); const merkleTree = await (await NativeWorldStateService.tmp()).fork(); - return new PublicTxSimulationTester(merkleTree, contractDataSource); + return new PublicTxSimulationTester(merkleTree, contractDataSource, globals, metrics); + } + + public setMetricsPrefix(prefix: string) { + this.metricsPrefix = prefix; } public async createTx( @@ -54,10 +82,14 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { /* need some unique first nullifier for note-nonce computations */ firstNullifier = new Fr(420000 + this.txCount++), ): Promise { - const setupCallRequests = await asyncMap(setupCalls, call => this.#createPubicCallRequestForCall(call, sender)); - const appCallRequests = await asyncMap(appCalls, call => this.#createPubicCallRequestForCall(call, sender)); + const setupCallRequests = await asyncMap(setupCalls, call => + this.#createPubicCallRequestForCall(call, call.sender ?? sender), + ); + const appCallRequests = await asyncMap(appCalls, call => + this.#createPubicCallRequestForCall(call, call.sender ?? sender), + ); const teardownCallRequest = teardownCall - ? await this.#createPubicCallRequestForCall(teardownCall, sender) + ? await this.#createPubicCallRequestForCall(teardownCall, teardownCall.sender ?? sender) : undefined; return createTxForPublicCalls(firstNullifier, setupCallRequests, appCallRequests, teardownCallRequest, feePayer); @@ -71,24 +103,48 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { feePayer: AztecAddress = sender, /* need some unique first nullifier for note-nonce computations */ firstNullifier = new Fr(420000 + this.txCount++), - globals = defaultGlobals(), + txLabel: string = 'unlabeledTx', ): Promise { const tx = await this.createTx(sender, setupCalls, appCalls, teardownCall, feePayer, firstNullifier); await this.setFeePayerBalance(feePayer); - const treesDB = new PublicTreesDB(this.merkleTree); - const contractsDB = new PublicContractsDB(this.contractDataSource); - const simulator = new PublicTxSimulator(treesDB, contractsDB, globals, /*doMerkleOperations=*/ true); + const txLabelWithCount = `${txLabel}.${this.txCount - 1}`; + const fullTxLabel = this.metricsPrefix ? `${this.metricsPrefix}.${txLabelWithCount}` : txLabelWithCount; - const startTime = performance.now(); - const avmResult = await simulator.simulate(tx); - const endTime = performance.now(); - this.logger.debug(`Public transaction simulation took ${endTime - startTime}ms`); + const avmResult = await this.simulator.simulate(tx, fullTxLabel); + + // Something like this is often useful for debugging: + //if (avmResult.revertReason) { + // // resolve / enrich revert reason + // const lastAppCall = appCalls[appCalls.length - 1]; + + // const contractArtifact = + // lastAppCall.contractArtifact || (await this.contractDataSource.getContractArtifact(lastAppCall.address)); + // const fnAbi = getContractFunctionAbi(lastAppCall.fnName, contractArtifact!); + // const revertReason = resolveAssertionMessageFromRevertData(avmResult.revertReason.revertData, fnAbi!); + // this.logger.debug(`Revert reason: ${revertReason}`); + //} return avmResult; } + public async simulateTxWithLabel( + txLabel: string, + sender: AztecAddress, + setupCalls?: TestEnqueuedCall[], + appCalls?: TestEnqueuedCall[], + teardownCall?: TestEnqueuedCall, + feePayer?: AztecAddress, + firstNullifier?: Fr, + ): Promise { + return await this.simulateTx(sender, setupCalls, appCalls, teardownCall, feePayer, firstNullifier, txLabel); + } + + public prettyPrintMetrics() { + this.metrics.prettyPrint(); + } + async #createPubicCallRequestForCall( call: TestEnqueuedCall, sender: AztecAddress, diff --git a/yarn-project/simulator/src/public/index.ts b/yarn-project/simulator/src/public/index.ts index 454196ae6502..24cf9008f7b1 100644 --- a/yarn-project/simulator/src/public/index.ts +++ b/yarn-project/simulator/src/public/index.ts @@ -1,5 +1,5 @@ export * from '../common/db_interfaces.js'; -export * from './public_tx_simulator/public_tx_simulator.js'; +export * from './public_tx_simulator/index.js'; export * from './public_db_sources.js'; export { PublicProcessor, PublicProcessorFactory } from './public_processor/public_processor.js'; export { SideEffectTrace } from './side_effect_trace.js'; diff --git a/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts b/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts index 596b8d078660..9587c4c262de 100644 --- a/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts +++ b/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts @@ -47,7 +47,7 @@ describe('Public Processor app tests: TokenContract', () => { getTelemetryClient(), ); - tester = new PublicTxSimulationTester(merkleTrees, contractDataSource); + tester = new PublicTxSimulationTester(merkleTrees, contractDataSource, globals); // make sure tx senders have fee balance await tester.setFeePayerBalance(admin); diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.ts b/yarn-project/simulator/src/public/public_processor/public_processor.ts index 3167901f0b3e..db85bb1c896e 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -34,7 +34,7 @@ import { import { ForkCheckpoint } from '@aztec/world-state/native'; import { PublicContractsDB, PublicTreesDB } from '../public_db_sources.js'; -import { PublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; +import { type PublicTxSimulator, TelemetryPublicTxSimulator } from '../public_tx_simulator/index.js'; import { PublicProcessorMetrics } from './public_processor_metrics.js'; /** @@ -87,8 +87,8 @@ export class PublicProcessorFactory { doMerkleOperations: boolean, skipFeeEnforcement: boolean, telemetryClient: TelemetryClient, - ) { - return new PublicTxSimulator( + ): PublicTxSimulator { + return new TelemetryPublicTxSimulator( treesDB, contractsDB, globalVariables, @@ -488,7 +488,7 @@ export class PublicProcessor implements Traceable { if (phase.reverted) { this.metrics.recordRevertedPhase(phase.phase); } else { - this.metrics.recordPhaseDuration(phase.phase, phase.durationMs); + this.metrics.recordPhaseDuration(phase.phase, phase.durationMs ?? 0); } }); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm.test.ts new file mode 100644 index 000000000000..5bbefbb07fc3 --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm.test.ts @@ -0,0 +1,18 @@ +import { createLogger } from '@aztec/foundation/log'; + +import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; +import { ammTest } from './amm_test.js'; + +describe('AVM Witgen & Circuit apps tests: AMM', () => { + const logger = createLogger('public-tx-apps-tests-amm'); + + let tester: PublicTxSimulationTester; + + beforeEach(async () => { + tester = await PublicTxSimulationTester.create(); + }); + + it('amm operations', async () => { + await ammTest(tester, logger); + }); +}); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm_test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm_test.ts new file mode 100644 index 000000000000..808a69847c72 --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm_test.ts @@ -0,0 +1,316 @@ +import { Fr } from '@aztec/foundation/fields'; +import type { Logger } from '@aztec/foundation/log'; +import { AMMContractArtifact } from '@aztec/noir-contracts.js/AMM'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; + +import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; +import { deployToken } from './token_test.js'; + +const INITIAL_TOKEN_BALANCE = 1_000_000_000n; +/** + * THIS TEST IS BRITTLE! If it breaks, don't try fixing it. + * `.skip` it or literally just delete it and notify AVM team. + * You do NOT need permission to remove this test! + */ +export async function ammTest(tester: PublicTxSimulationTester, logger: Logger) { + const startTime = performance.now(); + + const admin = AztecAddress.fromNumber(42); + const sender = AztecAddress.fromNumber(111); + + logger.debug(`Deploying tokens`); + const token0 = await deployToken(tester, admin, /*seed=*/ 0); + const token1 = await deployToken(tester, admin, /*seed=*/ 1); + const liquidityToken = await deployToken(tester, admin, /*seed=*/ 2); + logger.debug(`Deploying AMM`); + const constructorArgs = [token0, token1, liquidityToken]; + const amm = await tester.registerAndDeployContract( + constructorArgs, + /*deployer=*/ admin, + AMMContractArtifact, + /*skipNullifierInsertion=*/ false, + /*seed=*/ 3, + ); + + const ammConstructorResult = await tester.simulateTxWithLabel( + /*txLabel=*/ 'constructor', + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'constructor', + args: constructorArgs, + address: amm.address, + }, + ], + ); + expect(ammConstructorResult.revertCode.isOK()).toBe(true); + + logger.debug(`Setting AMM as minter for liquidity token`); + + // set the AMM as the minter for the liquidity token + const setMinterResult = await tester.simulateTxWithLabel( + /*txLabel=*/ 'set_minter', + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'set_minter', + args: [/*minter=*/ amm, /*approve=*/ true], + address: liquidityToken.address, + }, + ], + ); + expect(setMinterResult.revertCode.isOK()).toBe(true); + + logger.debug(`Adding liquidity`); + const amount0Max = (INITIAL_TOKEN_BALANCE * 6n) / 10n; + const amount0Min = (INITIAL_TOKEN_BALANCE * 4n) / 10n; + const amount1Max = (INITIAL_TOKEN_BALANCE * 5n) / 10n; + const amount1Min = (INITIAL_TOKEN_BALANCE * 4n) / 10n; + + const addLiquidityResult = await addLiquidity( + tester, + sender, + /*amm=*/ amm, + /*token0=*/ token0, + /*token1=*/ token1, + /*liquidityToken=*/ liquidityToken, + /*amount0Max=*/ amount0Max, + /*amount1Max=*/ amount1Max, + /*amount0Min=*/ amount0Min, + /*amount1Min=*/ amount1Min, + ); + expect(addLiquidityResult.revertCode.isOK()).toBe(true); + + logger.debug(`Swapping tokens`); + const swapResult = await swapExactTokensForTokens( + tester, + sender, + /*amm=*/ amm, + /*tokenIn=*/ token0, + /*tokenOut=*/ token1, + /*amountIn=*/ amount0Min / 10n, // something smaller than total liquidity + /*amountOutMin=*/ amount1Min / 100n, // something even smaller + ); + expect(swapResult.revertCode.isOK()).toBe(true); + + logger.debug(`Removing liquidity`); + const removeLiquidityResult = await removeLiquidity( + tester, + sender, + /*amm=*/ amm, + /*token0=*/ token0, + /*token1=*/ token1, + /*liquidityToken=*/ liquidityToken, + /*liquidity=*/ 100n, + /*amount0Min=*/ 1n, // remove some tiny amount + /*amount1Min=*/ 1n, + ); + expect(removeLiquidityResult.revertCode.isOK()).toBe(true); + + const endTime = performance.now(); + + logger.info(`AMM public tx simulator test took ${endTime - startTime}ms\n`); +} + +async function addLiquidity( + tester: PublicTxSimulationTester, + sender: AztecAddress, + amm: ContractInstanceWithAddress, + token0: ContractInstanceWithAddress, + token1: ContractInstanceWithAddress, + liquidityToken: ContractInstanceWithAddress, + amount0Max: bigint, + amount1Max: bigint, + amount0Min: bigint, + amount1Min: bigint, + _nonce?: bigint, +) { + const refundToken0PartialNote = { + commitment: new Fr(42), + }; + const refundToken1PartialNote = { + commitment: new Fr(66), + }; + const liquidityPartialNote = { + commitment: new Fr(99), + }; + + return await tester.simulateTxWithLabel( + /*txLabel=*/ 'add_liquidity', + /*sender=*/ sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + // token0.transfer_to_public enqueues a call to _increase_public_balance + { + sender: token0.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_increase_public_balance', + args: [/*to=*/ amm.address, /*amount=*/ amount0Max], + address: token0.address, + }, + // token0.prepare_private_balance_increase enqueues a call to _store_balances_set_partial_note + { + sender: token0.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_store_balances_set_partial_note', + args: [refundToken0PartialNote], + address: token0.address, + }, + // token1.transfer_to_public enqueues a call to _increase_public_balance + { + sender: token1.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_increase_public_balance', + args: [/*to=*/ amm.address, /*amount=*/ amount1Max], + address: token1.address, + }, + // token1.prepare_private_balance_increase enqueues a call to _store_balances_set_partial_note + { + sender: token1.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_store_balances_set_partial_note', + args: [refundToken1PartialNote], + address: token1.address, + }, + // liquidityToken.prepare_private_balance_increase enqueues a call to _store_balances_set_partial_note + { + sender: liquidityToken.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_store_balances_set_partial_note', + args: [liquidityPartialNote], + address: liquidityToken.address, + }, + // amm.add_liquidity enqueues a call to _add_liquidity + { + sender: amm.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_add_liquidity', + args: [ + /*config=*/ { + token0: token0.address, + token1: token1.address, + // eslint-disable-next-line camelcase + liquidity_token: liquidityToken.address, + }, + refundToken0PartialNote, + refundToken1PartialNote, + liquidityPartialNote, + amount0Max, + amount1Max, + amount0Min, + amount1Min, + ], + address: amm.address, + }, + ], + ); +} + +async function swapExactTokensForTokens( + tester: PublicTxSimulationTester, + sender: AztecAddress, + amm: ContractInstanceWithAddress, + tokenIn: ContractInstanceWithAddress, + tokenOut: ContractInstanceWithAddress, + amountIn: bigint, + amountOutMin: bigint, + _nonce?: bigint, +) { + const tokenOutPartialNote = { + commitment: new Fr(66), + }; + + return await tester.simulateTxWithLabel( + /*txLabel=*/ 'swap_exact_tokens_for_tokens', + /*sender=*/ sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + // tokenIn.transfer_to_public enqueues a call to _increase_public_balance + { + sender: tokenIn.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_increase_public_balance', + args: [/*to=*/ amm.address, /*amount=*/ amountIn], + address: tokenIn.address, + }, + // tokenOut.prepare_private_balance_increase enqueues a call to _store_balances_set_partial_note + { + sender: tokenOut.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_store_balances_set_partial_note', + args: [tokenOutPartialNote], + address: tokenOut.address, + }, + + { + sender: amm.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_swap_exact_tokens_for_tokens', + args: [tokenIn.address, tokenOut.address, amountIn, amountOutMin, tokenOutPartialNote], + address: amm.address, + }, + ], + ); +} + +async function removeLiquidity( + tester: PublicTxSimulationTester, + sender: AztecAddress, + amm: ContractInstanceWithAddress, + token0: ContractInstanceWithAddress, + token1: ContractInstanceWithAddress, + liquidityToken: ContractInstanceWithAddress, + liquidity: bigint, + amount0Min: bigint, + amount1Min: bigint, + _nonce?: bigint, +) { + const token0PartialNote = { + commitment: new Fr(111), + }; + const token1PartialNote = { + commitment: new Fr(222), + }; + + return await tester.simulateTxWithLabel( + /*txLabel=*/ 'remove_liquidity', + /*sender=*/ sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + // liquidityToken.transfer_to_public enqueues a call to _increase_public_balance + { + sender: liquidityToken.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_increase_public_balance', + args: [/*to=*/ amm.address, /*amount=*/ liquidity], + address: liquidityToken.address, + }, + // token0.prepare_private_balance_increase enqueues a call to _store_balances_set_partial_note + { + sender: token0.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_store_balances_set_partial_note', + args: [token0PartialNote], + address: token0.address, + }, + // token1.prepare_private_balance_increase enqueues a call to _store_balances_set_partial_note + { + sender: token1.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_store_balances_set_partial_note', + args: [token1PartialNote], + address: token1.address, + }, + // amm.remove_liquidity enqueues a call to _remove_liquidity + { + sender: amm.address, // INTERNAL FUNCTION! Sender must be 'this'. + fnName: '_remove_liquidity', + args: [ + /*config=*/ { + token0: token0.address, + token1: token1.address, + // eslint-disable-next-line camelcase + liquidity_token: liquidityToken.address, + }, + liquidity, + token0PartialNote, + token1PartialNote, + amount0Min, + amount1Min, + ], + address: amm.address, + }, + ], + ); +} diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/bench.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/bench.test.ts new file mode 100644 index 000000000000..8f3759100dac --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/bench.test.ts @@ -0,0 +1,203 @@ +import { randomInt } from '@aztec/foundation/crypto'; +import { Fr } from '@aztec/foundation/fields'; +import { createLogger } from '@aztec/foundation/log'; +import { AvmGadgetsTestContractArtifact } from '@aztec/noir-contracts.js/AvmGadgetsTest'; +import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; + +import { writeFileSync } from 'fs'; + +import { PublicTxSimulationTester, defaultGlobals } from '../../fixtures/public_tx_simulation_tester.js'; +import { TestExecutorMetrics } from '../../test_executor_metrics.js'; +import { ammTest } from './amm_test.js'; +import { tokenTest } from './token_test.js'; + +describe('Public TX simulator apps tests: benchmarks', () => { + const logger = createLogger('public-tx-apps-tests-bench'); + + const metrics = new TestExecutorMetrics(); + let tester: PublicTxSimulationTester; + + beforeEach(async () => { + tester = await PublicTxSimulationTester.create(defaultGlobals(), metrics); + }); + + afterAll(() => { + if (process.env.BENCH_OUTPUT) { + writeFileSync(process.env.BENCH_OUTPUT, metrics.toJSON()); + } else if (process.env.BENCH_OUTPUT_MD) { + writeFileSync(process.env.BENCH_OUTPUT_MD, metrics.toPrettyString()); + } else { + logger.info(`\n`); // sometimes jest tests obscure the last line(s) + logger.info(metrics.toPrettyString()); + } + }); + + it('Token Contract test', async () => { + tester.setMetricsPrefix('Token'); + await tokenTest(tester, logger); + }); + + it('AMM Contract test', async () => { + tester.setMetricsPrefix('AMM'); + await ammTest(tester, logger); + }); + + it('AVM simulator bulk test', async () => { + tester.setMetricsPrefix('AvmTest'); + const deployer = AztecAddress.fromNumber(42); + + const avmTestContract = await tester.registerAndDeployContract( + /*constructorArgs=*/ [], + deployer, + /*contractArtifact=*/ AvmTestContractArtifact, + ); + + // Get a deployed contract instance to pass to the contract + // for it to use as "expected" values when testing contract instance retrieval. + const expectContractInstance = avmTestContract; + const argsField = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); + const argsU8 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => new Fr(x)); + const args = [ + argsField, + argsU8, + /*getInstanceForAddress=*/ expectContractInstance.address, + /*expectedDeployer=*/ expectContractInstance.deployer, + /*expectedClassId=*/ expectContractInstance.currentContractClassId, + /*expectedInitializationHash=*/ expectContractInstance.initializationHash, + ]; + + const bulkResult = await tester.simulateTxWithLabel( + /*txLabel=*/ 'bulk_testing', + /*sender=*/ deployer, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: avmTestContract.address, + fnName: 'bulk_testing', + args, + }, + ], + ); + expect(bulkResult.revertCode.isOK()).toBe(true); + }); + + describe('AVM gadgets tests', () => { + const deployer = AztecAddress.fromNumber(42); + + let tester: PublicTxSimulationTester; + let avmGadgetsTestContract: ContractInstanceWithAddress; + + beforeAll(async () => { + tester = await PublicTxSimulationTester.create(defaultGlobals(), metrics); + avmGadgetsTestContract = await tester.registerAndDeployContract( + /*constructorArgs=*/ [], + deployer, + /*contractArtifact=*/ AvmGadgetsTestContractArtifact, + ); + tester.setMetricsPrefix(`AvmGadgetsTest`); + }); + + describe.each( + // sha sizes + [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 255, 256, 511, 512, 2048], + )('sha256_hash_%s', (length: number) => { + it(`sha256_hash_${length}`, async () => { + const result = await tester.simulateTxWithLabel( + /*txLabel=*/ `sha256_hash_${length}`, + /*sender=*/ deployer, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: avmGadgetsTestContract.address, + fnName: `sha256_hash_${length}`, + args: [/*input=*/ Array.from({ length: length }, () => randomInt(2 ** 8))], + }, + ], + ); + expect(result.revertCode.isOK()).toBe(true); + }); + }); + + it('keccak_hash', async () => { + const result = await tester.simulateTxWithLabel( + /*txLabel=*/ 'keccak_hash', + /*sender=*/ deployer, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: avmGadgetsTestContract.address, + fnName: 'keccak_hash', + args: [/*input=*/ Array.from({ length: 10 }, () => randomInt(2 ** 8))], + }, + ], + ); + expect(result.revertCode.isOK()).toBe(true); + }); + + it('keccak_f1600', async () => { + const result = await tester.simulateTxWithLabel( + /*txLabel=*/ 'keccak_f1600', + /*sender=*/ deployer, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: avmGadgetsTestContract.address, + fnName: 'keccak_f1600', + args: [/*input=*/ Array.from({ length: 25 }, () => randomInt(2 ** 32))], + }, + ], + ); + expect(result.revertCode.isOK()).toBe(true); + }); + + it('poseidon2_hash', async () => { + const result = await tester.simulateTxWithLabel( + /*txLabel=*/ 'poseidon2_hash', + /*sender=*/ deployer, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: avmGadgetsTestContract.address, + fnName: 'poseidon2_hash', + args: [/*input=*/ Array.from({ length: 10 }, () => Fr.random())], + }, + ], + ); + expect(result.revertCode.isOK()).toBe(true); + }); + + it('pedersen_hash', async () => { + const result = await tester.simulateTxWithLabel( + /*txLabel=*/ 'pedersen_hash', + /*sender=*/ deployer, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: avmGadgetsTestContract.address, + fnName: 'pedersen_hash', + args: [/*input=*/ Array.from({ length: 10 }, () => Fr.random())], + }, + ], + ); + expect(result.revertCode.isOK()).toBe(true); + }); + + it('pedersen_hash_with_index', async () => { + const result = await tester.simulateTxWithLabel( + /*txLabel=*/ 'pedersen_hash_with_index', + /*sender=*/ deployer, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: avmGadgetsTestContract.address, + fnName: 'pedersen_hash_with_index', + args: [/*input=*/ Array.from({ length: 10 }, () => Fr.random())], + }, + ], + ); + expect(result.revertCode.isOK()).toBe(true); + }); + }); +}); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts index 861aaadfd36f..7fcab3fb6a9e 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts @@ -1,116 +1,18 @@ -import { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; -import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; -import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; -import type { PublicTxResult } from '../public_tx_simulator.js'; +import { tokenTest } from './token_test.js'; describe('Public TX simulator apps tests: TokenContract', () => { const logger = createLogger('public-tx-apps-tests-token'); - const admin = AztecAddress.fromNumber(42); - const sender = AztecAddress.fromNumber(111); - const receiver = AztecAddress.fromNumber(222); - let token: ContractInstanceWithAddress; - let simTester: PublicTxSimulationTester; + let tester: PublicTxSimulationTester; - beforeEach(async () => { - simTester = await PublicTxSimulationTester.create(); - const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)]; - token = await simTester.registerAndDeployContract(constructorArgs, /*deployer=*/ admin, TokenContractArtifact); - - const constructorResult = await simTester.simulateTx( - /*sender=*/ admin, - /*setupCalls=*/ [], - /*appCalls=*/ [ - { - address: token.address, - fnName: 'constructor', - args: constructorArgs, - }, - ], - ); - expect(constructorResult.revertCode.isOK()).toBe(true); + beforeAll(async () => { + tester = await PublicTxSimulationTester.create(); }); - it('token mint, transfer, burn (and check balances)', async () => { - const startTime = performance.now(); - - const mintAmount = 100n; - const transferAmount = 50n; - const nonce = new Fr(0); - - await checkBalance(sender, 0n); - - const mintResult = await simTester.simulateTx( - /*sender=*/ admin, - /*setupCalls=*/ [], - /*appCalls=*/ [ - { - address: token.address, - fnName: 'mint_to_public', - args: [/*to=*/ sender, mintAmount], - }, - ], - ); - expect(mintResult.revertCode.isOK()).toBe(true); - await checkBalance(sender, mintAmount); - - const transferResult = await simTester.simulateTx( - /*sender=*/ sender, - /*setupCalls=*/ [], - /*appCalls=*/ [ - { - address: token.address, - fnName: 'transfer_in_public', - args: [/*from=*/ sender, /*to=*/ receiver, transferAmount, nonce], - }, - ], - ); - expect(transferResult.revertCode.isOK()).toBe(true); - await checkBalance(sender, mintAmount - transferAmount); - await checkBalance(receiver, transferAmount); - - const burnResult = await simTester.simulateTx( - /*sender=*/ receiver, - /*setupCalls=*/ [], - /*appCalls=*/ [ - { - address: token.address, - fnName: 'burn_public', - args: [/*from=*/ receiver, transferAmount, nonce], - }, - ], - ); - expect(burnResult.revertCode.isOK()).toBe(true); - await checkBalance(receiver, 0n); - - const endTime = performance.now(); - logger.verbose(`TokenContract public tx simulator test took ${endTime - startTime}ms\n`); + it('token constructor, mint, transfer, burn, check balances)', async () => { + await tokenTest(tester, logger); }); - - const checkBalance = async (account: AztecAddress, expectedBalance: bigint) => { - const balResult = await simTester.simulateTx( - sender, - /*setupCalls=*/ [], - /*appCalls=*/ [ - { - address: token.address, - fnName: 'balance_of_public', - args: [/*owner=*/ account], - isStaticCall: true, - }, - ], - ); - expect(balResult.revertCode.isOK()).toBe(true); - expectAppCall0Output(balResult, [new Fr(expectedBalance)]); - }; }); - -function expectAppCall0Output(txResult: PublicTxResult, expectedOutput: Fr[]): void { - expect(txResult.processedPhases).toEqual([ - expect.objectContaining({ returnValues: [expect.objectContaining({ values: expectedOutput })] }), - ]); -} diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token_test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token_test.ts new file mode 100644 index 000000000000..e2e510b2f2c3 --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token_test.ts @@ -0,0 +1,138 @@ +import { Fr } from '@aztec/foundation/fields'; +import type { Logger } from '@aztec/foundation/log'; +import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; + +import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; + +export async function tokenTest(tester: PublicTxSimulationTester, logger: Logger) { + const startTime = performance.now(); + + const admin = AztecAddress.fromNumber(42); + const sender = AztecAddress.fromNumber(111); + const receiver = AztecAddress.fromNumber(222); + + const token = await deployToken(tester, admin); + + const mintAmount = 100n; + const mintResult = await tester.simulateTxWithLabel( + /*txLabel=*/ 'mint_to_public', + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'mint_to_public', + args: [/*to=*/ sender, mintAmount], + }, + ], + ); + expect(mintResult.revertCode.isOK()).toBe(true); + await checkBalance(tester, token, sender, sender, mintAmount); + + const nonce = new Fr(0); + const transferAmount = 50n; + const transferResult = await tester.simulateTxWithLabel( + /*txLabel=*/ 'transfer_in_public', + /*sender=*/ sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'transfer_in_public', + args: [/*from=*/ sender, /*to=*/ receiver, transferAmount, nonce], + }, + ], + ); + expect(transferResult.revertCode.isOK()).toBe(true); + await checkBalance(tester, token, sender, receiver, mintAmount - transferAmount); + await checkBalance(tester, token, sender, receiver, transferAmount); + + const balResult = await tester.simulateTxWithLabel( + /*txLabel=*/ 'balance_of_public', + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'balance_of_public', + args: [/*owner=*/ receiver], + isStaticCall: true, + }, + ], + ); + expect(balResult.revertCode.isOK()).toBe(true); + + const burnResult = await tester.simulateTxWithLabel( + /*txLabel=*/ 'burn_public', + /*sender=*/ receiver, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'burn_public', + args: [/*from=*/ receiver, transferAmount, nonce], + }, + ], + ); + expect(burnResult.revertCode.isOK()).toBe(true); + await checkBalance(tester, token, sender, receiver, 0n); + + const endTime = performance.now(); + + logger.info(`TokenContract public tx simulator test took ${endTime - startTime}ms\n`); +} + +export async function deployToken(tester: PublicTxSimulationTester, admin: AztecAddress, seed = 0) { + const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)]; + const token = await tester.registerAndDeployContract( + constructorArgs, + /*deployer=*/ admin, + TokenContractArtifact, + /*skipNullifierInsertion=*/ false, + seed, + ); + + const result = await tester.simulateTxWithLabel( + /*txLabel=*/ 'Token.constructor', + /*sender=*/ admin, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + fnName: 'constructor', + args: constructorArgs, + address: token.address, + }, + ], + ); + expect(result.revertCode.isOK()).toBe(true); + return token; +} + +async function checkBalance( + tester: PublicTxSimulationTester, + token: ContractInstanceWithAddress, + sender: AztecAddress, + account: AztecAddress, + expectedBalance: bigint, +) { + const balResult = await tester.simulateTxWithLabel( + /*txLabel=*/ 'balance_of_public', + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: token.address, + fnName: 'balance_of_public', + args: [/*owner=*/ account], + isStaticCall: true, + }, + ], + ); + expect(balResult.revertCode.isOK()).toBe(true); + // should be 1 call with 1 return value that is expectedBalance + expect(balResult.processedPhases).toEqual([ + expect.objectContaining({ returnValues: [expect.objectContaining({ values: [new Fr(expectedBalance)] })] }), + ]); +} diff --git a/yarn-project/simulator/src/public/public_tx_simulator/index.ts b/yarn-project/simulator/src/public/public_tx_simulator/index.ts new file mode 100644 index 000000000000..314a17fb355f --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator/index.ts @@ -0,0 +1,2 @@ +export * from './public_tx_simulator.js'; +export { TelemetryPublicTxSimulator } from './telemetry_public_tx_simulator.js'; diff --git a/yarn-project/simulator/src/public/public_tx_simulator/measured_public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/measured_public_tx_simulator.ts new file mode 100644 index 000000000000..21a58a0f7df4 --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator/measured_public_tx_simulator.ts @@ -0,0 +1,111 @@ +import type { Fr } from '@aztec/foundation/fields'; +import { Timer } from '@aztec/foundation/timer'; +import type { Gas } from '@aztec/stdlib/gas'; +import type { AvmSimulationStats } from '@aztec/stdlib/stats'; +import { type GlobalVariables, PublicCallRequestWithCalldata, Tx, TxExecutionPhase } from '@aztec/stdlib/tx'; + +import type { AvmFinalizedCallResult } from '../avm/avm_contract_call_result.js'; +import type { AvmPersistableStateManager } from '../avm/index.js'; +import type { ExecutorMetricsInterface } from '../executor_metrics_interface.js'; +import type { PublicContractsDB, PublicTreesDB } from '../public_db_sources.js'; +import { PublicTxContext } from './public_tx_context.js'; +import { type ProcessedPhase, type PublicTxResult, PublicTxSimulator } from './public_tx_simulator.js'; + +/** + * A public tx simulator that tracks miscellaneous simulation metrics without telemetry. + */ +export class MeasuredPublicTxSimulator extends PublicTxSimulator { + constructor( + treesDB: PublicTreesDB, + contractsDB: PublicContractsDB, + globalVariables: GlobalVariables, + doMerkleOperations: boolean = false, + skipFeeEnforcement: boolean = false, + protected readonly metrics: ExecutorMetricsInterface, + ) { + super(treesDB, contractsDB, globalVariables, doMerkleOperations, skipFeeEnforcement); + } + + public override async simulate(tx: Tx, txLabel: string = 'unlabeledTx'): Promise { + this.metrics.startRecordingTxSimulation(txLabel); + let avmResult: PublicTxResult | undefined; + try { + avmResult = await super.simulate(tx); + } finally { + this.metrics.stopRecordingTxSimulation(txLabel, avmResult?.revertCode); + } + return avmResult; + } + + protected override async computeTxHash(tx: Tx) { + const timer = new Timer(); + const txHash = await super.computeTxHash(tx); + this.metrics.recordTxHashComputation(timer.ms()); + return txHash; + } + + protected override async insertNonRevertiblesFromPrivate(context: PublicTxContext, tx: Tx) { + const timer = new Timer(); + await super.insertNonRevertiblesFromPrivate(context, tx); + this.metrics.recordPrivateEffectsInsertion(timer.us(), 'non-revertible'); + } + + protected override async insertRevertiblesFromPrivate(context: PublicTxContext, tx: Tx): Promise { + const timer = new Timer(); + const result = await super.insertRevertiblesFromPrivate(context, tx); + this.metrics.recordPrivateEffectsInsertion(timer.us(), 'revertible'); + return result; + } + + protected override async simulatePhase(phase: TxExecutionPhase, context: PublicTxContext): Promise { + const timer = new Timer(); + const result = await super.simulatePhase(phase, context); + result.durationMs = timer.ms(); + return result; + } + + protected override async simulateEnqueuedCallInternal( + stateManager: AvmPersistableStateManager, + callRequest: PublicCallRequestWithCalldata, + allocatedGas: Gas, + transactionFee: Fr, + fnName: string, + ): Promise { + const timer = new Timer(); + const result = await super.simulateEnqueuedCallInternal( + stateManager, + callRequest, + allocatedGas, + transactionFee, + fnName, + ); + + this.log.verbose( + result.reverted + ? `Simulation of enqueued public call ${fnName} reverted with reason ${result.revertReason}.` + : `Simulation of enqueued public call ${fnName} completed successfully.`, + { + eventName: 'avm-simulation', + appCircuitName: fnName, + duration: timer.ms(), + } satisfies AvmSimulationStats, + ); + + if (result.reverted) { + this.metrics.recordEnqueuedCallSimulationFailure( + fnName, + timer.ms(), + allocatedGas.sub(result.gasLeft).l2Gas, + result.totalInstructions, + ); + } else { + this.metrics.recordEnqueuedCallSimulation( + fnName, + timer.ms(), + allocatedGas.sub(result.gasLeft).l2Gas, + result.totalInstructions, + ); + } + return result; + } +} diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts index 8d05a6457f7d..b31ba4895a68 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts @@ -1,6 +1,5 @@ import type { Fr } from '@aztec/foundation/fields'; import { type Logger, createLogger } from '@aztec/foundation/log'; -import { Timer } from '@aztec/foundation/timer'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { computeFeePayerBalanceStorageSlot } from '@aztec/protocol-contracts/fee-juice'; import { @@ -14,7 +13,6 @@ import { import { SimulationError } from '@aztec/stdlib/errors'; import type { Gas, GasUsed } from '@aztec/stdlib/gas'; import { ProvingRequestType } from '@aztec/stdlib/proofs'; -import type { AvmSimulationStats } from '@aztec/stdlib/stats'; import { type GlobalVariables, NestedProcessReturnValues, @@ -22,7 +20,6 @@ import { Tx, TxExecutionPhase, } from '@aztec/stdlib/tx'; -import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client'; import { strict as assert } from 'assert'; @@ -30,13 +27,12 @@ import { getPublicFunctionDebugName } from '../../common/debug_fn_name.js'; import type { AvmFinalizedCallResult } from '../avm/avm_contract_call_result.js'; import { type AvmPersistableStateManager, AvmSimulator } from '../avm/index.js'; import { NullifierCollisionError } from '../avm/journal/nullifiers.js'; -import { ExecutorMetrics } from '../executor_metrics.js'; import type { PublicContractsDB, PublicTreesDB } from '../public_db_sources.js'; import { PublicTxContext } from './public_tx_context.js'; export type ProcessedPhase = { phase: TxExecutionPhase; - durationMs: number; + durationMs?: number; returnValues: NestedProcessReturnValues[]; reverted: boolean; revertReason?: SimulationError; @@ -53,24 +49,16 @@ export type PublicTxResult = { }; export class PublicTxSimulator { - metrics: ExecutorMetrics; - - private log: Logger; + protected log: Logger; constructor( private treesDB: PublicTreesDB, - private contractsDB: PublicContractsDB, + protected contractsDB: PublicContractsDB, private globalVariables: GlobalVariables, private doMerkleOperations: boolean = false, private skipFeeEnforcement: boolean = false, - telemetryClient: TelemetryClient = getTelemetryClient(), ) { this.log = createLogger(`simulator:public_tx_simulator`); - this.metrics = new ExecutorMetrics(telemetryClient, 'PublicTxSimulator'); - } - - get tracer(): Tracer { - return this.metrics.tracer; } /** @@ -80,11 +68,9 @@ export class PublicTxSimulator { */ public async simulate(tx: Tx): Promise { try { - const startTime = process.hrtime.bigint(); + const txHash = await this.computeTxHash(tx); - const txHash = await tx.getTxHash(); this.log.debug(`Simulating ${tx.publicFunctionCalldata.length} public calls for tx ${txHash}`, { txHash }); - const context = await PublicTxContext.create( this.treesDB, this.contractsDB, @@ -93,15 +79,7 @@ export class PublicTxSimulator { this.doMerkleOperations, ); - const nonRevertStart = process.hrtime.bigint(); - await this.insertNonRevertiblesFromPrivate(context); - // add new contracts to the contracts db so that their functions may be found and called - // TODO(#6464): Should we allow emitting contracts in the private setup phase? - // FIXME(fcarreiro): this should conceptually use the hinted contracts db. - // However things should work as they are now because the hinted db would still pick up the new contracts. - await this.contractsDB.addNewNonRevertibleContracts(tx); - const nonRevertEnd = process.hrtime.bigint(); - this.metrics.recordPrivateEffectsInsertion(Number(nonRevertEnd - nonRevertStart) / 1_000, 'non-revertible'); + await this.insertNonRevertiblesFromPrivate(context, tx); const processedPhases: ProcessedPhase[] = []; if (context.hasPhase(TxExecutionPhase.SETUP)) { @@ -109,16 +87,8 @@ export class PublicTxSimulator { processedPhases.push(setupResult); } - const revertStart = process.hrtime.bigint(); - const success = await this.insertRevertiblesFromPrivate(context); + const success = await this.insertRevertiblesFromPrivate(context, tx); if (success) { - // add new contracts to the contracts db so that their functions may be found and called - // FIXME(fcarreiro): this should conceptually use the hinted contracts db. - // However things should work as they are now because the hinted db would still pick up the new contracts. - await this.contractsDB.addNewRevertibleContracts(tx); - const revertEnd = process.hrtime.bigint(); - this.metrics.recordPrivateEffectsInsertion(Number(revertEnd - revertStart) / 1_000, 'revertible'); - // Only proceed with app logic if there was no revert during revertible insertion if (context.hasPhase(TxExecutionPhase.APP_LOGIC)) { const appLogicResult: ProcessedPhase = await this.simulateAppLogicPhase(context); @@ -151,9 +121,6 @@ export class PublicTxSimulator { // However things should work as they are now because the hinted db would still pick up the new contracts. this.contractsDB.commitContractsForTx(/*onlyNonRevertibles=*/ !revertCode.isOK()); - const endTime = process.hrtime.bigint(); - this.log.debug(`Public TX simulator took ${Number(endTime - startTime) / 1_000_000} ms\n`); - return { avmProvingRequest, gasUsed: { @@ -175,6 +142,10 @@ export class PublicTxSimulator { } } + protected async computeTxHash(tx: Tx) { + return await tx.getTxHash(); + } + /** * Simulate the setup phase of a transaction's public execution. * @param context - WILL BE MUTATED. The context of the currently executing public transaction portion @@ -238,7 +209,7 @@ export class PublicTxSimulator { * @param context - WILL BE MUTATED. The context of the currently executing public transaction portion * @returns The phase result. */ - private async simulatePhase(phase: TxExecutionPhase, context: PublicTxContext): Promise { + protected async simulatePhase(phase: TxExecutionPhase, context: PublicTxContext): Promise { const callRequests = context.getCallRequestsForPhase(phase); this.log.debug(`Processing phase ${TxExecutionPhase[phase]} for tx ${context.txHash}`, { @@ -250,7 +221,6 @@ export class PublicTxSimulator { const returnValues: NestedProcessReturnValues[] = []; let reverted = false; let revertReason: SimulationError | undefined; - const phaseTimer = new Timer(); for (let i = callRequests.length - 1; i >= 0; i--) { if (reverted) { break; @@ -270,7 +240,6 @@ export class PublicTxSimulator { return { phase, - durationMs: phaseTimer.ms(), returnValues, reverted, revertReason, @@ -284,13 +253,7 @@ export class PublicTxSimulator { * @param callRequest - The public function call request, including the calldata. * @returns The result of execution. */ - @trackSpan('PublicTxSimulator.simulateEnqueuedCall', (phase, context, callRequest) => ({ - [Attributes.TX_HASH]: context.txHash.toString(), - [Attributes.TARGET_ADDRESS]: callRequest.request.contractAddress.toString(), - [Attributes.SENDER_ADDRESS]: callRequest.request.msgSender.toString(), - [Attributes.SIMULATOR_PHASE]: TxExecutionPhase[phase].toString(), - })) - private async simulateEnqueuedCall( + protected async simulateEnqueuedCall( phase: TxExecutionPhase, context: PublicTxContext, callRequest: PublicCallRequestWithCalldata, @@ -344,19 +307,12 @@ export class PublicTxSimulator { * while still simulating phases and generating a proving request. * * @param stateManager - The state manager for AvmSimulation - * @param context - The context of the currently executing public transaction portion * @param callRequest - The public function call request, including the calldata. * @param allocatedGas - The gas allocated to the enqueued call * @param fnName - The name of the function * @returns The result of execution. */ - @trackSpan( - 'PublicTxSimulator.simulateEnqueuedCallInternal', - (_stateManager, _callRequest, _allocatedGas, _transactionFee, fnName) => ({ - [Attributes.APP_CIRCUIT_NAME]: fnName, - }), - ) - private async simulateEnqueuedCallInternal( + protected async simulateEnqueuedCallInternal( stateManager: AvmPersistableStateManager, { request, calldata }: PublicCallRequestWithCalldata, allocatedGas: Gas, @@ -369,7 +325,6 @@ export class PublicTxSimulator { this.log.debug( `Executing enqueued public call to external function ${fnName}@${address} with ${allocatedGas.l2Gas} allocated L2 gas.`, ); - const timer = new Timer(); const simulator = await AvmSimulator.create( stateManager, @@ -382,32 +337,13 @@ export class PublicTxSimulator { allocatedGas, ); const avmCallResult = await simulator.execute(); - const result = avmCallResult.finalize(); - - this.log.verbose( - result.reverted - ? `Simulation of enqueued public call ${fnName} reverted with reason ${result.revertReason}.` - : `Simulation of enqueued public call ${fnName} completed successfully.`, - { - eventName: 'avm-simulation', - appCircuitName: fnName, - duration: timer.ms(), - } satisfies AvmSimulationStats, - ); - - if (result.reverted) { - this.metrics.recordFunctionSimulationFailure(); - } else { - this.metrics.recordFunctionSimulation(timer.ms(), allocatedGas.sub(result.gasLeft).l2Gas, fnName); - } - - return result; + return avmCallResult.finalize(); } /** * Insert the non-revertible accumulated data from private into the public state. */ - public async insertNonRevertiblesFromPrivate(context: PublicTxContext) { + protected async insertNonRevertiblesFromPrivate(context: PublicTxContext, tx: Tx) { const stateManager = context.state.getActiveStateManager(); try { await stateManager.writeSiloedNullifiersFromPrivate(context.nonRevertibleAccumulatedDataFromPrivate.nullifiers); @@ -423,13 +359,18 @@ export class PublicTxSimulator { await stateManager.writeUniqueNoteHash(noteHash); } } + // add new contracts to the contracts db so that their functions may be found and called + // TODO(#6464): Should we allow emitting contracts in the private setup phase? + // FIXME(fcarreiro): this should conceptually use the hinted contracts db. + // However things should work as they are now because the hinted db would still pick up the new contracts. + await this.contractsDB.addNewNonRevertibleContracts(tx); } /** * Insert the revertible accumulated data from private into the public state. * Start by forking state so we can rollback to the end of setup if app logic or teardown reverts. */ - public async insertRevertiblesFromPrivate(context: PublicTxContext): /*success=*/ Promise { + protected async insertRevertiblesFromPrivate(context: PublicTxContext, tx: Tx): /*success=*/ Promise { // Fork the state manager so we can rollback to end of setup if app logic reverts. await context.state.fork(); const stateManager = context.state.getActiveStateManager(); @@ -457,6 +398,11 @@ export class PublicTxSimulator { await stateManager.writeSiloedNoteHash(noteHash); } } + // add new contracts to the contracts db so that their functions may be found and called + // FIXME(fcarreiro): this should conceptually use the hinted contracts db. + // However things should work as they are now because the hinted db would still pick up the new contracts. + await this.contractsDB.addNewRevertibleContracts(tx); + return /*success=*/ true; } diff --git a/yarn-project/simulator/src/public/public_tx_simulator/telemetry_public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/telemetry_public_tx_simulator.ts new file mode 100644 index 000000000000..aff10196e728 --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator/telemetry_public_tx_simulator.ts @@ -0,0 +1,62 @@ +import type { Fr } from '@aztec/foundation/fields'; +import type { Gas } from '@aztec/stdlib/gas'; +import { type GlobalVariables, PublicCallRequestWithCalldata, TxExecutionPhase } from '@aztec/stdlib/tx'; +import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client'; + +import type { AvmFinalizedCallResult } from '../avm/avm_contract_call_result.js'; +import type { AvmPersistableStateManager } from '../avm/index.js'; +import { ExecutorMetrics } from '../executor_metrics.js'; +import type { PublicContractsDB, PublicTreesDB } from '../public_db_sources.js'; +import { MeasuredPublicTxSimulator } from './measured_public_tx_simulator.js'; +import { PublicTxContext } from './public_tx_context.js'; + +/** + * A public tx simulator that tracks runtime/production metrics with telemetry. + */ +export class TelemetryPublicTxSimulator extends MeasuredPublicTxSimulator { + /* tracer needed by trackSpans */ + public readonly tracer: Tracer; + + constructor( + treesDB: PublicTreesDB, + contractsDB: PublicContractsDB, + globalVariables: GlobalVariables, + doMerkleOperations: boolean = false, + skipFeeEnforcement: boolean = false, + telemetryClient: TelemetryClient = getTelemetryClient(), + ) { + const metrics = new ExecutorMetrics(telemetryClient, 'PublicTxSimulator'); + super(treesDB, contractsDB, globalVariables, doMerkleOperations, skipFeeEnforcement, metrics); + this.tracer = metrics.tracer; + } + + @trackSpan('PublicTxSimulator.simulateEnqueuedCall', (phase, context, callRequest) => ({ + [Attributes.TX_HASH]: context.txHash.toString(), + [Attributes.TARGET_ADDRESS]: callRequest.request.contractAddress.toString(), + [Attributes.SENDER_ADDRESS]: callRequest.request.msgSender.toString(), + [Attributes.SIMULATOR_PHASE]: TxExecutionPhase[phase].toString(), + })) + protected override async simulateEnqueuedCall( + phase: TxExecutionPhase, + context: PublicTxContext, + callRequest: PublicCallRequestWithCalldata, + ): Promise { + return await super.simulateEnqueuedCall(phase, context, callRequest); + } + + @trackSpan( + 'PublicTxSimulator.simulateEnqueuedCallInternal', + (_stateManager, _callRequest, _allocatedGas, _transactionFee, fnName) => ({ + [Attributes.APP_CIRCUIT_NAME]: fnName, + }), + ) + protected override async simulateEnqueuedCallInternal( + stateManager: AvmPersistableStateManager, + callRequest: PublicCallRequestWithCalldata, + allocatedGas: Gas, + transactionFee: Fr, + fnName: string, + ): Promise { + return await super.simulateEnqueuedCallInternal(stateManager, callRequest, allocatedGas, transactionFee, fnName); + } +} diff --git a/yarn-project/simulator/src/public/test_executor_metrics.ts b/yarn-project/simulator/src/public/test_executor_metrics.ts new file mode 100644 index 000000000000..3e2cf9c776be --- /dev/null +++ b/yarn-project/simulator/src/public/test_executor_metrics.ts @@ -0,0 +1,222 @@ +import { sum } from '@aztec/foundation/collection'; +import { type Logger, createLogger } from '@aztec/foundation/log'; +import { Timer } from '@aztec/foundation/timer'; +import type { RevertCode } from '@aztec/stdlib/avm'; + +import { strict as assert } from 'assert'; + +import type { ExecutorMetricsInterface } from './executor_metrics_interface.js'; + +export interface PublicEnqueuedCallMetrics { + fnName: string; + durationMs: number; + manaUsed: number; + totalInstructions: number; + reverted: boolean; +} + +export interface PublicTxMetrics { + totalDurationMs: number; + manaUsed: number; + totalInstructions: number; + txHashMs: number | undefined; + nonRevertiblePrivateInsertionsUs: number | undefined; + revertiblePrivateInsertionsUs: number | undefined; + enqueuedCalls: PublicEnqueuedCallMetrics[]; + revertedCode: RevertCode | undefined; +} + +const NUM_SPACES = 4; +const H1 = '# '; +const H2 = '\n## '; +const INDENT = ' '.repeat(NUM_SPACES); +const INDENT0 = '- '; +const INDENT1 = INDENT + '- '; +const INDENT2 = INDENT + INDENT + '- '; +const H_LINE = '\n---------------------------------------------------------------------'; + +export enum PublicTxMetricsFilter { + ALL, + TOTALS, + DURATIONS, + INSTRUCTIONS, +} + +export class TestExecutorMetrics implements ExecutorMetricsInterface { + private logger: Logger; + // tx label -> tx metrics + private txMetrics: Map = new Map(); + private currentTxLabel: string | undefined; + private txTimer: Timer | undefined; + + constructor() { + this.logger = createLogger(`simulator:test_executor_metrics`); + } + + startRecordingTxSimulation(txLabel: string) { + assert(!this.currentTxLabel, 'Cannot start recording tx simulation when another is live'); + assert(!this.txMetrics.has(txLabel), 'Cannot start recording metrics for tx with duplicate label'); + this.txMetrics.set(txLabel, { + totalDurationMs: 0, + manaUsed: 0, + totalInstructions: 0, + txHashMs: undefined, + nonRevertiblePrivateInsertionsUs: undefined, + revertiblePrivateInsertionsUs: undefined, + enqueuedCalls: [], + revertedCode: undefined, + }); + this.currentTxLabel = txLabel; + this.txTimer = new Timer(); + } + + stopRecordingTxSimulation(txLabel: string, revertedCode?: RevertCode) { + assert(this.currentTxLabel === txLabel, 'Cannot stop recording metrics for tx when another is live'); + + const txMetrics = this.txMetrics.get(txLabel)!; + + // total duration of tx + txMetrics.totalDurationMs = this.txTimer!.ms(); + this.logger.debug(`Public TX simulation of ${txLabel} took ${txMetrics.totalDurationMs}ms`); + + // add manaUsed across all enqueued calls + txMetrics.manaUsed = sum(txMetrics.enqueuedCalls.map(call => call.manaUsed)); + // add totalInstructions across all enqueued calls + txMetrics.totalInstructions = sum(txMetrics.enqueuedCalls.map(call => call.totalInstructions)); + txMetrics.revertedCode = revertedCode; + + this.currentTxLabel = undefined; + } + + recordEnqueuedCallSimulation(fnName: string, durationMs: number, manaUsed: number, totalInstructions: number) { + this.#recordEnqueuedCallSimulation(fnName, durationMs, manaUsed, totalInstructions, false); + } + + recordEnqueuedCallSimulationFailure(fnName: string, durationMs: number, manaUsed: number, totalInstructions: number) { + this.#recordEnqueuedCallSimulation(fnName, durationMs, manaUsed, totalInstructions, true); + } + + #recordEnqueuedCallSimulation( + fnName: string, + durationMs: number, + manaUsed: number, + totalInstructions: number, + reverted: boolean, + ) { + assert(this.currentTxLabel, 'Cannot record enqueued call simulation when no tx is live'); + const txMetrics = this.txMetrics.get(this.currentTxLabel)!; + txMetrics.enqueuedCalls.push({ + fnName, + durationMs, + manaUsed, + totalInstructions, + reverted, + }); + } + + recordTxHashComputation(durationMs: number) { + assert(this.currentTxLabel, 'Cannot record tx hash computation time when no tx is live'); + const txMetrics = this.txMetrics.get(this.currentTxLabel)!; + assert(txMetrics.txHashMs === undefined, 'Cannot RE-record tx hash computation time'); + txMetrics.txHashMs = durationMs; + } + + recordPrivateEffectsInsertion(durationUs: number, type: 'revertible' | 'non-revertible') { + assert(this.currentTxLabel, 'Cannot record private effects insertion when no tx is live'); + const txMetrics = this.txMetrics.get(this.currentTxLabel)!; + if (type === 'revertible') { + assert( + txMetrics.revertiblePrivateInsertionsUs === undefined, + 'Cannot RE-record revertible insertions of private effects', + ); + txMetrics.revertiblePrivateInsertionsUs = durationUs; + } else { + assert( + txMetrics.nonRevertiblePrivateInsertionsUs === undefined, + 'Cannot RE-record non-revertible insertions of private effects', + ); + txMetrics.nonRevertiblePrivateInsertionsUs = durationUs; + } + } + + prettyPrint(filter: PublicTxMetricsFilter = PublicTxMetricsFilter.ALL) { + this.logger.info(this.toPrettyString(filter)); + } + + toPrettyString(filter: PublicTxMetricsFilter = PublicTxMetricsFilter.ALL) { + let pretty = ''; + //pretty += H_LINE + '\n'; + pretty += `${H1}Public TX Simulation Metrics (${PublicTxMetricsFilter[filter]})\n`; + for (const [txLabel, txMetrics] of this.txMetrics.entries()) { + //pretty += H_LINE + '\n'; + pretty += `${H2}TX Label: ${txLabel}\n`; + if ( + filter == PublicTxMetricsFilter.DURATIONS || + filter === PublicTxMetricsFilter.TOTALS || + filter === PublicTxMetricsFilter.ALL + ) { + pretty += `${INDENT0}Total duration: ${fmtNum(txMetrics.totalDurationMs, 'ms')}\n`; + } + if (filter === PublicTxMetricsFilter.TOTALS || filter === PublicTxMetricsFilter.ALL) { + pretty += `${INDENT0}Total mana used: ${fmtNum(txMetrics.manaUsed)}\n`; + const manaPerSecond = Math.round((txMetrics.manaUsed * 1000) / txMetrics.totalDurationMs); + pretty += `${INDENT0}Mana per second: ${fmtNum(manaPerSecond)}\n`; + } + + if ( + filter === PublicTxMetricsFilter.INSTRUCTIONS || + filter === PublicTxMetricsFilter.TOTALS || + filter === PublicTxMetricsFilter.ALL + ) { + pretty += `${INDENT0}Total instructions executed: ${fmtNum(txMetrics.totalInstructions)}\n`; + } + if (filter === PublicTxMetricsFilter.DURATIONS || filter === PublicTxMetricsFilter.ALL) { + pretty += `${INDENT0}Tx hash computation: ${fmtNum(txMetrics.txHashMs!, 'ms')}\n`; + pretty += `${INDENT0}Private insertions:\n`; + pretty += `${INDENT1}Non-revertible: ${fmtNum(txMetrics.nonRevertiblePrivateInsertionsUs! / 1_000, 'ms')}\n`; + pretty += `${INDENT1}Revertible: ${fmtNum(txMetrics.revertiblePrivateInsertionsUs! / 1_000, 'ms')}\n`; + } + if (filter !== PublicTxMetricsFilter.TOTALS) { + // totals exclude enqueued calls + pretty += this.#enqueuedCallsToPrettyString(txMetrics, filter); + } + if (txMetrics.revertedCode !== undefined && !txMetrics.revertedCode.isOK()) { + pretty += `${INDENT0}Reverted code: ${txMetrics.revertedCode?.getDescription()}\n`; + } + pretty += H_LINE + '\n'; + } + return pretty; + } + + #enqueuedCallsToPrettyString(txMetrics: PublicTxMetrics, filter: PublicTxMetricsFilter) { + let pretty = ''; + pretty += `${INDENT0}Enqueued public calls:\n`; + for (const enqueuedCall of txMetrics.enqueuedCalls) { + pretty += `${INDENT1}**Fn: ${enqueuedCall.fnName}**\n`; + if (filter === PublicTxMetricsFilter.DURATIONS || filter === PublicTxMetricsFilter.ALL) { + pretty += `${INDENT2}Duration: ${fmtNum(enqueuedCall.durationMs, 'ms')}\n`; + } + if (filter === PublicTxMetricsFilter.ALL) { + pretty += `${INDENT2}Mana used: ${fmtNum(enqueuedCall.manaUsed)}\n`; + const manaPerSecond = Math.round((enqueuedCall.manaUsed * 1000) / enqueuedCall.durationMs); + pretty += `${INDENT2}Mana per second: ${fmtNum(manaPerSecond)}\n`; + } + + if (filter === PublicTxMetricsFilter.INSTRUCTIONS || filter === PublicTxMetricsFilter.ALL) { + pretty += `${INDENT2}Instructions executed: ${fmtNum(enqueuedCall.totalInstructions)}\n`; + } + if (enqueuedCall.reverted) { + pretty += `${INDENT2}Reverted!\n`; + } + } + return pretty; + } + + toJSON(indent = 2) { + return JSON.stringify(Object.fromEntries(this.txMetrics.entries()), null, indent); + } +} + +function fmtNum(num: number, unit?: string) { + return `\`${num.toLocaleString()}${unit ? ` ${unit}` : ''}\``; +} diff --git a/yarn-project/telemetry-client/src/attributes.ts b/yarn-project/telemetry-client/src/attributes.ts index 381bd53fb16e..7434a1452957 100644 --- a/yarn-project/telemetry-client/src/attributes.ts +++ b/yarn-project/telemetry-client/src/attributes.ts @@ -96,6 +96,7 @@ export const SIMULATOR_PHASE = 'aztec.simulator.phase'; export const TARGET_ADDRESS = 'aztec.address.target'; export const SENDER_ADDRESS = 'aztec.address.sender'; export const MANA_USED = 'aztec.mana.used'; +export const TOTAL_INSTRUCTIONS = 'aztec.total_instructions'; /** Whether a sync process is the initial run, which is usually slower than iterative ones. */ export const INITIAL_SYNC = 'aztec.initial_sync'; diff --git a/yarn-project/telemetry-client/src/metrics.ts b/yarn-project/telemetry-client/src/metrics.ts index ac89da2e7b4d..9d370bb5a750 100644 --- a/yarn-project/telemetry-client/src/metrics.ts +++ b/yarn-project/telemetry-client/src/metrics.ts @@ -98,11 +98,15 @@ export const PUBLIC_PROCESSOR_TOTAL_GAS_HISTOGRAM = 'aztec.public_processor.tota export const PUBLIC_PROCESSOR_GAS_RATE = 'aztec.public_processor.gas_rate'; export const PUBLIC_PROCESSOR_TREE_INSERTION = 'aztec.public_processor.tree_insertion'; +export const PUBLIC_EXECUTOR_PREFIX = 'aztec.public_executor.'; export const PUBLIC_EXECUTOR_SIMULATION_COUNT = 'aztec.public_executor.simulation_count'; export const PUBLIC_EXECUTOR_SIMULATION_DURATION = 'aztec.public_executor.simulation_duration'; export const PUBLIC_EXECUTOR_SIMULATION_MANA_PER_SECOND = 'aztec.public_executor.simulation_mana_per_second'; -export const PUBLIC_EXECUTION_SIMULATION_BYTECODE_SIZE = 'aztec.public_executor.simulation_bytecode_size'; -export const PUBLIC_EXECUTION_PRIVATE_EFFECTS_INSERTION = 'aztec.public_executor.private_effects_insertion'; +export const PUBLIC_EXECUTOR_SIMULATION_MANA_USED = 'aztec.public_executor.simulation_mana_used'; +export const PUBLIC_EXECUTOR_SIMULATION_TOTAL_INSTRUCTIONS = 'aztec.public_executor.simulation_total_instructions'; +export const PUBLIC_EXECUTOR_TX_HASHING = 'aztec.public_executor.tx_hashing'; +export const PUBLIC_EXECUTOR_PRIVATE_EFFECTS_INSERTION = 'aztec.public_executor.private_effects_insertion'; +export const PUBLIC_EXECUTOR_SIMULATION_BYTECODE_SIZE = 'aztec.public_executor.simulation_bytecode_size'; export const PROVING_ORCHESTRATOR_BASE_ROLLUP_INPUTS_DURATION = 'aztec.proving_orchestrator.base_rollup.inputs_duration';