@aztec/simulator 3.0.0-rc.5 → 4.0.0-nightly.20260107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dest/private/circuit_recording/circuit_recorder.d.ts +1 -1
  2. package/dest/private/circuit_recording/circuit_recorder.d.ts.map +1 -1
  3. package/dest/private/circuit_recording/circuit_recorder.js +15 -14
  4. package/dest/public/avm/opcodes/accrued_substate.js +1 -1
  5. package/dest/public/avm/opcodes/external_calls.d.ts +1 -1
  6. package/dest/public/avm/opcodes/external_calls.d.ts.map +1 -1
  7. package/dest/public/avm/opcodes/external_calls.js +1 -0
  8. package/dest/public/avm/opcodes/hashing.d.ts +1 -1
  9. package/dest/public/avm/opcodes/hashing.d.ts.map +1 -1
  10. package/dest/public/avm/opcodes/hashing.js +6 -3
  11. package/dest/public/debug_fn_name.d.ts +1 -1
  12. package/dest/public/debug_fn_name.d.ts.map +1 -1
  13. package/dest/public/debug_fn_name.js +10 -3
  14. package/dest/public/fixtures/bulk_test.js +3 -51
  15. package/dest/public/fixtures/custom_bytecode_tester.d.ts +28 -6
  16. package/dest/public/fixtures/custom_bytecode_tester.d.ts.map +1 -1
  17. package/dest/public/fixtures/custom_bytecode_tester.js +36 -12
  18. package/dest/public/fixtures/custom_bytecode_tests.d.ts +3 -1
  19. package/dest/public/fixtures/custom_bytecode_tests.d.ts.map +1 -1
  20. package/dest/public/fixtures/custom_bytecode_tests.js +54 -10
  21. package/dest/public/fixtures/index.d.ts +4 -2
  22. package/dest/public/fixtures/index.d.ts.map +1 -1
  23. package/dest/public/fixtures/index.js +3 -1
  24. package/dest/public/fixtures/minimal_public_tx.d.ts +2 -7
  25. package/dest/public/fixtures/minimal_public_tx.d.ts.map +1 -1
  26. package/dest/public/fixtures/minimal_public_tx.js +2 -12
  27. package/dest/public/fixtures/opcode_spammer.d.ts +123 -0
  28. package/dest/public/fixtures/opcode_spammer.d.ts.map +1 -0
  29. package/dest/public/fixtures/opcode_spammer.js +1681 -0
  30. package/dest/public/fixtures/public_tx_simulation_tester.d.ts +15 -2
  31. package/dest/public/fixtures/public_tx_simulation_tester.d.ts.map +1 -1
  32. package/dest/public/fixtures/public_tx_simulation_tester.js +34 -7
  33. package/dest/public/fixtures/utils.d.ts +1 -1
  34. package/dest/public/fixtures/utils.d.ts.map +1 -1
  35. package/dest/public/fixtures/utils.js +3 -2
  36. package/dest/public/fuzzing/avm_fuzzer_simulator.d.ts +1 -1
  37. package/dest/public/fuzzing/avm_fuzzer_simulator.d.ts.map +1 -1
  38. package/dest/public/fuzzing/avm_fuzzer_simulator.js +4 -1
  39. package/dest/public/fuzzing/avm_simulator_bin.js +18 -2
  40. package/dest/public/hinting_db_sources.d.ts +2 -1
  41. package/dest/public/hinting_db_sources.d.ts.map +1 -1
  42. package/dest/public/hinting_db_sources.js +5 -0
  43. package/dest/public/public_processor/guarded_merkle_tree.d.ts +2 -1
  44. package/dest/public/public_processor/guarded_merkle_tree.d.ts.map +1 -1
  45. package/dest/public/public_processor/guarded_merkle_tree.js +5 -0
  46. package/dest/public/public_processor/public_processor.d.ts +2 -2
  47. package/dest/public/public_processor/public_processor.d.ts.map +1 -1
  48. package/dest/public/public_processor/public_processor.js +417 -28
  49. package/dest/public/public_tx_simulator/contract_provider_for_cpp.d.ts +1 -1
  50. package/dest/public/public_tx_simulator/contract_provider_for_cpp.d.ts.map +1 -1
  51. package/dest/public/public_tx_simulator/contract_provider_for_cpp.js +15 -11
  52. package/dest/public/public_tx_simulator/cpp_public_tx_simulator.d.ts +16 -1
  53. package/dest/public/public_tx_simulator/cpp_public_tx_simulator.d.ts.map +1 -1
  54. package/dest/public/public_tx_simulator/cpp_public_tx_simulator.js +41 -3
  55. package/dest/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.d.ts +1 -1
  56. package/dest/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.d.ts.map +1 -1
  57. package/dest/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.js +3 -2
  58. package/dest/public/public_tx_simulator/public_tx_simulator_interface.d.ts +24 -1
  59. package/dest/public/public_tx_simulator/public_tx_simulator_interface.d.ts.map +1 -1
  60. package/dest/public/public_tx_simulator/telemetry_public_tx_simulator.js +395 -19
  61. package/package.json +16 -16
  62. package/src/private/circuit_recording/circuit_recorder.ts +16 -15
  63. package/src/public/avm/opcodes/accrued_substate.ts +1 -1
  64. package/src/public/avm/opcodes/external_calls.ts +1 -0
  65. package/src/public/avm/opcodes/hashing.ts +7 -3
  66. package/src/public/debug_fn_name.ts +10 -3
  67. package/src/public/fixtures/bulk_test.ts +6 -6
  68. package/src/public/fixtures/custom_bytecode_tester.ts +53 -19
  69. package/src/public/fixtures/custom_bytecode_tests.ts +70 -10
  70. package/src/public/fixtures/index.ts +7 -1
  71. package/src/public/fixtures/minimal_public_tx.ts +3 -12
  72. package/src/public/fixtures/opcode_spammer.ts +1638 -0
  73. package/src/public/fixtures/public_tx_simulation_tester.ts +38 -5
  74. package/src/public/fixtures/utils.ts +1 -2
  75. package/src/public/fuzzing/avm_fuzzer_simulator.ts +8 -1
  76. package/src/public/fuzzing/avm_simulator_bin.ts +21 -2
  77. package/src/public/hinting_db_sources.ts +4 -0
  78. package/src/public/public_processor/guarded_merkle_tree.ts +4 -0
  79. package/src/public/public_processor/public_processor.ts +20 -9
  80. package/src/public/public_tx_simulator/contract_provider_for_cpp.ts +16 -11
  81. package/src/public/public_tx_simulator/cpp_public_tx_simulator.ts +48 -3
  82. package/src/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.ts +3 -2
  83. package/src/public/public_tx_simulator/public_tx_simulator_interface.ts +23 -0
@@ -31,7 +31,7 @@ const DEFAULT_GAS_FEES = new GasFees(2, 3);
31
31
  export type TestEnqueuedCall = {
32
32
  sender?: AztecAddress;
33
33
  address: AztecAddress;
34
- fnName: string;
34
+ fnName?: string;
35
35
  args: any[];
36
36
  isStaticCall?: boolean;
37
37
  contractArtifact?: ContractArtifact;
@@ -225,6 +225,25 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester {
225
225
  this.metrics.prettyPrint();
226
226
  }
227
227
 
228
+ /**
229
+ * Cancel the current simulation if one is in progress.
230
+ * This signals the underlying simulator (e.g., C++) to stop at the next safe point.
231
+ * Safe to call even if no simulation is in progress.
232
+ *
233
+ * @param waitTimeoutMs - If provided, wait up to this many ms for the simulation to actually stop.
234
+ */
235
+ public async cancel(waitTimeoutMs?: number): Promise<void> {
236
+ await this.simulator.cancel?.(waitTimeoutMs);
237
+ }
238
+
239
+ /**
240
+ * Get the underlying simulator for advanced test scenarios.
241
+ * Use this when you need direct control over simulation (e.g., for testing cancellation).
242
+ */
243
+ public getSimulator(): MeasuredPublicTxSimulatorInterface {
244
+ return this.simulator;
245
+ }
246
+
228
247
  async #createPubicCallRequestForCall(
229
248
  call: TestEnqueuedCall,
230
249
  sender: AztecAddress,
@@ -235,10 +254,24 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester {
235
254
  throw new Error(`Contract artifact not found for address: ${address}`);
236
255
  }
237
256
 
238
- const fnSelector = await getFunctionSelector(call.fnName, contractArtifact);
239
- const fnAbi = getContractFunctionAbi(call.fnName, contractArtifact)!;
240
- const encodedArgs = encodeArguments(fnAbi, call.args);
241
- const calldata = [fnSelector.toField(), ...encodedArgs];
257
+ let calldata: Fr[] = [];
258
+ if (!call.fnName) {
259
+ this.logger.debug(
260
+ `No function name specified for call to contract ${call.address.toString()}. Assuming this is a custom bytecode with no public_dispatch function.`,
261
+ );
262
+ this.logger.debug(`Not using ABI to encode arguments. Not prepending fn selector to calldata.`);
263
+ try {
264
+ calldata = call.args.map(arg => new Fr(arg));
265
+ } catch (error) {
266
+ this.logger.warn(`Tried assuming that all arguments are Field-like. Failed. Error: ${error}`);
267
+ throw error;
268
+ }
269
+ } else {
270
+ const fnSelector = await getFunctionSelector(call.fnName, contractArtifact);
271
+ const fnAbi = getContractFunctionAbi(call.fnName, contractArtifact)!;
272
+ const encodedArgs = encodeArguments(fnAbi, call.args);
273
+ calldata = [fnSelector.toField(), ...encodedArgs];
274
+ }
242
275
  const isStaticCall = call.isStaticCall ?? false;
243
276
  const request = await PublicCallRequest.fromCalldata(sender, address, isStaticCall, calldata);
244
277
 
@@ -132,8 +132,7 @@ export async function createTxForPublicCalls(
132
132
  : Gas.empty();
133
133
  const gasSettings = new GasSettings(gasLimits, teardownGasLimits, maxFeesPerGas, GasFees.empty());
134
134
  const txContext = new TxContext(Fr.zero(), Fr.zero(), gasSettings);
135
- const header = BlockHeader.empty();
136
- header.globalVariables = globals;
135
+ const header = BlockHeader.empty({ globalVariables: globals });
137
136
  const constantData = new TxConstantData(header, txContext, Fr.zero(), Fr.zero());
138
137
  const includeByTimestamp = 0n; // Not used in the simulator.
139
138
 
@@ -185,7 +185,7 @@ export class AvmFuzzerSimulator extends BaseAvmSimulationTester {
185
185
  super(contractDataSource, merkleTrees);
186
186
  const contractsDb = new PublicContractsDB(contractDataSource);
187
187
  this.simulator = new PublicTxSimulator(merkleTrees, contractsDb, globals, {
188
- skipFeeEnforcement: true,
188
+ skipFeeEnforcement: false,
189
189
  collectDebugLogs: false,
190
190
  collectHints: false,
191
191
  collectStatistics: false,
@@ -209,6 +209,13 @@ export class AvmFuzzerSimulator extends BaseAvmSimulationTester {
209
209
  * Simulate a transaction from a C++ AvmTxHint.
210
210
  */
211
211
  public async simulate(txHint: AvmTxHint): Promise<PublicTxResult> {
212
+ // Compute fee from gas limits and max fees per gas (upper bound on fee)
213
+ const totalFee =
214
+ BigInt(txHint.gasSettings.gasLimits.daGas) * txHint.gasSettings.maxFeesPerGas.feePerDaGas +
215
+ BigInt(txHint.gasSettings.gasLimits.l2Gas) * txHint.gasSettings.maxFeesPerGas.feePerL2Gas;
216
+
217
+ await this.setFeePayerBalance(txHint.feePayer, new Fr(totalFee));
218
+
212
219
  const tx = await createTxFromHint(txHint);
213
220
  return await this.simulator.simulate(tx);
214
221
  }
@@ -98,6 +98,7 @@ async function execute(base64Line: string): Promise<void> {
98
98
  });
99
99
  writeSync(process.stdout.fd, resultBuffer.toString('base64') + '\n');
100
100
  } catch (error: any) {
101
+ // If we error, treat as reverted
101
102
  const errorResult = serializeWithMessagePack({
102
103
  reverted: true,
103
104
  output: [] as string[],
@@ -110,12 +111,30 @@ async function execute(base64Line: string): Promise<void> {
110
111
 
111
112
  function mainLoop() {
112
113
  const rl = createInterface({ input: process.stdin, terminal: false });
114
+
115
+ // Process lines sequentially to avoid race conditions in responses
116
+ const lineQueue: string[] = [];
117
+ let processing = false;
118
+
119
+ async function processQueue() {
120
+ if (processing || lineQueue.length === 0) {
121
+ return;
122
+ }
123
+ processing = true;
124
+ while (lineQueue.length > 0) {
125
+ const line = lineQueue.shift()!;
126
+ await execute(line);
127
+ }
128
+ processing = false;
129
+ }
130
+
113
131
  rl.on('line', (line: string) => {
114
132
  if (line.trim()) {
115
- void execute(line);
133
+ lineQueue.push(line);
134
+ void processQueue();
116
135
  }
117
136
  });
118
137
  rl.on('close', () => process.exit(0));
119
138
  }
120
139
 
121
- mainLoop();
140
+ void mainLoop();
@@ -572,6 +572,10 @@ export class HintingMerkleWriteOperations implements MerkleTreeWriteOperations {
572
572
  return await this.db.close();
573
573
  }
574
574
 
575
+ async [Symbol.dispose](): Promise<void> {
576
+ await this.close();
577
+ }
578
+
575
579
  public async findLeafIndices<ID extends MerkleTreeId>(
576
580
  treeId: ID,
577
581
  values: MerkleTreeLeafType<ID>[],
@@ -81,6 +81,10 @@ export class GuardedMerkleTreeOperations implements MerkleTreeWriteOperations {
81
81
  close(): Promise<void> {
82
82
  return this.guardAndPush(() => this.target.close());
83
83
  }
84
+
85
+ async [Symbol.dispose](): Promise<void> {
86
+ await this.close();
87
+ }
84
88
  getTreeInfo(treeId: MerkleTreeId): Promise<TreeInfo> {
85
89
  return this.guardAndPush(() => this.target.getTreeInfo(treeId));
86
90
  }
@@ -126,7 +126,7 @@ export class PublicProcessor implements Traceable {
126
126
  private dateProvider: DateProvider,
127
127
  telemetryClient: TelemetryClient = getTelemetryClient(),
128
128
  private log = createLogger('simulator:public-processor'),
129
- private opts: Pick<SequencerConfig, 'fakeProcessingDelayPerTxMs'> = {},
129
+ private opts: Pick<SequencerConfig, 'fakeProcessingDelayPerTxMs' | 'fakeThrowAfterProcessingTxCount'> = {},
130
130
  ) {
131
131
  this.metrics = new PublicProcessorMetrics(telemetryClient, 'PublicProcessor');
132
132
  }
@@ -160,7 +160,7 @@ export class PublicProcessor implements Traceable {
160
160
  let totalBlockGas = new Gas(0, 0);
161
161
  let totalBlobFields = 0;
162
162
 
163
- for await (const origTx of txs) {
163
+ for await (const tx of txs) {
164
164
  // Only process up to the max tx limit
165
165
  if (maxTransactions !== undefined && result.length >= maxTransactions) {
166
166
  this.log.debug(`Stopping tx processing due to reaching the max tx limit.`);
@@ -174,8 +174,8 @@ export class PublicProcessor implements Traceable {
174
174
  }
175
175
 
176
176
  // Skip this tx if it'd exceed max block size
177
- const txHash = origTx.getTxHash().toString();
178
- const preTxSizeInBytes = origTx.getEstimatedPrivateTxEffectsSize();
177
+ const txHash = tx.getTxHash().toString();
178
+ const preTxSizeInBytes = tx.getEstimatedPrivateTxEffectsSize();
179
179
  if (maxBlockSize !== undefined && totalSizeInBytes + preTxSizeInBytes > maxBlockSize) {
180
180
  this.log.warn(`Skipping processing of tx ${txHash} sized ${preTxSizeInBytes} bytes due to block size limit`, {
181
181
  txHash,
@@ -187,7 +187,7 @@ export class PublicProcessor implements Traceable {
187
187
  }
188
188
 
189
189
  // Skip this tx if its gas limit would exceed the block gas limit
190
- const txGasLimit = origTx.data.constants.txContext.gasSettings.gasLimits;
190
+ const txGasLimit = tx.data.constants.txContext.gasSettings.gasLimits;
191
191
  if (maxBlockGas !== undefined && totalBlockGas.add(txGasLimit).gtAny(maxBlockGas)) {
192
192
  this.log.warn(`Skipping processing of tx ${txHash} due to block gas limit`, {
193
193
  txHash,
@@ -198,9 +198,6 @@ export class PublicProcessor implements Traceable {
198
198
  continue;
199
199
  }
200
200
 
201
- // The processor modifies the tx objects in place, so we need to clone them.
202
- const tx = Tx.clone(origTx);
203
-
204
201
  // We validate the tx before processing it, to avoid unnecessary work.
205
202
  if (preprocessValidator) {
206
203
  const result = await preprocessValidator.validateTx(tx);
@@ -233,6 +230,12 @@ export class PublicProcessor implements Traceable {
233
230
  try {
234
231
  const [processedTx, returnValues] = await this.processTx(tx, deadline);
235
232
 
233
+ // Inject a fake processing failure after N txs if requested
234
+ const fakeThrowAfter = this.opts.fakeThrowAfterProcessingTxCount;
235
+ if (fakeThrowAfter !== undefined && result.length + failed.length + 1 >= fakeThrowAfter) {
236
+ throw new Error(`Fake error after processing ${fakeThrowAfter} txs`);
237
+ }
238
+
236
239
  const txBlobFields = processedTx.txEffect.getNumBlobFields();
237
240
 
238
241
  // If the actual size of this tx would exceed block size, skip it
@@ -282,7 +285,15 @@ export class PublicProcessor implements Traceable {
282
285
  if (err?.name === 'PublicProcessorTimeoutError') {
283
286
  this.log.warn(`Stopping tx processing due to timeout.`);
284
287
  // We hit the transaction execution deadline.
285
- // There may still be a transaction executing. We stop the guarded fork to prevent any further access to the world state.
288
+ // There may still be a transaction executing on a worker thread (C++ via NAPI).
289
+ // Signal cancellation AND WAIT for the simulation to actually stop.
290
+ // This is critical because C++ might be in the middle of a slow operation (e.g., pad_trees)
291
+ // and won't check the cancellation flag until that operation completes.
292
+ // Without waiting, we'd proceed to revert checkpoints while C++ is still writing to state.
293
+ // Wait for C++ to stop gracefully.
294
+ await this.publicTxSimulator.cancel?.();
295
+
296
+ // Now stop the guarded fork to prevent any further TS-side access to the world state.
286
297
  await this.guardedMerkleTree.stop();
287
298
 
288
299
  // We now know there can't be any further access to world state. The fork is in a state where there is:
@@ -18,7 +18,7 @@ export class ContractProviderForCpp implements ContractProvider {
18
18
  ) {}
19
19
 
20
20
  public getContractInstance = async (address: string): Promise<Buffer | undefined> => {
21
- this.log.debug(`Contract provider callback: getContractInstance(${address})`);
21
+ this.log.trace(`Contract provider callback: getContractInstance(${address})`);
22
22
 
23
23
  const aztecAddr = AztecAddress.fromString(address);
24
24
 
@@ -33,7 +33,7 @@ export class ContractProviderForCpp implements ContractProvider {
33
33
  };
34
34
 
35
35
  public getContractClass = async (classId: string): Promise<Buffer | undefined> => {
36
- this.log.debug(`Contract provider callback: getContractClass(${classId})`);
36
+ this.log.trace(`Contract provider callback: getContractClass(${classId})`);
37
37
 
38
38
  // Parse classId string to Fr
39
39
  const classIdFr = Fr.fromString(classId);
@@ -50,7 +50,7 @@ export class ContractProviderForCpp implements ContractProvider {
50
50
  };
51
51
 
52
52
  public addContracts = async (contractDeploymentDataBuffer: Buffer): Promise<void> => {
53
- this.log.debug(`Contract provider callback: addContracts`);
53
+ this.log.trace(`Contract provider callback: addContracts`);
54
54
 
55
55
  const rawData: any = deserializeFromMessagePack(contractDeploymentDataBuffer);
56
56
 
@@ -58,12 +58,12 @@ export class ContractProviderForCpp implements ContractProvider {
58
58
  const contractDeploymentData = ContractDeploymentData.fromPlainObject(rawData);
59
59
 
60
60
  // Add contracts to the contracts DB
61
- this.log.debug(`Calling contractsDB.addContracts`);
61
+ this.log.trace(`Calling contractsDB.addContracts`);
62
62
  await this.contractsDB.addContracts(contractDeploymentData);
63
63
  };
64
64
 
65
65
  public getBytecodeCommitment = async (classId: string): Promise<Buffer | undefined> => {
66
- this.log.debug(`Contract provider callback: getBytecodeCommitment(${classId})`);
66
+ this.log.trace(`Contract provider callback: getBytecodeCommitment(${classId})`);
67
67
 
68
68
  // Parse classId string to Fr
69
69
  const classIdFr = Fr.fromString(classId);
@@ -81,18 +81,23 @@ export class ContractProviderForCpp implements ContractProvider {
81
81
  };
82
82
 
83
83
  public getDebugFunctionName = async (address: string, selector: string): Promise<string | undefined> => {
84
- this.log.debug(`Contract provider callback: getDebugFunctionName(${address}, ${selector})`);
84
+ this.log.trace(`Contract provider callback: getDebugFunctionName(${address}, ${selector})`);
85
85
 
86
86
  // Parse address and selector strings
87
87
  const aztecAddr = AztecAddress.fromString(address);
88
88
  const selectorFr = Fr.fromString(selector);
89
- const functionSelector = FunctionSelector.fromField(selectorFr);
89
+ const functionSelector = FunctionSelector.fromFieldOrUndefined(selectorFr);
90
+
91
+ if (!functionSelector) {
92
+ this.log.trace(`calldata[0] is not a function selector: ${selector}`);
93
+ return undefined;
94
+ }
90
95
 
91
96
  // Fetch debug function name from the contracts DB
92
97
  const name = await this.contractsDB.getDebugFunctionName(aztecAddr, functionSelector);
93
98
 
94
99
  if (!name) {
95
- this.log.debug(`Debug function name not found for ${address}:${selector}`);
100
+ this.log.trace(`Debug function name not found for ${address}:${selector}`);
96
101
  return undefined;
97
102
  }
98
103
 
@@ -100,17 +105,17 @@ export class ContractProviderForCpp implements ContractProvider {
100
105
  };
101
106
 
102
107
  public createCheckpoint = (): Promise<void> => {
103
- this.log.debug(`Contract provider callback: createCheckpoint`);
108
+ this.log.trace(`Contract provider callback: createCheckpoint`);
104
109
  return Promise.resolve(this.contractsDB.createCheckpoint());
105
110
  };
106
111
 
107
112
  public commitCheckpoint = (): Promise<void> => {
108
- this.log.debug(`Contract provider callback: commitCheckpoint`);
113
+ this.log.trace(`Contract provider callback: commitCheckpoint`);
109
114
  return Promise.resolve(this.contractsDB.commitCheckpoint());
110
115
  };
111
116
 
112
117
  public revertCheckpoint = (): Promise<void> => {
113
- this.log.debug(`Contract provider callback: revertCheckpoint`);
118
+ this.log.trace(`Contract provider callback: revertCheckpoint`);
114
119
  return Promise.resolve(this.contractsDB.revertCheckpoint());
115
120
  };
116
121
  }
@@ -1,5 +1,6 @@
1
1
  import { type Logger, createLogger, logLevel } from '@aztec/foundation/log';
2
- import { avmSimulate } from '@aztec/native';
2
+ import { sleep } from '@aztec/foundation/sleep';
3
+ import { type CancellationToken, avmSimulate, cancelSimulation, createCancellationToken } from '@aztec/native';
3
4
  import { ProtocolContractsList } from '@aztec/protocol-contracts';
4
5
  import {
5
6
  AvmFastSimulationInputs,
@@ -33,6 +34,10 @@ import type {
33
34
  */
34
35
  export class CppPublicTxSimulator extends PublicTxSimulator implements PublicTxSimulatorInterface {
35
36
  protected override log: Logger;
37
+ /** Current cancellation token for in-flight simulation. */
38
+ private cancellationToken?: CancellationToken;
39
+ /** Current simulation promise, used to wait for completion after cancellation. */
40
+ private simulationPromise?: Promise<Buffer>;
36
41
 
37
42
  constructor(
38
43
  merkleTree: MerkleTreeWriteOperations,
@@ -85,12 +90,25 @@ export class CppPublicTxSimulator extends PublicTxSimulator implements PublicTxS
85
90
  this.log.trace(`Serializing fast simulation inputs to msgpack...`);
86
91
  const inputBuffer = fastSimInputs.serializeWithMessagePack();
87
92
 
93
+ // Create cancellation token for this simulation
94
+ this.cancellationToken = createCancellationToken();
95
+
96
+ // Store the promise so cancel() can wait for it
97
+ this.log.debug(`Calling C++ simulator for tx ${txHash}`);
98
+ this.simulationPromise = avmSimulate(inputBuffer, contractProvider, wsCppHandle, logLevel, this.cancellationToken);
99
+
88
100
  let resultBuffer: Buffer;
89
101
  try {
90
- this.log.debug(`Calling C++ simulator for tx ${txHash}`);
91
- resultBuffer = await avmSimulate(inputBuffer, contractProvider, wsCppHandle, logLevel);
102
+ resultBuffer = await this.simulationPromise;
92
103
  } catch (error: any) {
104
+ // Check if this was a cancellation
105
+ if (error.message?.includes('Simulation cancelled')) {
106
+ throw new SimulationError(`C++ simulation cancelled`, []);
107
+ }
93
108
  throw new SimulationError(`C++ simulation failed: ${error.message}`, []);
109
+ } finally {
110
+ this.cancellationToken = undefined;
111
+ this.simulationPromise = undefined;
94
112
  }
95
113
 
96
114
  // If we've reached this point, C++ succeeded during simulation,
@@ -109,6 +127,33 @@ export class CppPublicTxSimulator extends PublicTxSimulator implements PublicTxS
109
127
 
110
128
  return cppResult;
111
129
  }
130
+
131
+ /**
132
+ * Cancel the current simulation if one is in progress.
133
+ * This signals the C++ simulator to stop at the next opcode or before the next WorldState write.
134
+ * Safe to call even if no simulation is in progress.
135
+ *
136
+ * @param waitTimeoutMs - If provided, wait up to this many ms for the simulation to actually stop.
137
+ * This is important because C++ might be in the middle of a slow operation
138
+ * (e.g., pad_trees) and won't check the cancellation flag until it completes.
139
+ * Default timeout of 100ms after cancellation.
140
+ */
141
+ public async cancel(waitTimeoutMs: number = 100): Promise<void> {
142
+ if (this.cancellationToken) {
143
+ this.log.debug('Cancelling C++ simulation');
144
+ cancelSimulation(this.cancellationToken);
145
+ }
146
+
147
+ // Wait for the simulation to actually complete if not already done
148
+ if (this.simulationPromise) {
149
+ this.log.debug(`Waiting up to ${waitTimeoutMs}ms for C++ simulation to stop`);
150
+ await Promise.race([
151
+ this.simulationPromise.catch(() => {}), // Ignore rejection, just wait for completion
152
+ sleep(waitTimeoutMs),
153
+ ]);
154
+ this.log.debug('C++ simulation stopped or wait timed out');
155
+ }
156
+ }
112
157
  }
113
158
 
114
159
  export class MeasuredCppPublicTxSimulator extends CppPublicTxSimulator implements MeasuredPublicTxSimulatorInterface {
@@ -39,7 +39,7 @@ export class CppVsTsPublicTxSimulator extends PublicTxSimulator implements Publi
39
39
  config?: Partial<PublicSimulatorConfig>,
40
40
  ) {
41
41
  super(merkleTree, contractsDB, globalVariables, config);
42
- this.log = createLogger(`simulator:cpp_public_tx_simulator`);
42
+ this.log = createLogger(`simulator:cpp_vs_public_tx_simulator`);
43
43
  }
44
44
 
45
45
  /**
@@ -205,7 +205,8 @@ export class CppVsTsPublicTxSimulator extends PublicTxSimulator implements Publi
205
205
  cppGasUsed: cppResult.gasUsed.totalGas.l2Gas,
206
206
  });
207
207
 
208
- return tsResult;
208
+ // Return cpp result as it has more detailed metadata / revert reasons
209
+ return cppResult;
209
210
  }
210
211
  }
211
212
 
@@ -3,8 +3,31 @@ import type { Tx } from '@aztec/stdlib/tx';
3
3
 
4
4
  export interface PublicTxSimulatorInterface {
5
5
  simulate(tx: Tx): Promise<PublicTxResult>;
6
+ /**
7
+ * Cancel the current simulation if one is in progress.
8
+ * This signals the underlying simulator (e.g., C++) to stop at the next safe point.
9
+ * Safe to call even if no simulation is in progress.
10
+ * Optional - not all implementations support cancellation.
11
+ *
12
+ * @param waitTimeoutMs - If provided, wait up to this many ms for the simulation to actually stop.
13
+ * This is important because signaling cancellation doesn't immediately stop C++ -
14
+ * it only sets a flag that C++ checks at certain points. If C++ is in the middle
15
+ * of a slow operation (e.g., pad_trees), it won't stop until that completes.
16
+ * @returns Promise that resolves when cancellation is signaled (and optionally when simulation stops)
17
+ */
18
+ cancel?(waitTimeoutMs?: number): Promise<void>;
6
19
  }
7
20
 
8
21
  export interface MeasuredPublicTxSimulatorInterface {
9
22
  simulate(tx: Tx, txLabel: string): Promise<PublicTxResult>;
23
+ /**
24
+ * Cancel the current simulation if one is in progress.
25
+ * This signals the underlying simulator (e.g., C++) to stop at the next safe point.
26
+ * Safe to call even if no simulation is in progress.
27
+ * Optional - not all implementations support cancellation.
28
+ *
29
+ * @param waitTimeoutMs - If provided, wait up to this many ms for the simulation to actually stop.
30
+ * @returns Promise that resolves when cancellation is signaled (and optionally when simulation stops)
31
+ */
32
+ cancel?(waitTimeoutMs?: number): Promise<void>;
10
33
  }