@aztec/simulator 0.0.1-commit.f504929 → 0.0.1-commit.f5a9928

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 (110) hide show
  1. package/dest/client.d.ts +1 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +0 -2
  4. package/dest/private/acvm_wasm.d.ts +1 -1
  5. package/dest/private/acvm_wasm.d.ts.map +1 -1
  6. package/dest/private/acvm_wasm.js +3 -1
  7. package/dest/private/circuit_recording/circuit_recorder.d.ts +36 -23
  8. package/dest/private/circuit_recording/circuit_recorder.d.ts.map +1 -1
  9. package/dest/private/circuit_recording/circuit_recorder.js +65 -69
  10. package/dest/private/circuit_recording/file_circuit_recorder.d.ts +7 -19
  11. package/dest/private/circuit_recording/file_circuit_recorder.d.ts.map +1 -1
  12. package/dest/private/circuit_recording/file_circuit_recorder.js +38 -42
  13. package/dest/private/circuit_recording/simulator_recorder_wrapper.d.ts +1 -1
  14. package/dest/private/circuit_recording/simulator_recorder_wrapper.d.ts.map +1 -1
  15. package/dest/private/circuit_recording/simulator_recorder_wrapper.js +11 -14
  16. package/dest/public/avm/avm_simulator.js +1 -1
  17. package/dest/public/avm/calldata.d.ts +1 -1
  18. package/dest/public/avm/calldata.d.ts.map +1 -1
  19. package/dest/public/avm/calldata.js +3 -2
  20. package/dest/public/avm/fixtures/base_avm_simulation_tester.d.ts +2 -1
  21. package/dest/public/avm/fixtures/base_avm_simulation_tester.d.ts.map +1 -1
  22. package/dest/public/avm/fixtures/base_avm_simulation_tester.js +18 -3
  23. package/dest/public/avm/fixtures/utils.d.ts +1 -1
  24. package/dest/public/avm/fixtures/utils.d.ts.map +1 -1
  25. package/dest/public/avm/fixtures/utils.js +4 -1
  26. package/dest/public/avm/opcodes/contract.d.ts +3 -2
  27. package/dest/public/avm/opcodes/contract.d.ts.map +1 -1
  28. package/dest/public/avm/opcodes/contract.js +5 -1
  29. package/dest/public/avm/opcodes/ec_add.d.ts +2 -4
  30. package/dest/public/avm/opcodes/ec_add.d.ts.map +1 -1
  31. package/dest/public/avm/opcodes/ec_add.js +13 -27
  32. package/dest/public/contracts_db_checkpoint.d.ts +2 -2
  33. package/dest/public/contracts_db_checkpoint.d.ts.map +1 -1
  34. package/dest/public/contracts_db_checkpoint.js +1 -1
  35. package/dest/public/fixtures/amm_test.js +2 -2
  36. package/dest/public/fixtures/bulk_test.d.ts +1 -1
  37. package/dest/public/fixtures/bulk_test.d.ts.map +1 -1
  38. package/dest/public/fixtures/bulk_test.js +33 -6
  39. package/dest/public/fixtures/custom_bytecode_tester.d.ts +1 -1
  40. package/dest/public/fixtures/custom_bytecode_tester.d.ts.map +1 -1
  41. package/dest/public/fixtures/custom_bytecode_tester.js +2 -2
  42. package/dest/public/fixtures/custom_bytecode_tests.d.ts +8 -1
  43. package/dest/public/fixtures/custom_bytecode_tests.d.ts.map +1 -1
  44. package/dest/public/fixtures/custom_bytecode_tests.js +91 -2
  45. package/dest/public/fixtures/opcode_spammer.d.ts +1 -1
  46. package/dest/public/fixtures/opcode_spammer.d.ts.map +1 -1
  47. package/dest/public/fixtures/opcode_spammer.js +2 -10
  48. package/dest/public/fixtures/public_tx_simulation_tester.d.ts +6 -5
  49. package/dest/public/fixtures/public_tx_simulation_tester.d.ts.map +1 -1
  50. package/dest/public/fixtures/public_tx_simulation_tester.js +11 -9
  51. package/dest/public/fixtures/token_test.js +3 -3
  52. package/dest/public/fixtures/utils.d.ts +2 -2
  53. package/dest/public/fixtures/utils.d.ts.map +1 -1
  54. package/dest/public/fixtures/utils.js +17 -16
  55. package/dest/public/hinting_db_sources.d.ts +4 -4
  56. package/dest/public/hinting_db_sources.d.ts.map +1 -1
  57. package/dest/public/hinting_db_sources.js +7 -6
  58. package/dest/public/public_db_sources.d.ts +6 -3
  59. package/dest/public/public_db_sources.d.ts.map +1 -1
  60. package/dest/public/public_db_sources.js +16 -10
  61. package/dest/public/public_processor/guarded_merkle_tree.d.ts +4 -4
  62. package/dest/public/public_processor/guarded_merkle_tree.d.ts.map +1 -1
  63. package/dest/public/public_processor/guarded_merkle_tree.js +4 -4
  64. package/dest/public/public_processor/public_processor.d.ts +5 -4
  65. package/dest/public/public_processor/public_processor.d.ts.map +1 -1
  66. package/dest/public/public_processor/public_processor.js +107 -63
  67. package/dest/public/public_processor/public_processor_metrics.d.ts +4 -1
  68. package/dest/public/public_processor/public_processor_metrics.d.ts.map +1 -1
  69. package/dest/public/public_processor/public_processor_metrics.js +8 -0
  70. package/dest/public/public_tx_simulator/contract_provider_for_cpp.d.ts +1 -1
  71. package/dest/public/public_tx_simulator/contract_provider_for_cpp.d.ts.map +1 -1
  72. package/dest/public/public_tx_simulator/contract_provider_for_cpp.js +5 -4
  73. package/dest/public/public_tx_simulator/public_tx_context.d.ts +7 -3
  74. package/dest/public/public_tx_simulator/public_tx_context.d.ts.map +1 -1
  75. package/dest/public/public_tx_simulator/public_tx_context.js +8 -10
  76. package/dest/public/public_tx_simulator/public_tx_simulator.d.ts +6 -2
  77. package/dest/public/public_tx_simulator/public_tx_simulator.d.ts.map +1 -1
  78. package/dest/public/public_tx_simulator/public_tx_simulator.js +12 -7
  79. package/dest/public/state_manager/state_manager.js +1 -1
  80. package/package.json +16 -16
  81. package/src/client.ts +0 -2
  82. package/src/private/acvm_wasm.ts +4 -1
  83. package/src/private/circuit_recording/circuit_recorder.ts +84 -78
  84. package/src/private/circuit_recording/file_circuit_recorder.ts +38 -48
  85. package/src/private/circuit_recording/simulator_recorder_wrapper.ts +9 -15
  86. package/src/public/avm/avm_simulator.ts +1 -1
  87. package/src/public/avm/calldata.ts +3 -2
  88. package/src/public/avm/fixtures/base_avm_simulation_tester.ts +23 -3
  89. package/src/public/avm/fixtures/utils.ts +4 -1
  90. package/src/public/avm/opcodes/contract.ts +5 -1
  91. package/src/public/avm/opcodes/ec_add.ts +12 -31
  92. package/src/public/avm/opcodes/external_calls.ts +1 -1
  93. package/src/public/contracts_db_checkpoint.ts +1 -1
  94. package/src/public/fixtures/amm_test.ts +2 -2
  95. package/src/public/fixtures/bulk_test.ts +31 -6
  96. package/src/public/fixtures/custom_bytecode_tester.ts +2 -2
  97. package/src/public/fixtures/custom_bytecode_tests.ts +209 -2
  98. package/src/public/fixtures/opcode_spammer.ts +4 -8
  99. package/src/public/fixtures/public_tx_simulation_tester.ts +21 -12
  100. package/src/public/fixtures/token_test.ts +3 -3
  101. package/src/public/fixtures/utils.ts +19 -20
  102. package/src/public/hinting_db_sources.ts +9 -6
  103. package/src/public/public_db_sources.ts +21 -14
  104. package/src/public/public_processor/guarded_merkle_tree.ts +5 -5
  105. package/src/public/public_processor/public_processor.ts +138 -77
  106. package/src/public/public_processor/public_processor_metrics.ts +12 -0
  107. package/src/public/public_tx_simulator/contract_provider_for_cpp.ts +5 -4
  108. package/src/public/public_tx_simulator/public_tx_context.ts +8 -10
  109. package/src/public/public_tx_simulator/public_tx_simulator.ts +11 -7
  110. package/src/public/state_manager/state_manager.ts +1 -1
@@ -1,9 +1,14 @@
1
- import { MAX_NOTE_HASHES_PER_TX, MAX_NULLIFIERS_PER_TX, NULLIFIER_SUBTREE_HEIGHT } from '@aztec/constants';
1
+ import {
2
+ MAX_NOTE_HASHES_PER_TX,
3
+ MAX_NULLIFIERS_PER_TX,
4
+ MAX_TX_BLOB_DATA_SIZE_IN_FIELDS,
5
+ NULLIFIER_SUBTREE_HEIGHT,
6
+ } from '@aztec/constants';
2
7
  import { padArrayEnd } from '@aztec/foundation/collection';
3
8
  import { Fr } from '@aztec/foundation/curves/bn254';
4
9
  import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
5
10
  import { sleep } from '@aztec/foundation/sleep';
6
- import { DateProvider, Timer, elapsed, executeTimeout } from '@aztec/foundation/timer';
11
+ import { DateProvider, Timer, elapsed, execWithSignal } from '@aztec/foundation/timer';
7
12
  import { ProtocolContractAddress } from '@aztec/protocol-contracts';
8
13
  import { ContractClassPublishedEvent } from '@aztec/protocol-contracts/class-registry';
9
14
  import { computeFeePayerBalanceLeafSlot, computeFeePayerBalanceStorageSlot } from '@aztec/protocol-contracts/fee-juice';
@@ -76,17 +81,15 @@ export class PublicProcessorFactory {
76
81
  /**
77
82
  * Creates a new instance of a PublicProcessor.
78
83
  * @param globalVariables - The global variables for the block being processed.
79
- * @param skipFeeEnforcement - Allows disabling balance checks for fee estimations.
84
+ * @param contractsDB - Optional pre-populated contracts DB; a fresh one is constructed if omitted.
80
85
  * @returns A new instance of a PublicProcessor.
81
86
  */
82
87
  public create(
83
88
  merkleTree: MerkleTreeWriteOperations,
84
89
  globalVariables: GlobalVariables,
85
90
  config: PublicSimulatorConfig,
91
+ contractsDB: PublicContractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()),
86
92
  ): PublicProcessor {
87
- const bindings = this.log.getBindings();
88
- const contractsDB = new PublicContractsDB(this.contractDataSource, bindings);
89
-
90
93
  const guardedFork = new GuardedMerkleTreeOperations(merkleTree);
91
94
  const publicTxSimulator = this.createPublicTxSimulator(guardedFork, contractsDB, globalVariables, config);
92
95
 
@@ -97,7 +100,7 @@ export class PublicProcessorFactory {
97
100
  publicTxSimulator,
98
101
  this.dateProvider,
99
102
  this.telemetryClient,
100
- createLogger('simulator:public-processor', bindings),
103
+ createLogger('simulator:public-processor', this.log.getBindings()),
101
104
  );
102
105
  }
103
106
 
@@ -125,6 +128,17 @@ class PublicProcessorTimeoutError extends Error {
125
128
  }
126
129
  }
127
130
 
131
+ class PublicProcessorAbortError extends Error {
132
+ constructor(message: string = 'Aborted while processing tx') {
133
+ super(message);
134
+ this.name = 'PublicProcessorAbortError';
135
+ }
136
+ }
137
+
138
+ function isPublicProcessorInterruptError(err: any) {
139
+ return err?.name === 'PublicProcessorTimeoutError' || err?.name === 'PublicProcessorAbortError';
140
+ }
141
+
128
142
  /**
129
143
  * Converts Txs lifted from the P2P module into ProcessedTx objects by executing
130
144
  * any public function calls in them. Txs with private calls only are unaffected.
@@ -160,8 +174,8 @@ export class PublicProcessor implements Traceable {
160
174
  txs: Iterable<Tx> | AsyncIterable<Tx>,
161
175
  limits: PublicProcessorLimits = {},
162
176
  validator: PublicProcessorValidator = {},
163
- ): Promise<[ProcessedTx[], FailedTx[], Tx[], NestedProcessReturnValues[], number, DebugLog[]]> {
164
- const { maxTransactions, maxBlockSize, deadline, maxBlockGas, maxBlobFields } = limits;
177
+ ): Promise<[ProcessedTx[], FailedTx[], Tx[], NestedProcessReturnValues[], DebugLog[]]> {
178
+ const { maxTransactions, deadline, maxBlockGas, maxBlobFields, isBuildingProposal, signal } = limits;
165
179
  const { preprocessValidator, nullifierCache } = validator;
166
180
  const result: ProcessedTx[] = [];
167
181
  const usedTxs: Tx[] = [];
@@ -174,6 +188,8 @@ export class PublicProcessor implements Traceable {
174
188
  let totalPublicGas = new Gas(0, 0);
175
189
  let totalBlockGas = new Gas(0, 0);
176
190
  let totalBlobFields = 0;
191
+ let silentlySkippedCount = 0;
192
+ let totalSilentlySkippedDurationMs = 0;
177
193
 
178
194
  for await (const tx of txs) {
179
195
  // Only process up to the max tx limit
@@ -182,28 +198,33 @@ export class PublicProcessor implements Traceable {
182
198
  break;
183
199
  }
184
200
 
185
- // Bail if we've hit the deadline
201
+ // Bail if we've hit the deadline or have been interrupted.
186
202
  if (deadline && this.dateProvider.now() > +deadline) {
187
203
  this.log.warn(`Stopping tx processing due to timeout.`);
188
204
  break;
189
205
  }
206
+ if (signal?.aborted) {
207
+ this.log.warn(`Stopping tx processing due to abort signal.`);
208
+ break;
209
+ }
190
210
 
191
- // Skip this tx if it'd exceed max block size
192
211
  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
- });
212
+
213
+ // Skip this tx if its estimated blob fields would exceed the limit.
214
+ // Only done during proposal building: during re-execution we must process the exact txs from the proposal.
215
+ const txBlobFields = tx.getPrivateTxEffectsSizeInFields();
216
+ if (isBuildingProposal && maxBlobFields !== undefined && totalBlobFields + txBlobFields > maxBlobFields) {
217
+ this.log.warn(
218
+ `Skipping tx ${txHash} with ${txBlobFields} fields from private side effects due to blob fields limit`,
219
+ { txHash, txBlobFields, totalBlobFields, maxBlobFields },
220
+ );
201
221
  continue;
202
222
  }
203
223
 
204
- // Skip this tx if its gas limit would exceed the block gas limit
224
+ // Skip this tx if its gas limit would exceed the block gas limit (either da or l2).
225
+ // Only done during proposal building: during re-execution we must process the exact txs from the proposal.
205
226
  const txGasLimit = tx.data.constants.txContext.gasSettings.gasLimits;
206
- if (maxBlockGas !== undefined && totalBlockGas.add(txGasLimit).gtAny(maxBlockGas)) {
227
+ if (isBuildingProposal && maxBlockGas !== undefined && totalBlockGas.add(txGasLimit).gtAny(maxBlockGas)) {
207
228
  this.log.warn(`Skipping processing of tx ${txHash} due to block gas limit`, {
208
229
  txHash,
209
230
  txGasLimit,
@@ -223,11 +244,6 @@ export class PublicProcessor implements Traceable {
223
244
  failed.push({ tx, error: new Error(`Tx failed preprocess validation: ${reason}`) });
224
245
  returns.push(new NestedProcessReturnValues([]));
225
246
  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
247
  } else {
232
248
  this.log.trace(`Tx ${txHash.toString()} is valid before processing.`);
233
249
  }
@@ -243,7 +259,9 @@ export class PublicProcessor implements Traceable {
243
259
  this.contractsDB.createCheckpoint();
244
260
 
245
261
  try {
246
- const [processedTx, returnValues, txDebugLogs] = await this.processTx(tx, deadline);
262
+ const [txProcessingTimeMs, [processedTx, returnValues, txDebugLogs]] = await elapsed(() =>
263
+ this.processTx(tx, deadline, signal),
264
+ );
247
265
 
248
266
  // Inject a fake processing failure after N txs if requested
249
267
  const fakeThrowAfter = this.opts.fakeThrowAfterProcessingTxCount;
@@ -252,23 +270,25 @@ export class PublicProcessor implements Traceable {
252
270
  }
253
271
 
254
272
  const txBlobFields = processedTx.txEffect.getNumBlobFields();
255
-
256
- // If the actual size of this tx would exceed block size, skip it
257
273
  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
274
+
275
+ // A single tx's effects must fit within the per-tx blob encoding: the rollup circuit encodes each
276
+ // tx into a fixed [Field; MAX_TX_BLOB_DATA_SIZE_IN_FIELDS] array, so a larger tx effect cannot be
277
+ // proven. The per-category side-effect limits already guarantee this upstream, so reaching here means
278
+ // the tx is malformed; reject it as invalid rather than letting it poison proving.
279
+ if (txBlobFields > MAX_TX_BLOB_DATA_SIZE_IN_FIELDS) {
280
+ const error = new Error(
281
+ `Tx ${txHash} produced ${txBlobFields} blob fields, exceeding the per-tx maximum of ${MAX_TX_BLOB_DATA_SIZE_IN_FIELDS}`,
282
+ );
283
+ this.log.error(error.message, { txHash, txBlobFields });
266
284
  await checkpoint.revert();
267
285
  this.contractsDB.revertCheckpoint();
286
+ failed.push({ tx, error });
287
+ returns.push(new NestedProcessReturnValues([]));
268
288
  continue;
269
289
  }
270
290
 
271
- // If the actual blob fields of this tx would exceed the limit, skip it
291
+ // If the actual blob fields of this tx would exceed the limit, skip it.
272
292
  // Note: maxBlobFields already accounts for block end blob fields and previous blocks in checkpoint.
273
293
  if (maxBlobFields !== undefined && totalBlobFields + txBlobFields > maxBlobFields) {
274
294
  this.log.debug(
@@ -278,14 +298,37 @@ export class PublicProcessor implements Traceable {
278
298
  txBlobFields,
279
299
  totalBlobFields,
280
300
  maxBlobFields,
301
+ txProcessingTimeMs,
281
302
  },
282
303
  );
304
+ silentlySkippedCount += 1;
305
+ totalSilentlySkippedDurationMs += txProcessingTimeMs;
306
+ this.metrics.recordSilentlySkipped(txProcessingTimeMs);
283
307
  // Need to revert the checkpoint here and don't go any further
284
308
  await checkpoint.revert();
285
309
  this.contractsDB.revertCheckpoint();
286
310
  continue;
287
311
  }
288
312
 
313
+ // During re-execution, check if the actual gas used by this tx would push the block over the gas limit.
314
+ // Unlike the proposal-building check (which uses declared gas limits pessimistically before processing),
315
+ // this uses actual gas and stops processing when the limit is exceeded.
316
+ if (
317
+ !isBuildingProposal &&
318
+ maxBlockGas !== undefined &&
319
+ totalBlockGas.add(processedTx.gasUsed.totalGas).gtAny(maxBlockGas)
320
+ ) {
321
+ this.log.warn(`Stopping re-execution since tx ${txHash} would push block gas over limit`, {
322
+ txHash,
323
+ txGas: processedTx.gasUsed.totalGas,
324
+ totalBlockGas,
325
+ maxBlockGas,
326
+ });
327
+ await checkpoint.revert();
328
+ this.contractsDB.revertCheckpoint();
329
+ break;
330
+ }
331
+
289
332
  // FIXME(fcarreiro): it's ugly to have to notify the validator of nullifiers.
290
333
  // I'd rather pass the validators the processedTx as well and let them deal with it.
291
334
  nullifierCache?.addNullifiers(processedTx.txEffect.nullifiers.map(n => n.toBuffer()));
@@ -300,16 +343,15 @@ export class PublicProcessor implements Traceable {
300
343
  totalBlockGas = totalBlockGas.add(processedTx.gasUsed.totalGas);
301
344
  totalSizeInBytes += txSize;
302
345
  totalBlobFields += txBlobFields;
346
+
347
+ // Commit the tx-level contracts checkpoint on success
348
+ this.contractsDB.commitCheckpoint();
303
349
  } 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.
350
+ if (isPublicProcessorInterruptError(err)) {
351
+ const interruptReason = err.name === 'PublicProcessorTimeoutError' ? 'timeout' : 'abort signal';
352
+ this.log.warn(`Stopping tx processing due to ${interruptReason}.`);
353
+ // The tx may still be executing on a worker thread (C++ via NAPI).
354
+ // Signal cancellation AND WAIT for the simulation to actually stop before touching fork checkpoints.
313
355
  await this.publicTxSimulator.cancel?.();
314
356
 
315
357
  // Now stop the guarded fork to prevent any further TS-side access to the world state.
@@ -319,14 +361,10 @@ export class PublicProcessor implements Traceable {
319
361
  // 1. At least one outstanding checkpoint that has not been committed (the one created before we processed the tx).
320
362
  // 2. Possible state updates on that checkpoint or any others created during execution.
321
363
 
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();
364
+ // Revert all checkpoints at or above this checkpoint's depth (inclusive), destroying any outstanding state
365
+ // updates from this tx and any nested checkpoints created during execution. This preserves any checkpoints
366
+ // created by callers below our depth.
367
+ await checkpoint.revertToCheckpoint();
330
368
 
331
369
  // Revert any contracts added to the DB for the tx.
332
370
  this.contractsDB.revertCheckpoint();
@@ -338,9 +376,9 @@ export class PublicProcessor implements Traceable {
338
376
  break;
339
377
  }
340
378
 
341
- // Roll back state to start of TX before proceeding to next TX
342
- await checkpoint.revert();
343
- await this.guardedMerkleTree.getUnderlyingFork().revertAllCheckpoints();
379
+ // Roll back state to start of TX before proceeding to next TX.
380
+ // Reverts all checkpoints at or above this checkpoint's depth, preserving any caller checkpoints below.
381
+ await checkpoint.revertToCheckpoint();
344
382
  this.contractsDB.revertCheckpoint();
345
383
  const errorMessage = err instanceof Error || err instanceof AssertionError ? err.message : 'Unknown error';
346
384
  this.log.warn(`Failed to process tx ${txHash.toString()}: ${errorMessage} ${err?.stack}`);
@@ -352,7 +390,6 @@ export class PublicProcessor implements Traceable {
352
390
  } finally {
353
391
  // Base case is we always commit the checkpoint. Using the ForkCheckpoint means this has no effect if the tx was previously reverted
354
392
  await checkpoint.commit();
355
- this.contractsDB.commitCheckpointOkIfNone();
356
393
  }
357
394
  }
358
395
 
@@ -360,15 +397,26 @@ export class PublicProcessor implements Traceable {
360
397
  const rate = duration > 0 ? totalPublicGas.l2Gas / duration : 0;
361
398
  this.metrics.recordAllTxs(totalPublicGas, rate);
362
399
 
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
- });
400
+ const silentlySkippedDurationMs = Math.round(totalSilentlySkippedDurationMs);
401
+ this.log.info(
402
+ `Processed ${result.length} successful txs and ${failed.length} failed txs ` +
403
+ `(${silentlySkippedCount} silently skipped, ${silentlySkippedDurationMs}ms wasted) ` +
404
+ `in ${duration}s`,
405
+ {
406
+ blockNumber: this.globalVariables.blockNumber,
407
+ successfulCount: result.length,
408
+ failedCount: failed.length,
409
+ duration,
410
+ rate,
411
+ totalPublicGas,
412
+ totalBlockGas,
413
+ totalSizeInBytes,
414
+ silentlySkippedCount,
415
+ silentlySkippedDurationMs,
416
+ },
417
+ );
370
418
 
371
- return [result, failed, usedTxs, returns, totalBlobFields, debugLogs];
419
+ return [result, failed, usedTxs, returns, debugLogs];
372
420
  }
373
421
 
374
422
  private async checkWorldStateUnchanged(
@@ -391,9 +439,10 @@ export class PublicProcessor implements Traceable {
391
439
  private async processTx(
392
440
  tx: Tx,
393
441
  deadline: Date | undefined,
442
+ signal: AbortSignal | undefined,
394
443
  ): Promise<[ProcessedTx, NestedProcessReturnValues[], DebugLog[]]> {
395
444
  const [time, [processedTx, returnValues, debugLogs]] = await elapsed(() =>
396
- this.processTxWithinDeadline(tx, deadline),
445
+ this.processTxWithinDeadline(tx, deadline, signal),
397
446
  );
398
447
 
399
448
  this.log.verbose(
@@ -447,10 +496,11 @@ export class PublicProcessor implements Traceable {
447
496
  this.metrics.recordTreeInsertions(Number(treeInsertionEnd - treeInsertionStart) / 1_000);
448
497
  }
449
498
 
450
- /** Processes the given tx within deadline. Returns timeout if deadline is hit. */
499
+ /** Processes the given tx within deadline or until the signal is aborted. */
451
500
  private async processTxWithinDeadline(
452
501
  tx: Tx,
453
502
  deadline: Date | undefined,
503
+ signal: AbortSignal | undefined,
454
504
  ): Promise<[ProcessedTx, NestedProcessReturnValues[] | undefined, DebugLog[]]> {
455
505
  const innerProcessFn: () => Promise<[ProcessedTx, NestedProcessReturnValues[] | undefined, DebugLog[]]> =
456
506
  tx.hasPublicCalls() ? () => this.processTxWithPublicCalls(tx) : () => this.processPrivateOnlyTx(tx);
@@ -467,27 +517,38 @@ export class PublicProcessor implements Traceable {
467
517
  }
468
518
  : innerProcessFn;
469
519
 
470
- if (!deadline) {
520
+ const processingSignal = this.getProcessingSignal(tx, deadline, signal);
521
+ if (!processingSignal) {
471
522
  return await processFn();
472
523
  }
473
524
 
474
- const txHash = tx.getTxHash();
525
+ return await execWithSignal(
526
+ () => processFn(),
527
+ processingSignal,
528
+ signal =>
529
+ signal.reason?.name === 'TimeoutError' ? new PublicProcessorTimeoutError() : new PublicProcessorAbortError(),
530
+ );
531
+ }
532
+
533
+ private getProcessingSignal(tx: Tx, deadline: Date | undefined, signal: AbortSignal | undefined) {
534
+ if (!deadline) {
535
+ return signal;
536
+ }
537
+
475
538
  const timeout = +deadline - this.dateProvider.now();
476
539
  if (timeout <= 0) {
477
540
  throw new PublicProcessorTimeoutError();
478
541
  }
479
542
 
543
+ const txHash = tx.getTxHash();
480
544
  this.log.debug(`Processing tx ${txHash.toString()} within ${timeout}ms`, {
481
545
  deadline: deadline.toISOString(),
482
546
  now: new Date(this.dateProvider.now()).toISOString(),
483
547
  txHash,
484
548
  });
485
549
 
486
- return await executeTimeout(
487
- () => processFn(),
488
- timeout,
489
- () => new PublicProcessorTimeoutError(),
490
- );
550
+ const timeoutSignal = AbortSignal.timeout(timeout);
551
+ return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
491
552
  }
492
553
 
493
554
  /**
@@ -544,7 +605,7 @@ export class PublicProcessor implements Traceable {
544
605
  // Fee payment insertion has already been done. Do the rest.
545
606
  await this.doTreeInsertionsForPrivateOnlyTx(processedTx);
546
607
 
547
- await this.contractsDB.addNewContracts(tx);
608
+ this.contractsDB.addNewContracts(tx);
548
609
 
549
610
  return [processedTx, undefined, []];
550
611
  }
@@ -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
  }
@@ -23,7 +23,7 @@ export class ContractProviderForCpp implements ContractProvider {
23
23
  public getContractInstance = async (address: string): Promise<Buffer | undefined> => {
24
24
  this.log.trace(`Contract provider callback: getContractInstance(${address})`);
25
25
 
26
- const aztecAddr = AztecAddress.fromString(address);
26
+ const aztecAddr = AztecAddress.fromStringUnsafe(address);
27
27
 
28
28
  const instance = await this.contractsDB.getContractInstance(aztecAddr, this.globalVariables.timestamp);
29
29
 
@@ -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> => {
@@ -87,7 +88,7 @@ export class ContractProviderForCpp implements ContractProvider {
87
88
  this.log.trace(`Contract provider callback: getDebugFunctionName(${address}, ${selector})`);
88
89
 
89
90
  // Parse address and selector strings
90
- const aztecAddr = AztecAddress.fromString(address);
91
+ const aztecAddr = AztecAddress.fromStringUnsafe(address);
91
92
  const selectorFr = Fr.fromString(selector);
92
93
  const functionSelector = FunctionSelector.fromFieldOrUndefined(selectorFr);
93
94
 
@@ -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) {
@@ -402,7 +402,7 @@ export class PublicPersistableStateManager {
402
402
 
403
403
  // This will internally decide whether to check the nullifier tree or not depending on doMerkleOperations.
404
404
  const nullifierExistsInTree = await this.checkNullifierExists(
405
- AztecAddress.fromNumber(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS),
405
+ AztecAddress.fromNumberUnsafe(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS),
406
406
  contractAddress.toField(),
407
407
  );
408
408
  assert(