@aztec/simulator 0.0.1-commit.733c4a3 → 0.0.1-commit.7689c7f

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 (86) hide show
  1. package/dest/private/acvm_wasm.d.ts +1 -1
  2. package/dest/private/acvm_wasm.d.ts.map +1 -1
  3. package/dest/private/acvm_wasm.js +3 -1
  4. package/dest/private/circuit_recording/circuit_recorder.js +2 -2
  5. package/dest/public/avm/avm_simulator.js +1 -1
  6. package/dest/public/avm/calldata.d.ts +1 -1
  7. package/dest/public/avm/calldata.d.ts.map +1 -1
  8. package/dest/public/avm/calldata.js +3 -2
  9. package/dest/public/avm/fixtures/base_avm_simulation_tester.d.ts +2 -1
  10. package/dest/public/avm/fixtures/base_avm_simulation_tester.d.ts.map +1 -1
  11. package/dest/public/avm/fixtures/base_avm_simulation_tester.js +17 -2
  12. package/dest/public/avm/fixtures/utils.d.ts +1 -1
  13. package/dest/public/avm/fixtures/utils.d.ts.map +1 -1
  14. package/dest/public/avm/fixtures/utils.js +3 -0
  15. package/dest/public/avm/opcodes/contract.d.ts +3 -2
  16. package/dest/public/avm/opcodes/contract.d.ts.map +1 -1
  17. package/dest/public/avm/opcodes/contract.js +5 -1
  18. package/dest/public/avm/opcodes/ec_add.d.ts +2 -4
  19. package/dest/public/avm/opcodes/ec_add.d.ts.map +1 -1
  20. package/dest/public/avm/opcodes/ec_add.js +13 -27
  21. package/dest/public/contracts_db_checkpoint.d.ts +2 -2
  22. package/dest/public/contracts_db_checkpoint.d.ts.map +1 -1
  23. package/dest/public/contracts_db_checkpoint.js +1 -1
  24. package/dest/public/fixtures/bulk_test.d.ts +1 -1
  25. package/dest/public/fixtures/bulk_test.d.ts.map +1 -1
  26. package/dest/public/fixtures/bulk_test.js +31 -4
  27. package/dest/public/fixtures/custom_bytecode_tests.d.ts +3 -1
  28. package/dest/public/fixtures/custom_bytecode_tests.d.ts.map +1 -1
  29. package/dest/public/fixtures/custom_bytecode_tests.js +61 -1
  30. package/dest/public/fixtures/opcode_spammer.d.ts +1 -1
  31. package/dest/public/fixtures/opcode_spammer.d.ts.map +1 -1
  32. package/dest/public/fixtures/opcode_spammer.js +2 -10
  33. package/dest/public/fixtures/public_tx_simulation_tester.d.ts +6 -5
  34. package/dest/public/fixtures/public_tx_simulation_tester.d.ts.map +1 -1
  35. package/dest/public/fixtures/public_tx_simulation_tester.js +10 -10
  36. package/dest/public/fixtures/utils.d.ts +2 -2
  37. package/dest/public/fixtures/utils.d.ts.map +1 -1
  38. package/dest/public/fixtures/utils.js +15 -16
  39. package/dest/public/hinting_db_sources.d.ts +4 -4
  40. package/dest/public/hinting_db_sources.d.ts.map +1 -1
  41. package/dest/public/hinting_db_sources.js +7 -6
  42. package/dest/public/public_db_sources.d.ts +6 -3
  43. package/dest/public/public_db_sources.d.ts.map +1 -1
  44. package/dest/public/public_db_sources.js +16 -10
  45. package/dest/public/public_processor/guarded_merkle_tree.d.ts +4 -4
  46. package/dest/public/public_processor/guarded_merkle_tree.d.ts.map +1 -1
  47. package/dest/public/public_processor/guarded_merkle_tree.js +4 -4
  48. package/dest/public/public_processor/public_processor.d.ts +5 -4
  49. package/dest/public/public_processor/public_processor.d.ts.map +1 -1
  50. package/dest/public/public_processor/public_processor.js +93 -68
  51. package/dest/public/public_processor/public_processor_metrics.d.ts +4 -1
  52. package/dest/public/public_processor/public_processor_metrics.d.ts.map +1 -1
  53. package/dest/public/public_processor/public_processor_metrics.js +8 -0
  54. package/dest/public/public_tx_simulator/contract_provider_for_cpp.d.ts +1 -1
  55. package/dest/public/public_tx_simulator/contract_provider_for_cpp.d.ts.map +1 -1
  56. package/dest/public/public_tx_simulator/contract_provider_for_cpp.js +3 -2
  57. package/dest/public/public_tx_simulator/public_tx_context.d.ts +7 -3
  58. package/dest/public/public_tx_simulator/public_tx_context.d.ts.map +1 -1
  59. package/dest/public/public_tx_simulator/public_tx_context.js +8 -10
  60. package/dest/public/public_tx_simulator/public_tx_simulator.d.ts +6 -2
  61. package/dest/public/public_tx_simulator/public_tx_simulator.d.ts.map +1 -1
  62. package/dest/public/public_tx_simulator/public_tx_simulator.js +12 -7
  63. package/package.json +16 -16
  64. package/src/private/acvm_wasm.ts +4 -1
  65. package/src/private/circuit_recording/circuit_recorder.ts +2 -2
  66. package/src/public/avm/avm_simulator.ts +1 -1
  67. package/src/public/avm/calldata.ts +3 -2
  68. package/src/public/avm/fixtures/base_avm_simulation_tester.ts +22 -2
  69. package/src/public/avm/fixtures/utils.ts +3 -0
  70. package/src/public/avm/opcodes/contract.ts +5 -1
  71. package/src/public/avm/opcodes/ec_add.ts +12 -31
  72. package/src/public/avm/opcodes/external_calls.ts +1 -1
  73. package/src/public/contracts_db_checkpoint.ts +1 -1
  74. package/src/public/fixtures/bulk_test.ts +29 -4
  75. package/src/public/fixtures/custom_bytecode_tests.ts +139 -1
  76. package/src/public/fixtures/opcode_spammer.ts +4 -8
  77. package/src/public/fixtures/public_tx_simulation_tester.ts +21 -11
  78. package/src/public/fixtures/utils.ts +22 -18
  79. package/src/public/hinting_db_sources.ts +9 -6
  80. package/src/public/public_db_sources.ts +21 -14
  81. package/src/public/public_processor/guarded_merkle_tree.ts +5 -5
  82. package/src/public/public_processor/public_processor.ts +120 -80
  83. package/src/public/public_processor/public_processor_metrics.ts +12 -0
  84. package/src/public/public_tx_simulator/contract_provider_for_cpp.ts +3 -2
  85. package/src/public/public_tx_simulator/public_tx_context.ts +8 -10
  86. package/src/public/public_tx_simulator/public_tx_simulator.ts +11 -7
@@ -3,7 +3,7 @@ import { padArrayEnd } from '@aztec/foundation/collection';
3
3
  import { Fr } from '@aztec/foundation/curves/bn254';
4
4
  import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
5
5
  import { sleep } from '@aztec/foundation/sleep';
6
- import { DateProvider, Timer, elapsed, executeTimeout } from '@aztec/foundation/timer';
6
+ import { DateProvider, Timer, elapsed, execWithSignal } from '@aztec/foundation/timer';
7
7
  import { ProtocolContractAddress } from '@aztec/protocol-contracts';
8
8
  import { ContractClassPublishedEvent } from '@aztec/protocol-contracts/class-registry';
9
9
  import { computeFeePayerBalanceLeafSlot, computeFeePayerBalanceStorageSlot } from '@aztec/protocol-contracts/fee-juice';
@@ -76,17 +76,15 @@ export class PublicProcessorFactory {
76
76
  /**
77
77
  * Creates a new instance of a PublicProcessor.
78
78
  * @param globalVariables - The global variables for the block being processed.
79
- * @param skipFeeEnforcement - Allows disabling balance checks for fee estimations.
79
+ * @param contractsDB - Optional pre-populated contracts DB; a fresh one is constructed if omitted.
80
80
  * @returns A new instance of a PublicProcessor.
81
81
  */
82
82
  public create(
83
83
  merkleTree: MerkleTreeWriteOperations,
84
84
  globalVariables: GlobalVariables,
85
85
  config: PublicSimulatorConfig,
86
+ contractsDB: PublicContractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()),
86
87
  ): PublicProcessor {
87
- const bindings = this.log.getBindings();
88
- const contractsDB = new PublicContractsDB(this.contractDataSource, bindings);
89
-
90
88
  const guardedFork = new GuardedMerkleTreeOperations(merkleTree);
91
89
  const publicTxSimulator = this.createPublicTxSimulator(guardedFork, contractsDB, globalVariables, config);
92
90
 
@@ -97,7 +95,7 @@ export class PublicProcessorFactory {
97
95
  publicTxSimulator,
98
96
  this.dateProvider,
99
97
  this.telemetryClient,
100
- createLogger('simulator:public-processor', bindings),
98
+ createLogger('simulator:public-processor', this.log.getBindings()),
101
99
  );
102
100
  }
103
101
 
@@ -125,6 +123,17 @@ class PublicProcessorTimeoutError extends Error {
125
123
  }
126
124
  }
127
125
 
126
+ class PublicProcessorAbortError extends Error {
127
+ constructor(message: string = 'Aborted while processing tx') {
128
+ super(message);
129
+ this.name = 'PublicProcessorAbortError';
130
+ }
131
+ }
132
+
133
+ function isPublicProcessorInterruptError(err: any) {
134
+ return err?.name === 'PublicProcessorTimeoutError' || err?.name === 'PublicProcessorAbortError';
135
+ }
136
+
128
137
  /**
129
138
  * Converts Txs lifted from the P2P module into ProcessedTx objects by executing
130
139
  * any public function calls in them. Txs with private calls only are unaffected.
@@ -160,8 +169,8 @@ export class PublicProcessor implements Traceable {
160
169
  txs: Iterable<Tx> | AsyncIterable<Tx>,
161
170
  limits: PublicProcessorLimits = {},
162
171
  validator: PublicProcessorValidator = {},
163
- ): Promise<[ProcessedTx[], FailedTx[], Tx[], NestedProcessReturnValues[], number, DebugLog[]]> {
164
- const { maxTransactions, maxBlockSize, deadline, maxBlockGas, maxBlobFields } = limits;
172
+ ): Promise<[ProcessedTx[], FailedTx[], Tx[], NestedProcessReturnValues[], DebugLog[]]> {
173
+ const { maxTransactions, deadline, maxBlockGas, maxBlobFields, isBuildingProposal, signal } = limits;
165
174
  const { preprocessValidator, nullifierCache } = validator;
166
175
  const result: ProcessedTx[] = [];
167
176
  const usedTxs: Tx[] = [];
@@ -174,6 +183,8 @@ export class PublicProcessor implements Traceable {
174
183
  let totalPublicGas = new Gas(0, 0);
175
184
  let totalBlockGas = new Gas(0, 0);
176
185
  let totalBlobFields = 0;
186
+ let silentlySkippedCount = 0;
187
+ let totalSilentlySkippedDurationMs = 0;
177
188
 
178
189
  for await (const tx of txs) {
179
190
  // Only process up to the max tx limit
@@ -182,28 +193,33 @@ export class PublicProcessor implements Traceable {
182
193
  break;
183
194
  }
184
195
 
185
- // Bail if we've hit the deadline
196
+ // Bail if we've hit the deadline or have been interrupted.
186
197
  if (deadline && this.dateProvider.now() > +deadline) {
187
198
  this.log.warn(`Stopping tx processing due to timeout.`);
188
199
  break;
189
200
  }
201
+ if (signal?.aborted) {
202
+ this.log.warn(`Stopping tx processing due to abort signal.`);
203
+ break;
204
+ }
190
205
 
191
- // Skip this tx if it'd exceed max block size
192
206
  const txHash = tx.getTxHash().toString();
193
- const preTxSizeInBytes = tx.getEstimatedPrivateTxEffectsSize();
194
- if (maxBlockSize !== undefined && totalSizeInBytes + preTxSizeInBytes > maxBlockSize) {
195
- this.log.warn(`Skipping processing of tx ${txHash} sized ${preTxSizeInBytes} bytes due to block size limit`, {
196
- txHash,
197
- sizeInBytes: preTxSizeInBytes,
198
- totalSizeInBytes,
199
- maxBlockSize,
200
- });
207
+
208
+ // Skip this tx if its estimated blob fields would exceed the limit.
209
+ // Only done during proposal building: during re-execution we must process the exact txs from the proposal.
210
+ const txBlobFields = tx.getPrivateTxEffectsSizeInFields();
211
+ if (isBuildingProposal && maxBlobFields !== undefined && totalBlobFields + txBlobFields > maxBlobFields) {
212
+ this.log.warn(
213
+ `Skipping tx ${txHash} with ${txBlobFields} fields from private side effects due to blob fields limit`,
214
+ { txHash, txBlobFields, totalBlobFields, maxBlobFields },
215
+ );
201
216
  continue;
202
217
  }
203
218
 
204
- // Skip this tx if its gas limit would exceed the block gas limit
219
+ // Skip this tx if its gas limit would exceed the block gas limit (either da or l2).
220
+ // Only done during proposal building: during re-execution we must process the exact txs from the proposal.
205
221
  const txGasLimit = tx.data.constants.txContext.gasSettings.gasLimits;
206
- if (maxBlockGas !== undefined && totalBlockGas.add(txGasLimit).gtAny(maxBlockGas)) {
222
+ if (isBuildingProposal && maxBlockGas !== undefined && totalBlockGas.add(txGasLimit).gtAny(maxBlockGas)) {
207
223
  this.log.warn(`Skipping processing of tx ${txHash} due to block gas limit`, {
208
224
  txHash,
209
225
  txGasLimit,
@@ -223,11 +239,6 @@ export class PublicProcessor implements Traceable {
223
239
  failed.push({ tx, error: new Error(`Tx failed preprocess validation: ${reason}`) });
224
240
  returns.push(new NestedProcessReturnValues([]));
225
241
  continue;
226
- } else if (result.result === 'skipped') {
227
- const reason = result.reason.join(', ');
228
- this.log.debug(`Skipping tx ${txHash.toString()} due to pre-process validation: ${reason}`);
229
- returns.push(new NestedProcessReturnValues([]));
230
- continue;
231
242
  } else {
232
243
  this.log.trace(`Tx ${txHash.toString()} is valid before processing.`);
233
244
  }
@@ -243,7 +254,9 @@ export class PublicProcessor implements Traceable {
243
254
  this.contractsDB.createCheckpoint();
244
255
 
245
256
  try {
246
- const [processedTx, returnValues, txDebugLogs] = await this.processTx(tx, deadline);
257
+ const [txProcessingTimeMs, [processedTx, returnValues, txDebugLogs]] = await elapsed(() =>
258
+ this.processTx(tx, deadline, signal),
259
+ );
247
260
 
248
261
  // Inject a fake processing failure after N txs if requested
249
262
  const fakeThrowAfter = this.opts.fakeThrowAfterProcessingTxCount;
@@ -252,23 +265,9 @@ export class PublicProcessor implements Traceable {
252
265
  }
253
266
 
254
267
  const txBlobFields = processedTx.txEffect.getNumBlobFields();
255
-
256
- // If the actual size of this tx would exceed block size, skip it
257
268
  const txSize = txBlobFields * Fr.SIZE_IN_BYTES;
258
- if (maxBlockSize !== undefined && totalSizeInBytes + txSize > maxBlockSize) {
259
- this.log.debug(`Skipping processed tx ${txHash} sized ${txSize} due to max block size.`, {
260
- txHash,
261
- sizeInBytes: txSize,
262
- totalSizeInBytes,
263
- maxBlockSize,
264
- });
265
- // Need to revert the checkpoint here and don't go any further
266
- await checkpoint.revert();
267
- this.contractsDB.revertCheckpoint();
268
- continue;
269
- }
270
269
 
271
- // If the actual blob fields of this tx would exceed the limit, skip it
270
+ // If the actual blob fields of this tx would exceed the limit, skip it.
272
271
  // Note: maxBlobFields already accounts for block end blob fields and previous blocks in checkpoint.
273
272
  if (maxBlobFields !== undefined && totalBlobFields + txBlobFields > maxBlobFields) {
274
273
  this.log.debug(
@@ -278,14 +277,37 @@ export class PublicProcessor implements Traceable {
278
277
  txBlobFields,
279
278
  totalBlobFields,
280
279
  maxBlobFields,
280
+ txProcessingTimeMs,
281
281
  },
282
282
  );
283
+ silentlySkippedCount += 1;
284
+ totalSilentlySkippedDurationMs += txProcessingTimeMs;
285
+ this.metrics.recordSilentlySkipped(txProcessingTimeMs);
283
286
  // Need to revert the checkpoint here and don't go any further
284
287
  await checkpoint.revert();
285
288
  this.contractsDB.revertCheckpoint();
286
289
  continue;
287
290
  }
288
291
 
292
+ // During re-execution, check if the actual gas used by this tx would push the block over the gas limit.
293
+ // Unlike the proposal-building check (which uses declared gas limits pessimistically before processing),
294
+ // this uses actual gas and stops processing when the limit is exceeded.
295
+ if (
296
+ !isBuildingProposal &&
297
+ maxBlockGas !== undefined &&
298
+ totalBlockGas.add(processedTx.gasUsed.totalGas).gtAny(maxBlockGas)
299
+ ) {
300
+ this.log.warn(`Stopping re-execution since tx ${txHash} would push block gas over limit`, {
301
+ txHash,
302
+ txGas: processedTx.gasUsed.totalGas,
303
+ totalBlockGas,
304
+ maxBlockGas,
305
+ });
306
+ await checkpoint.revert();
307
+ this.contractsDB.revertCheckpoint();
308
+ break;
309
+ }
310
+
289
311
  // FIXME(fcarreiro): it's ugly to have to notify the validator of nullifiers.
290
312
  // I'd rather pass the validators the processedTx as well and let them deal with it.
291
313
  nullifierCache?.addNullifiers(processedTx.txEffect.nullifiers.map(n => n.toBuffer()));
@@ -300,16 +322,15 @@ export class PublicProcessor implements Traceable {
300
322
  totalBlockGas = totalBlockGas.add(processedTx.gasUsed.totalGas);
301
323
  totalSizeInBytes += txSize;
302
324
  totalBlobFields += txBlobFields;
325
+
326
+ // Commit the tx-level contracts checkpoint on success
327
+ this.contractsDB.commitCheckpoint();
303
328
  } catch (err: any) {
304
- if (err?.name === 'PublicProcessorTimeoutError') {
305
- this.log.warn(`Stopping tx processing due to timeout.`);
306
- // We hit the transaction execution deadline.
307
- // There may still be a transaction executing on a worker thread (C++ via NAPI).
308
- // Signal cancellation AND WAIT for the simulation to actually stop.
309
- // This is critical because C++ might be in the middle of a slow operation (e.g., pad_trees)
310
- // and won't check the cancellation flag until that operation completes.
311
- // Without waiting, we'd proceed to revert checkpoints while C++ is still writing to state.
312
- // Wait for C++ to stop gracefully.
329
+ if (isPublicProcessorInterruptError(err)) {
330
+ const interruptReason = err.name === 'PublicProcessorTimeoutError' ? 'timeout' : 'abort signal';
331
+ this.log.warn(`Stopping tx processing due to ${interruptReason}.`);
332
+ // The tx may still be executing on a worker thread (C++ via NAPI).
333
+ // Signal cancellation AND WAIT for the simulation to actually stop before touching fork checkpoints.
313
334
  await this.publicTxSimulator.cancel?.();
314
335
 
315
336
  // Now stop the guarded fork to prevent any further TS-side access to the world state.
@@ -319,14 +340,10 @@ export class PublicProcessor implements Traceable {
319
340
  // 1. At least one outstanding checkpoint that has not been committed (the one created before we processed the tx).
320
341
  // 2. Possible state updates on that checkpoint or any others created during execution.
321
342
 
322
- // First we revert a checkpoint as managed by the ForkCheckpoint. This will revert whatever is the current checkpoint
323
- // which may not be the one originally created by this object. But that is ok, we do this to fulfil the ForkCheckpoint
324
- // lifecycle expectations and ensure it doesn't attempt to commit later on.
325
- await checkpoint.revert();
326
-
327
- // Now we want to revert any/all remaining checkpoints, destroying any outstanding state updates.
328
- // This needs to be done directly on the underlying fork as the guarded fork has been stopped.
329
- await this.guardedMerkleTree.getUnderlyingFork().revertAllCheckpoints();
343
+ // Revert all checkpoints at or above this checkpoint's depth (inclusive), destroying any outstanding state
344
+ // updates from this tx and any nested checkpoints created during execution. This preserves any checkpoints
345
+ // created by callers below our depth.
346
+ await checkpoint.revertToCheckpoint();
330
347
 
331
348
  // Revert any contracts added to the DB for the tx.
332
349
  this.contractsDB.revertCheckpoint();
@@ -338,9 +355,9 @@ export class PublicProcessor implements Traceable {
338
355
  break;
339
356
  }
340
357
 
341
- // Roll back state to start of TX before proceeding to next TX
342
- await checkpoint.revert();
343
- await this.guardedMerkleTree.getUnderlyingFork().revertAllCheckpoints();
358
+ // Roll back state to start of TX before proceeding to next TX.
359
+ // Reverts all checkpoints at or above this checkpoint's depth, preserving any caller checkpoints below.
360
+ await checkpoint.revertToCheckpoint();
344
361
  this.contractsDB.revertCheckpoint();
345
362
  const errorMessage = err instanceof Error || err instanceof AssertionError ? err.message : 'Unknown error';
346
363
  this.log.warn(`Failed to process tx ${txHash.toString()}: ${errorMessage} ${err?.stack}`);
@@ -352,7 +369,6 @@ export class PublicProcessor implements Traceable {
352
369
  } finally {
353
370
  // Base case is we always commit the checkpoint. Using the ForkCheckpoint means this has no effect if the tx was previously reverted
354
371
  await checkpoint.commit();
355
- this.contractsDB.commitCheckpointOkIfNone();
356
372
  }
357
373
  }
358
374
 
@@ -360,15 +376,26 @@ export class PublicProcessor implements Traceable {
360
376
  const rate = duration > 0 ? totalPublicGas.l2Gas / duration : 0;
361
377
  this.metrics.recordAllTxs(totalPublicGas, rate);
362
378
 
363
- this.log.info(`Processed ${result.length} successful txs and ${failed.length} failed txs in ${duration}s`, {
364
- duration,
365
- rate,
366
- totalPublicGas,
367
- totalBlockGas,
368
- totalSizeInBytes,
369
- });
379
+ const silentlySkippedDurationMs = Math.round(totalSilentlySkippedDurationMs);
380
+ this.log.info(
381
+ `Processed ${result.length} successful txs and ${failed.length} failed txs ` +
382
+ `(${silentlySkippedCount} silently skipped, ${silentlySkippedDurationMs}ms wasted) ` +
383
+ `in ${duration}s`,
384
+ {
385
+ blockNumber: this.globalVariables.blockNumber,
386
+ successfulCount: result.length,
387
+ failedCount: failed.length,
388
+ duration,
389
+ rate,
390
+ totalPublicGas,
391
+ totalBlockGas,
392
+ totalSizeInBytes,
393
+ silentlySkippedCount,
394
+ silentlySkippedDurationMs,
395
+ },
396
+ );
370
397
 
371
- return [result, failed, usedTxs, returns, totalBlobFields, debugLogs];
398
+ return [result, failed, usedTxs, returns, debugLogs];
372
399
  }
373
400
 
374
401
  private async checkWorldStateUnchanged(
@@ -391,9 +418,10 @@ export class PublicProcessor implements Traceable {
391
418
  private async processTx(
392
419
  tx: Tx,
393
420
  deadline: Date | undefined,
421
+ signal: AbortSignal | undefined,
394
422
  ): Promise<[ProcessedTx, NestedProcessReturnValues[], DebugLog[]]> {
395
423
  const [time, [processedTx, returnValues, debugLogs]] = await elapsed(() =>
396
- this.processTxWithinDeadline(tx, deadline),
424
+ this.processTxWithinDeadline(tx, deadline, signal),
397
425
  );
398
426
 
399
427
  this.log.verbose(
@@ -447,10 +475,11 @@ export class PublicProcessor implements Traceable {
447
475
  this.metrics.recordTreeInsertions(Number(treeInsertionEnd - treeInsertionStart) / 1_000);
448
476
  }
449
477
 
450
- /** Processes the given tx within deadline. Returns timeout if deadline is hit. */
478
+ /** Processes the given tx within deadline or until the signal is aborted. */
451
479
  private async processTxWithinDeadline(
452
480
  tx: Tx,
453
481
  deadline: Date | undefined,
482
+ signal: AbortSignal | undefined,
454
483
  ): Promise<[ProcessedTx, NestedProcessReturnValues[] | undefined, DebugLog[]]> {
455
484
  const innerProcessFn: () => Promise<[ProcessedTx, NestedProcessReturnValues[] | undefined, DebugLog[]]> =
456
485
  tx.hasPublicCalls() ? () => this.processTxWithPublicCalls(tx) : () => this.processPrivateOnlyTx(tx);
@@ -467,27 +496,38 @@ export class PublicProcessor implements Traceable {
467
496
  }
468
497
  : innerProcessFn;
469
498
 
470
- if (!deadline) {
499
+ const processingSignal = this.getProcessingSignal(tx, deadline, signal);
500
+ if (!processingSignal) {
471
501
  return await processFn();
472
502
  }
473
503
 
474
- const txHash = tx.getTxHash();
504
+ return await execWithSignal(
505
+ () => processFn(),
506
+ processingSignal,
507
+ signal =>
508
+ signal.reason?.name === 'TimeoutError' ? new PublicProcessorTimeoutError() : new PublicProcessorAbortError(),
509
+ );
510
+ }
511
+
512
+ private getProcessingSignal(tx: Tx, deadline: Date | undefined, signal: AbortSignal | undefined) {
513
+ if (!deadline) {
514
+ return signal;
515
+ }
516
+
475
517
  const timeout = +deadline - this.dateProvider.now();
476
518
  if (timeout <= 0) {
477
519
  throw new PublicProcessorTimeoutError();
478
520
  }
479
521
 
522
+ const txHash = tx.getTxHash();
480
523
  this.log.debug(`Processing tx ${txHash.toString()} within ${timeout}ms`, {
481
524
  deadline: deadline.toISOString(),
482
525
  now: new Date(this.dateProvider.now()).toISOString(),
483
526
  txHash,
484
527
  });
485
528
 
486
- return await executeTimeout(
487
- () => processFn(),
488
- timeout,
489
- () => new PublicProcessorTimeoutError(),
490
- );
529
+ const timeoutSignal = AbortSignal.timeout(timeout);
530
+ return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
491
531
  }
492
532
 
493
533
  /**
@@ -544,7 +584,7 @@ export class PublicProcessor implements Traceable {
544
584
  // Fee payment insertion has already been done. Do the rest.
545
585
  await this.doTreeInsertionsForPrivateOnlyTx(processedTx);
546
586
 
547
- await this.contractsDB.addNewContracts(tx);
587
+ this.contractsDB.addNewContracts(tx);
548
588
 
549
589
  return [processedTx, undefined, []];
550
590
  }
@@ -30,6 +30,9 @@ export class PublicProcessorMetrics {
30
30
 
31
31
  private treeInsertionDuration: Histogram;
32
32
 
33
+ private silentlySkippedCount: UpDownCounter;
34
+ private silentlySkippedDuration: Histogram;
35
+
33
36
  constructor(client: TelemetryClient, name = 'PublicProcessor') {
34
37
  this.tracer = client.getTracer(name);
35
38
  const meter = client.getMeter(name);
@@ -60,6 +63,10 @@ export class PublicProcessorMetrics {
60
63
  this.gasRate = meter.createHistogram(Metrics.PUBLIC_PROCESSOR_GAS_RATE);
61
64
 
62
65
  this.treeInsertionDuration = meter.createHistogram(Metrics.PUBLIC_PROCESSOR_TREE_INSERTION);
66
+
67
+ this.silentlySkippedCount = createUpDownCounterWithDefault(meter, Metrics.PUBLIC_PROCESSOR_SILENTLY_SKIPPED_COUNT);
68
+
69
+ this.silentlySkippedDuration = meter.createHistogram(Metrics.PUBLIC_PROCESSOR_SILENTLY_SKIPPED_DURATION);
63
70
  }
64
71
 
65
72
  recordPhaseDuration(phaseName: TxExecutionPhase, durationMs: number) {
@@ -123,4 +130,9 @@ export class PublicProcessorMetrics {
123
130
  recordTreeInsertions(durationUs: number) {
124
131
  this.treeInsertionDuration.record(Math.ceil(durationUs));
125
132
  }
133
+
134
+ recordSilentlySkipped(durationMs: number) {
135
+ this.silentlySkippedCount.add(1);
136
+ this.silentlySkippedDuration.record(Math.ceil(durationMs));
137
+ }
126
138
  }
@@ -52,6 +52,7 @@ export class ContractProviderForCpp implements ContractProvider {
52
52
  return serializeWithMessagePack(contractClass);
53
53
  };
54
54
 
55
+ // eslint-disable-next-line require-await
55
56
  public addContracts = async (contractDeploymentDataBuffer: Buffer): Promise<void> => {
56
57
  this.log.trace(`Contract provider callback: addContracts`);
57
58
 
@@ -61,8 +62,8 @@ export class ContractProviderForCpp implements ContractProvider {
61
62
  const contractDeploymentData = ContractDeploymentData.fromPlainObject(rawData);
62
63
 
63
64
  // Add contracts to the contracts DB
64
- this.log.trace(`Calling contractsDB.addContracts`);
65
- await this.contractsDB.addContracts(contractDeploymentData);
65
+ this.log.trace(`Calling contractsDB.addContractsFromLogs`);
66
+ this.contractsDB.addContractsFromLogs(contractDeploymentData);
66
67
  };
67
68
 
68
69
  public getBytecodeCommitment = async (classId: string): Promise<Buffer | undefined> => {
@@ -174,14 +174,8 @@ export class PublicTxContext {
174
174
  }
175
175
  if (phase === TxExecutionPhase.SETUP) {
176
176
  this.log.warn(`Setup phase reverted! The transaction will be thrown out.`);
177
- } else if (phase === TxExecutionPhase.APP_LOGIC) {
178
- this.revertCode = RevertCode.APP_LOGIC_REVERTED;
179
- } else if (phase === TxExecutionPhase.TEARDOWN) {
180
- if (this.revertCode.equals(RevertCode.APP_LOGIC_REVERTED)) {
181
- this.revertCode = RevertCode.BOTH_REVERTED;
182
- } else {
183
- this.revertCode = RevertCode.TEARDOWN_REVERTED;
184
- }
177
+ } else if (phase === TxExecutionPhase.APP_LOGIC || phase === TxExecutionPhase.TEARDOWN) {
178
+ this.revertCode = RevertCode.REVERTED;
185
179
  }
186
180
  }
187
181
 
@@ -247,8 +241,12 @@ export class PublicTxContext {
247
241
  }
248
242
 
249
243
  /**
250
- * The gasUsed by public and private,
251
- * as if the entire teardown gas limit was consumed.
244
+ * The gasUsed by public and private, as if the entire teardown gas limit was consumed.
245
+ *
246
+ * This is intentional: teardown is used for gas accounting and refunds, so the transaction
247
+ * fee must be deterministic _before_ teardown executes. If fees depended on teardown's actual
248
+ * consumption there would be a circular dependency. Billing the full teardown gas limit
249
+ * (set by the user) makes the fee known in advance and available to the teardown function.
252
250
  */
253
251
  getTotalGasUsed(): Gas {
254
252
  return this.gasUsedByPrivate.add(this.gasUsedByPublic);
@@ -27,7 +27,6 @@ import { type PublicContractsDB, PublicTreesDB } from '../public_db_sources.js';
27
27
  import {
28
28
  L2ToL1MessageLimitReachedError,
29
29
  NoteHashLimitReachedError,
30
- NullifierCollisionError,
31
30
  NullifierLimitReachedError,
32
31
  } from '../side_effect_errors.js';
33
32
  import type { PublicPersistableStateManager } from '../state_manager/state_manager.js';
@@ -147,7 +146,8 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
147
146
  hintingContractsDB.createCheckpoint();
148
147
 
149
148
  try {
150
- // This will throw if there is a nullifier collision or other insertion error (limit reached).
149
+ // This may throw: a side-effect limit error triggers a soft revert (caught below), while a
150
+ // nullifier collision is unrecoverable and propagates out of simulate() to throw out the tx.
151
151
  await this.insertRevertiblesFromPrivate(context);
152
152
 
153
153
  // Only proceed with app logic if there was no revert during revertible insertion.
@@ -401,7 +401,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
401
401
  // However, things work as expected because later calls to getters on the hintingContractsDB
402
402
  // will pick up the new contracts and will generate the necessary hints.
403
403
  // So, a consumer of the hints will always see the new contracts.
404
- await this.contractsDB.addContracts(context.nonRevertibleContractDeploymentData);
404
+ this.contractsDB.addContractsFromLogs(context.nonRevertibleContractDeploymentData);
405
405
  }
406
406
 
407
407
  /**
@@ -409,9 +409,13 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
409
409
  * Throws TxSimRevertibleInsertionsRevert if there is some checked error during revertible insertions.
410
410
  * This function checks for the following errors:
411
411
  * - NullifierLimitReachedError
412
- * - NullifierCollisionError
413
412
  * - NoteHashLimitReachedError
414
413
  * - L2ToL1MessageLimitReachedError
414
+ *
415
+ * Note: NullifierCollisionError is intentionally NOT caught here. A nullifier collision
416
+ * during revertible insertions is unprovable (the nullifier originated from private, so
417
+ * a collision indicates the tx should never have been proposed). It propagates as-is to
418
+ * make the transaction unrecoverable, matching the AVM circuit behavior.
415
419
  */
416
420
  protected async insertRevertiblesFromPrivate(context: PublicTxContext) {
417
421
  const stateManager = context.state.getActiveStateManager();
@@ -421,7 +425,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
421
425
  await stateManager.writeSiloedNullifier(siloedNullifier);
422
426
  }
423
427
  } catch (e: any) {
424
- if (e instanceof NullifierLimitReachedError || e instanceof NullifierCollisionError) {
428
+ if (e instanceof NullifierLimitReachedError) {
425
429
  context.revert(
426
430
  TxExecutionPhase.APP_LOGIC,
427
431
  new SimulationError(
@@ -431,7 +435,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
431
435
  );
432
436
  throw new TxSimRevertibleInsertionsRevert();
433
437
  } else {
434
- // Unchecked/unknown error - re-throw as-is
438
+ // Unchecked/unknown error or NullifierCollisionError (unrecoverable) - re-throw as-is
435
439
  throw e;
436
440
  }
437
441
  }
@@ -486,7 +490,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
486
490
  // However, things work as expected because later calls to getters on the hintingContractsDB
487
491
  // will pick up the new contracts and will generate the necessary hints.
488
492
  // So, a consumer of the hints will always see the new contracts.
489
- await this.contractsDB.addContracts(context.revertibleContractDeploymentData);
493
+ this.contractsDB.addContractsFromLogs(context.revertibleContractDeploymentData);
490
494
  }
491
495
 
492
496
  private async payFee(context: PublicTxContext) {