@aztec/p2p 4.0.0-rc.4 → 4.0.0-rc.6

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 (100) hide show
  1. package/dest/client/factory.d.ts +1 -1
  2. package/dest/client/factory.d.ts.map +1 -1
  3. package/dest/client/factory.js +8 -20
  4. package/dest/client/interface.d.ts +6 -11
  5. package/dest/client/interface.d.ts.map +1 -1
  6. package/dest/client/p2p_client.d.ts +4 -11
  7. package/dest/client/p2p_client.d.ts.map +1 -1
  8. package/dest/client/p2p_client.js +5 -57
  9. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.js +1 -2
  10. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +1 -1
  11. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts +1 -1
  12. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts.map +1 -1
  13. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.js +2 -0
  14. package/dest/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.js +2 -2
  15. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.d.ts +2 -2
  16. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.d.ts.map +1 -1
  17. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.js +10 -6
  18. package/dest/mem_pools/tx_pool_v2/index.d.ts +2 -2
  19. package/dest/mem_pools/tx_pool_v2/index.d.ts.map +1 -1
  20. package/dest/mem_pools/tx_pool_v2/index.js +1 -1
  21. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +5 -5
  22. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  23. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +26 -4
  24. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  25. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +48 -7
  26. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +1 -1
  27. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
  28. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +9 -10
  29. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +2 -2
  30. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  31. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +2 -2
  32. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  33. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +166 -146
  34. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts +2 -2
  35. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts.map +1 -1
  36. package/dest/msg_validators/tx_validator/aggregate_tx_validator.js +3 -3
  37. package/dest/msg_validators/tx_validator/factory.d.ts +114 -6
  38. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  39. package/dest/msg_validators/tx_validator/factory.js +219 -58
  40. package/dest/msg_validators/tx_validator/gas_validator.d.ts +58 -3
  41. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  42. package/dest/msg_validators/tx_validator/gas_validator.js +73 -36
  43. package/dest/msg_validators/tx_validator/index.d.ts +2 -1
  44. package/dest/msg_validators/tx_validator/index.d.ts.map +1 -1
  45. package/dest/msg_validators/tx_validator/index.js +1 -0
  46. package/dest/msg_validators/tx_validator/nullifier_cache.d.ts +14 -0
  47. package/dest/msg_validators/tx_validator/nullifier_cache.d.ts.map +1 -0
  48. package/dest/msg_validators/tx_validator/nullifier_cache.js +24 -0
  49. package/dest/services/dummy_service.d.ts +4 -4
  50. package/dest/services/dummy_service.d.ts.map +1 -1
  51. package/dest/services/dummy_service.js +4 -4
  52. package/dest/services/encoding.d.ts +2 -2
  53. package/dest/services/encoding.d.ts.map +1 -1
  54. package/dest/services/encoding.js +7 -7
  55. package/dest/services/libp2p/libp2p_service.d.ts +11 -7
  56. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  57. package/dest/services/libp2p/libp2p_service.js +60 -70
  58. package/dest/services/reqresp/batch-tx-requester/tx_validator.js +2 -2
  59. package/dest/services/service.d.ts +5 -3
  60. package/dest/services/service.d.ts.map +1 -1
  61. package/dest/services/tx_provider.d.ts +3 -3
  62. package/dest/services/tx_provider.d.ts.map +1 -1
  63. package/dest/services/tx_provider.js +4 -4
  64. package/dest/test-helpers/mock-pubsub.d.ts +3 -2
  65. package/dest/test-helpers/mock-pubsub.d.ts.map +1 -1
  66. package/dest/test-helpers/mock-pubsub.js +6 -0
  67. package/dest/test-helpers/testbench-utils.d.ts +2 -2
  68. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  69. package/dest/util.d.ts +2 -2
  70. package/dest/util.d.ts.map +1 -1
  71. package/package.json +14 -14
  72. package/src/client/factory.ts +13 -33
  73. package/src/client/interface.ts +12 -11
  74. package/src/client/p2p_client.ts +7 -76
  75. package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +1 -1
  76. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +1 -1
  77. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.ts +3 -0
  78. package/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.ts +2 -2
  79. package/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.ts +10 -6
  80. package/src/mem_pools/tx_pool_v2/index.ts +1 -1
  81. package/src/mem_pools/tx_pool_v2/interfaces.ts +4 -4
  82. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +65 -10
  83. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +11 -11
  84. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +1 -1
  85. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +173 -148
  86. package/src/msg_validators/tx_validator/README.md +115 -0
  87. package/src/msg_validators/tx_validator/aggregate_tx_validator.ts +3 -3
  88. package/src/msg_validators/tx_validator/factory.ts +353 -77
  89. package/src/msg_validators/tx_validator/gas_validator.ts +90 -27
  90. package/src/msg_validators/tx_validator/index.ts +1 -0
  91. package/src/msg_validators/tx_validator/nullifier_cache.ts +30 -0
  92. package/src/services/dummy_service.ts +6 -6
  93. package/src/services/encoding.ts +5 -6
  94. package/src/services/libp2p/libp2p_service.ts +74 -79
  95. package/src/services/reqresp/batch-tx-requester/tx_validator.ts +2 -2
  96. package/src/services/service.ts +11 -2
  97. package/src/services/tx_provider.ts +2 -2
  98. package/src/test-helpers/mock-pubsub.ts +10 -0
  99. package/src/test-helpers/testbench-utils.ts +1 -1
  100. package/src/util.ts +7 -1
@@ -187,6 +187,30 @@ export class TxPoolV2Impl {
187
187
  const errors = new Map<string, TxPoolRejectionError>();
188
188
  const acceptedPending = new Set<string>();
189
189
 
190
+ // Phase 1: Pre-compute all throwable I/O outside the transaction.
191
+ // If any pre-computation throws, the entire call fails before mutations happen.
192
+ const precomputed = new Map<string, { meta: TxMetaData; minedBlockId: L2BlockId | undefined; isValid: boolean }>();
193
+
194
+ const validator = await this.#createTxValidator();
195
+
196
+ for (const tx of txs) {
197
+ const txHash = tx.getTxHash();
198
+ const txHashStr = txHash.toString();
199
+
200
+ const meta = await buildTxMetaData(tx);
201
+ const minedBlockId = await this.#getMinedBlockId(txHash);
202
+
203
+ // Validate non-mined txs (mined and pre-protected txs bypass validation inside the transaction)
204
+ let isValid = true;
205
+ if (!minedBlockId) {
206
+ isValid = await this.#validateMeta(meta, validator);
207
+ }
208
+
209
+ precomputed.set(txHashStr, { meta, minedBlockId, isValid });
210
+ }
211
+
212
+ // Phase 2: Apply mutations inside the transaction using only pre-computed results,
213
+ // in-memory reads, and buffered DB writes. Nothing here can throw an unhandled exception.
190
214
  const poolAccess = this.#createPreAddPoolAccess();
191
215
  const preAddContext: PreAddContext | undefined =
192
216
  opts.feeComparisonOnly !== undefined ? { feeComparisonOnly: opts.feeComparisonOnly } : undefined;
@@ -202,22 +226,25 @@ export class TxPoolV2Impl {
202
226
  continue;
203
227
  }
204
228
 
205
- // Check mined status first (applies to all paths)
206
- const minedBlockId = await this.#getMinedBlockId(txHash);
229
+ const { meta, minedBlockId, isValid } = precomputed.get(txHashStr)!;
207
230
  const preProtectedSlot = this.#indices.getProtectionSlot(txHashStr);
208
231
 
209
232
  if (minedBlockId) {
210
233
  // Already mined - add directly (protection already set if pre-protected)
211
- await this.#addTx(tx, { mined: minedBlockId }, opts);
234
+ await this.#addTx(tx, { mined: minedBlockId }, opts, meta);
212
235
  accepted.push(txHash);
213
236
  } else if (preProtectedSlot !== undefined) {
214
237
  // Pre-protected and not mined - add as protected (bypass validation)
215
- await this.#addTx(tx, { protected: preProtectedSlot }, opts);
238
+ await this.#addTx(tx, { protected: preProtectedSlot }, opts, meta);
216
239
  accepted.push(txHash);
240
+ } else if (!isValid) {
241
+ // Failed pre-computed validation
242
+ rejected.push(txHash);
217
243
  } else {
218
- // Regular pending tx - validate and run pre-add rules
244
+ // Regular pending tx - run pre-add rules using pre-computed metadata
219
245
  const result = await this.#tryAddRegularPendingTx(
220
246
  tx,
247
+ meta,
221
248
  opts,
222
249
  poolAccess,
223
250
  acceptedPending,
@@ -227,13 +254,18 @@ export class TxPoolV2Impl {
227
254
  );
228
255
  if (result.status === 'accepted') {
229
256
  acceptedPending.add(txHashStr);
230
- } else if (result.status === 'rejected') {
231
- rejected.push(txHash);
232
257
  } else {
233
258
  ignored.push(txHash);
234
259
  }
235
260
  }
236
261
  }
262
+
263
+ // Run post-add eviction rules for pending txs (inside transaction for atomicity)
264
+ if (acceptedPending.size > 0) {
265
+ const feePayers = Array.from(acceptedPending).map(txHash => this.#indices.getMetadata(txHash)!.feePayer);
266
+ const uniqueFeePayers = new Set<string>(feePayers);
267
+ await this.#evictionManager.evictAfterNewTxs(Array.from(acceptedPending), [...uniqueFeePayers]);
268
+ }
237
269
  });
238
270
 
239
271
  // Build final accepted list for pending txs (excludes intra-batch evictions)
@@ -249,37 +281,24 @@ export class TxPoolV2Impl {
249
281
  this.#instrumentation.recordRejected(rejected.length);
250
282
  }
251
283
 
252
- // Run post-add eviction rules for pending txs
253
- if (acceptedPending.size > 0) {
254
- const feePayers = Array.from(acceptedPending).map(txHash => this.#indices.getMetadata(txHash)!.feePayer);
255
- const uniqueFeePayers = new Set<string>(feePayers);
256
- await this.#evictionManager.evictAfterNewTxs(Array.from(acceptedPending), [...uniqueFeePayers]);
257
- }
258
-
259
284
  return { accepted, ignored, rejected, ...(errors.size > 0 ? { errors } : {}) };
260
285
  }
261
286
 
262
- /** Validates and adds a regular pending tx. Returns status. */
287
+ /** Adds a validated pending tx, running pre-add rules and evicting conflicts. */
263
288
  async #tryAddRegularPendingTx(
264
289
  tx: Tx,
290
+ precomputedMeta: TxMetaData,
265
291
  opts: { source?: string },
266
292
  poolAccess: PreAddPoolAccess,
267
293
  acceptedPending: Set<string>,
268
294
  ignored: TxHash[],
269
295
  errors: Map<string, TxPoolRejectionError>,
270
296
  preAddContext?: PreAddContext,
271
- ): Promise<{ status: 'accepted' | 'ignored' | 'rejected' }> {
272
- const txHash = tx.getTxHash();
273
- const txHashStr = txHash.toString();
274
-
275
- // Build metadata and validate using metadata
276
- const meta = await buildTxMetaData(tx);
277
- if (!(await this.#validateMeta(meta))) {
278
- return { status: 'rejected' };
279
- }
297
+ ): Promise<{ status: 'accepted' | 'ignored' }> {
298
+ const txHashStr = tx.getTxHash().toString();
280
299
 
281
300
  // Run pre-add rules
282
- const preAddResult = await this.#evictionManager.runPreAddRules(meta, poolAccess, preAddContext);
301
+ const preAddResult = await this.#evictionManager.runPreAddRules(precomputedMeta, poolAccess, preAddContext);
283
302
 
284
303
  if (preAddResult.shouldIgnore) {
285
304
  this.#log.debug(`Ignoring tx ${txHashStr}: ${preAddResult.reason?.message ?? 'unknown reason'}`);
@@ -317,11 +336,11 @@ export class TxPoolV2Impl {
317
336
  }
318
337
 
319
338
  // Add the transaction
320
- await this.#addTx(tx, 'pending', opts);
339
+ await this.#addTx(tx, 'pending', opts, precomputedMeta);
321
340
  return { status: 'accepted' };
322
341
  }
323
342
 
324
- async canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored' | 'rejected'> {
343
+ async canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored'> {
325
344
  const txHashStr = tx.getTxHash().toString();
326
345
 
327
346
  // Check if already in pool
@@ -329,14 +348,8 @@ export class TxPoolV2Impl {
329
348
  return 'ignored';
330
349
  }
331
350
 
332
- // Build metadata and validate using metadata
351
+ // Build metadata and check pre-add rules
333
352
  const meta = await buildTxMetaData(tx);
334
- const validationResult = await this.#validateMeta(meta, undefined, 'can add pending');
335
- if (validationResult !== true) {
336
- return 'rejected';
337
- }
338
-
339
- // Use pre-add rules
340
353
  const poolAccess = this.#createPreAddPoolAccess();
341
354
  const preAddResult = await this.#evictionManager.runPreAddRules(meta, poolAccess);
342
355
 
@@ -379,33 +392,35 @@ export class TxPoolV2Impl {
379
392
  let softDeletedHits = 0;
380
393
  let missingPreviouslyEvicted = 0;
381
394
 
382
- for (const txHash of txHashes) {
383
- const txHashStr = txHash.toString();
395
+ await this.#store.transactionAsync(async () => {
396
+ for (const txHash of txHashes) {
397
+ const txHashStr = txHash.toString();
384
398
 
385
- if (this.#indices.has(txHashStr)) {
386
- // Update protection for existing tx
387
- this.#indices.updateProtection(txHashStr, slotNumber);
388
- } else if (this.#deletedPool.isSoftDeleted(txHashStr)) {
389
- // Resurrect soft-deleted tx as protected
390
- const buffer = await this.#txsDB.getAsync(txHashStr);
391
- if (buffer) {
392
- const tx = Tx.fromBuffer(buffer);
393
- await this.#addTx(tx, { protected: slotNumber });
394
- softDeletedHits++;
399
+ if (this.#indices.has(txHashStr)) {
400
+ // Update protection for existing tx
401
+ this.#indices.updateProtection(txHashStr, slotNumber);
402
+ } else if (this.#deletedPool.isSoftDeleted(txHashStr)) {
403
+ // Resurrect soft-deleted tx as protected
404
+ const buffer = await this.#txsDB.getAsync(txHashStr);
405
+ if (buffer) {
406
+ const tx = Tx.fromBuffer(buffer);
407
+ await this.#addTx(tx, { protected: slotNumber });
408
+ softDeletedHits++;
409
+ } else {
410
+ // Data missing despite soft-delete flag — treat as truly missing
411
+ this.#indices.setProtection(txHashStr, slotNumber);
412
+ missing.push(txHash);
413
+ }
395
414
  } else {
396
- // Data missing despite soft-delete flag treat as truly missing
415
+ // Truly missing pre-record protection for tx we don't have yet
397
416
  this.#indices.setProtection(txHashStr, slotNumber);
398
417
  missing.push(txHash);
399
- }
400
- } else {
401
- // Truly missing — pre-record protection for tx we don't have yet
402
- this.#indices.setProtection(txHashStr, slotNumber);
403
- missing.push(txHash);
404
- if (this.#evictedTxHashes.has(txHashStr)) {
405
- missingPreviouslyEvicted++;
418
+ if (this.#evictedTxHashes.has(txHashStr)) {
419
+ missingPreviouslyEvicted++;
420
+ }
406
421
  }
407
422
  }
408
- }
423
+ });
409
424
 
410
425
  // Record metrics
411
426
  if (softDeletedHits > 0) {
@@ -466,56 +481,60 @@ export class TxPoolV2Impl {
466
481
  }
467
482
  }
468
483
 
469
- // Step 4: Mark txs as mined (only those we have in the pool)
470
- for (const meta of found) {
471
- this.#indices.markAsMined(meta, blockId);
472
- await this.#deletedPool.clearIfMinedHigher(meta.txHash, blockId.number);
473
- }
484
+ await this.#store.transactionAsync(async () => {
485
+ // Step 4: Mark txs as mined (only those we have in the pool)
486
+ for (const meta of found) {
487
+ this.#indices.markAsMined(meta, blockId);
488
+ await this.#deletedPool.clearIfMinedHigher(meta.txHash, blockId.number);
489
+ }
474
490
 
475
- // Step 5: Run eviction rules (remove pending txs with conflicting nullifiers/expired timestamps)
476
- await this.#evictionManager.evictAfterNewBlock(block.header, nullifiers, feePayers);
491
+ // Step 5: Run post-event eviction rules (inside transaction for atomicity)
492
+ await this.#evictionManager.evictAfterNewBlock(block.header, nullifiers, feePayers);
493
+ });
477
494
 
478
495
  this.#log.info(`Marked ${found.length} txs as mined in block ${blockId.number}`);
479
496
  }
480
497
 
481
498
  async prepareForSlot(slotNumber: SlotNumber): Promise<void> {
482
- // Step 0: Clean up slot-deleted txs from previous slots
483
- await this.#deletedPool.cleanupSlotDeleted(slotNumber);
499
+ await this.#store.transactionAsync(async () => {
500
+ // Step 0: Clean up slot-deleted txs from previous slots
501
+ await this.#deletedPool.cleanupSlotDeleted(slotNumber);
484
502
 
485
- // Step 1: Find expired protected txs
486
- const expiredProtected = this.#indices.findExpiredProtectedTxs(slotNumber);
503
+ // Step 1: Find expired protected txs
504
+ const expiredProtected = this.#indices.findExpiredProtectedTxs(slotNumber);
487
505
 
488
- // Step 2: Clear protection for all expired entries (including those without metadata)
489
- this.#indices.clearProtection(expiredProtected);
506
+ // Step 2: Clear protection for all expired entries (including those without metadata)
507
+ this.#indices.clearProtection(expiredProtected);
490
508
 
491
- // Step 3: Filter to only txs that have metadata and are not mined
492
- const txsToRestore = this.#indices.filterRestorable(expiredProtected);
493
- if (txsToRestore.length === 0) {
494
- this.#log.debug(`Preparing for slot ${slotNumber}, no txs to unprotect`);
495
- return;
496
- }
509
+ // Step 3: Filter to only txs that have metadata and are not mined
510
+ const txsToRestore = this.#indices.filterRestorable(expiredProtected);
511
+ if (txsToRestore.length === 0) {
512
+ this.#log.debug(`Preparing for slot ${slotNumber}, no txs to unprotect`);
513
+ return;
514
+ }
497
515
 
498
- this.#log.info(`Preparing for slot ${slotNumber}: unprotecting ${txsToRestore.length} txs`);
516
+ this.#log.info(`Preparing for slot ${slotNumber}: unprotecting ${txsToRestore.length} txs`);
499
517
 
500
- // Step 4: Validate for pending pool
501
- const { valid, invalid } = await this.#revalidateMetadata(txsToRestore, 'during prepareForSlot');
518
+ // Step 4: Validate for pending pool
519
+ const { valid, invalid } = await this.#revalidateMetadata(txsToRestore, 'during prepareForSlot');
502
520
 
503
- // Step 5: Resolve nullifier conflicts and add winners to pending indices
504
- const { added, toEvict } = this.#applyNullifierConflictResolution(valid);
521
+ // Step 5: Resolve nullifier conflicts and add winners to pending indices
522
+ const { added, toEvict } = this.#applyNullifierConflictResolution(valid);
505
523
 
506
- // Step 6: Delete invalid txs and evict conflict losers
507
- await this.#deleteTxsBatch(invalid);
508
- await this.#evictTxs(toEvict, 'NullifierConflict');
524
+ // Step 6: Delete invalid txs and evict conflict losers
525
+ await this.#deleteTxsBatch(invalid);
526
+ await this.#evictTxs(toEvict, 'NullifierConflict');
509
527
 
510
- // Step 7: Run eviction rules (enforce pool size limit)
511
- if (added.length > 0) {
512
- const feePayers = added.map(meta => meta.feePayer);
513
- const uniqueFeePayers = new Set<string>(feePayers);
514
- await this.#evictionManager.evictAfterNewTxs(
515
- added.map(m => m.txHash),
516
- [...uniqueFeePayers],
517
- );
518
- }
528
+ // Step 7: Run eviction rules (enforce pool size limit)
529
+ if (added.length > 0) {
530
+ const feePayers = added.map(meta => meta.feePayer);
531
+ const uniqueFeePayers = new Set<string>(feePayers);
532
+ await this.#evictionManager.evictAfterNewTxs(
533
+ added.map(m => m.txHash),
534
+ [...uniqueFeePayers],
535
+ );
536
+ }
537
+ });
519
538
  }
520
539
 
521
540
  async handlePrunedBlocks(latestBlock: L2BlockId, options?: { deleteAllTxs?: boolean }): Promise<void> {
@@ -528,57 +547,60 @@ export class TxPoolV2Impl {
528
547
 
529
548
  this.#log.info(`Handling prune to block ${latestBlock.number}: un-mining ${txsToUnmine.length} txs`);
530
549
 
531
- // Step 2: Mark ALL un-mined txs with their original mined block number
532
- // This ensures they get soft-deleted if removed later, and only hard-deleted
533
- // when their original mined block is finalized
534
- await this.#deletedPool.markFromPrunedBlock(
535
- txsToUnmine.map(m => ({
536
- txHash: m.txHash,
537
- minedAtBlock: BlockNumber(m.minedL2BlockId!.number),
538
- })),
539
- );
550
+ await this.#store.transactionAsync(async () => {
551
+ // Step 2: Mark ALL un-mined txs with their original mined block number
552
+ // This ensures they get soft-deleted if removed later, and only hard-deleted
553
+ // when their original mined block is finalized
554
+ await this.#deletedPool.markFromPrunedBlock(
555
+ txsToUnmine.map(m => ({
556
+ txHash: m.txHash,
557
+ minedAtBlock: BlockNumber(m.minedL2BlockId!.number),
558
+ })),
559
+ );
540
560
 
541
- // Step 3: Unmine - clear mined status from metadata
542
- for (const meta of txsToUnmine) {
543
- this.#indices.markAsUnmined(meta);
544
- }
561
+ // Step 3: Unmine - clear mined status from metadata
562
+ for (const meta of txsToUnmine) {
563
+ this.#indices.markAsUnmined(meta);
564
+ }
545
565
 
546
- // If deleteAllTxs is set (epoch prune), delete all un-mined txs and return early
547
- if (options?.deleteAllTxs) {
548
- const allTxHashes = txsToUnmine.map(m => m.txHash);
549
- await this.#deleteTxsBatch(allTxHashes);
550
- this.#log.info(
551
- `Handled prune to block ${latestBlock.number} with deleteAllTxs: deleted ${allTxHashes.length} txs`,
552
- );
553
- return;
554
- }
566
+ // If deleteAllTxs is set (epoch prune), delete all un-mined txs and return early
567
+ if (options?.deleteAllTxs) {
568
+ const allTxHashes = txsToUnmine.map(m => m.txHash);
569
+ await this.#deleteTxsBatch(allTxHashes);
570
+ this.#log.info(
571
+ `Handled prune to block ${latestBlock.number} with deleteAllTxs: deleted ${allTxHashes.length} txs`,
572
+ );
573
+ return;
574
+ }
555
575
 
556
- // Step 4: Filter out protected txs (they'll be handled by prepareForSlot)
557
- const unprotectedTxs = this.#indices.filterUnprotected(txsToUnmine);
576
+ // Step 4: Filter out protected txs (they'll be handled by prepareForSlot)
577
+ const unprotectedTxs = this.#indices.filterUnprotected(txsToUnmine);
558
578
 
559
- // Step 5: Validate for pending pool
560
- const { valid, invalid } = await this.#revalidateMetadata(unprotectedTxs, 'during handlePrunedBlocks');
579
+ // Step 5: Validate for pending pool
580
+ const { valid, invalid } = await this.#revalidateMetadata(unprotectedTxs, 'during handlePrunedBlocks');
561
581
 
562
- // Step 6: Resolve nullifier conflicts and add winners to pending indices
563
- const { toEvict } = this.#applyNullifierConflictResolution(valid);
582
+ // Step 6: Resolve nullifier conflicts and add winners to pending indices
583
+ const { toEvict } = this.#applyNullifierConflictResolution(valid);
564
584
 
565
- // Step 7: Delete invalid txs and evict conflict losers
566
- await this.#deleteTxsBatch(invalid);
567
- await this.#evictTxs(toEvict, 'NullifierConflict');
585
+ // Step 7: Delete invalid txs and evict conflict losers
586
+ await this.#deleteTxsBatch(invalid);
587
+ await this.#evictTxs(toEvict, 'NullifierConflict');
568
588
 
569
- this.#log.info(
570
- `Handled prune to block ${latestBlock.number}: ${valid.length} txs restored to pending, ${invalid.length} invalid, ${toEvict.length} evicted due to nullifier conflicts`,
571
- { txHashesRestored: valid.map(m => m.txHash), txHashesInvalid: invalid, txHashesEvicted: toEvict },
572
- );
589
+ this.#log.info(
590
+ `Handled prune to block ${latestBlock.number}: ${valid.length} txs restored to pending, ${invalid.length} invalid, ${toEvict.length} evicted due to nullifier conflicts`,
591
+ { txHashesRestored: valid.map(m => m.txHash), txHashesInvalid: invalid, txHashesEvicted: toEvict },
592
+ );
573
593
 
574
- // Step 8: Run eviction rules for ALL pending txs (not just restored ones)
575
- // This handles cases like existing pending txs with invalid fee payer balances
576
- await this.#evictionManager.evictAfterChainPrune(latestBlock.number);
594
+ // Step 8: Run eviction rules for ALL pending txs (not just restored ones)
595
+ // This handles cases like existing pending txs with invalid fee payer balances
596
+ await this.#evictionManager.evictAfterChainPrune(latestBlock.number);
597
+ });
577
598
  }
578
599
 
579
600
  async handleFailedExecution(txHashes: TxHash[]): Promise<void> {
580
- // Delete failed txs
581
- await this.#deleteTxsBatch(txHashes.map(h => h.toString()));
601
+ await this.#store.transactionAsync(async () => {
602
+ await this.#deleteTxsBatch(txHashes.map(h => h.toString()));
603
+ });
582
604
 
583
605
  this.#log.info(`Deleted ${txHashes.length} failed txs`, { txHashes: txHashes.map(h => h.toString()) });
584
606
  }
@@ -589,27 +611,29 @@ export class TxPoolV2Impl {
589
611
  // Step 1: Find mined txs at or before finalized block
590
612
  const minedTxsToFinalize = this.#indices.findTxsMinedAtOrBefore(blockNumber);
591
613
 
592
- // Step 2: Collect mined txs for archiving (before deletion)
593
- const txsToArchive: Tx[] = [];
594
- if (this.#archive.isEnabled()) {
595
- for (const txHashStr of minedTxsToFinalize) {
596
- const buffer = await this.#txsDB.getAsync(txHashStr);
597
- if (buffer) {
598
- txsToArchive.push(Tx.fromBuffer(buffer));
614
+ await this.#store.transactionAsync(async () => {
615
+ // Step 2: Collect mined txs for archiving (before deletion)
616
+ const txsToArchive: Tx[] = [];
617
+ if (this.#archive.isEnabled()) {
618
+ for (const txHashStr of minedTxsToFinalize) {
619
+ const buffer = await this.#txsDB.getAsync(txHashStr);
620
+ if (buffer) {
621
+ txsToArchive.push(Tx.fromBuffer(buffer));
622
+ }
599
623
  }
600
624
  }
601
- }
602
625
 
603
- // Step 3: Delete mined txs from active pool
604
- await this.#deleteTxsBatch(minedTxsToFinalize);
626
+ // Step 3: Delete mined txs from active pool
627
+ await this.#deleteTxsBatch(minedTxsToFinalize);
605
628
 
606
- // Step 4: Finalize soft-deleted txs
607
- await this.#deletedPool.finalizeBlock(blockNumber);
629
+ // Step 4: Finalize soft-deleted txs
630
+ await this.#deletedPool.finalizeBlock(blockNumber);
608
631
 
609
- // Step 5: Archive mined txs
610
- if (txsToArchive.length > 0) {
611
- await this.#archive.archiveTxs(txsToArchive);
612
- }
632
+ // Step 5: Archive mined txs
633
+ if (txsToArchive.length > 0) {
634
+ await this.#archive.archiveTxs(txsToArchive);
635
+ }
636
+ });
613
637
 
614
638
  if (minedTxsToFinalize.length > 0) {
615
639
  this.#log.info(`Finalized ${minedTxsToFinalize.length} mined txs from blocks up to ${blockNumber}`, {
@@ -754,9 +778,10 @@ export class TxPoolV2Impl {
754
778
  tx: Tx,
755
779
  state: 'pending' | { protected: SlotNumber } | { mined: L2BlockId },
756
780
  opts: { source?: string } = {},
781
+ precomputedMeta?: TxMetaData,
757
782
  ): Promise<TxMetaData> {
758
783
  const txHashStr = tx.getTxHash().toString();
759
- const meta = await buildTxMetaData(tx);
784
+ const meta = precomputedMeta ?? (await buildTxMetaData(tx));
760
785
  meta.receivedAt = this.#dateProvider.now();
761
786
 
762
787
  await this.#txsDB.set(txHashStr, tx.toBuffer());
@@ -0,0 +1,115 @@
1
+ # Transaction Validation
2
+
3
+ This module defines the transaction validators and the factory functions that assemble them for each entry point into the system.
4
+
5
+ ## Validation Strategy
6
+
7
+ Transactions enter the system through different paths. **Unsolicited** transactions (gossip and RPC) are fully validated before acceptance. **Solicited** transactions (req/resp and block proposals) are only checked for well-formedness because we must store them for block re-execution — they may ultimately be invalid, which is caught during block building and reported as part of block validation/attestation.
8
+
9
+ When solicited transactions fail to be mined, they may be migrated to the pending pool. At that point, the pool runs the state-dependent checks that were skipped on initial receipt.
10
+
11
+ ## Entry Points
12
+
13
+ ### 1. Gossip (libp2p pubsub)
14
+
15
+ **Factory**: `createFirstStageTxValidationsForGossipedTransactions` + `createSecondStageTxValidationsForGossipedTransactions`
16
+ **Called from**: `LibP2PService.handleGossipedTx()` in `libp2p_service.ts`
17
+
18
+ Unsolicited transactions from any peer. Fully validated in two stages with a pool pre-check in between to avoid wasting CPU on proof verification for transactions the pool would reject:
19
+
20
+ | Step | What runs | On failure |
21
+ |------|-----------|------------|
22
+ | **Stage 1** (fast) | TxPermitted, Data, Metadata, Timestamp, DoubleSpend, Gas, Phases, BlockHeader | Penalize peer, reject tx |
23
+ | **Pool pre-check** | `canAddPendingTx` — checks for duplicates, pool capacity | Ignore tx (no penalty) |
24
+ | **Stage 2** (slow) | Proof verification | Penalize peer, reject tx |
25
+ | **Pool add** | `addPendingTxs` | Accept, ignore, or reject |
26
+
27
+ Each stage-1 and stage-2 validator is paired with a `PeerErrorSeverity`. If a validator fails, the sending peer is penalized with that severity. The `doubleSpendValidator` has special handling: its severity is determined by how recently the nullifier appeared (recent = high tolerance, old = low tolerance).
28
+
29
+ ### 2. JSON-RPC
30
+
31
+ **Factory**: `createTxValidatorForAcceptingTxsOverRPC`
32
+ **Called from**: `AztecNodeService.isValidTx()` in `aztec-node/server.ts`
33
+
34
+ Unsolicited transactions from a local wallet/PXE. Runs the full set of checks as a single aggregate validator:
35
+
36
+ - TxPermitted, Size, Data, Metadata, Timestamp, DoubleSpend, Phases, BlockHeader
37
+ - Gas (optional — skipped when `skipFeeEnforcement` is set)
38
+ - Proof verification (optional — skipped for simulations when no verifier is provided)
39
+
40
+ ### 3. Req/resp and block proposals
41
+
42
+ **Factories**: `createTxValidatorForReqResponseReceivedTxs`, `createTxValidatorForBlockProposalReceivedTxs`
43
+ **Called from**: `LibP2PService.validateRequestedTx()`, `LibP2PService.validateTxsReceivedInBlockProposal()`, and `BatchRequestTxValidator` in `batch-tx-requester/tx_validator.ts`
44
+
45
+ Solicited transactions — we requested these from peers or received them as part of a block proposal we need to validate. We must accept them for re-execution even if they are invalid against the current state. Only well-formedness is checked:
46
+
47
+ - Metadata, Size, Data, Proof
48
+
49
+ State-dependent checks are deferred to either the block building validator (for txs included in blocks) or the pending pool migration validator (for unmined txs migrating to pending).
50
+
51
+ ### 4. Block building
52
+
53
+ **Factory**: `createTxValidatorForBlockBuilding`
54
+ **Called from**: `CheckpointBuilder.makeBlockBuilderDeps()` in `validator-client/checkpoint_builder.ts`
55
+
56
+ Transactions already in the pool, about to be sequenced into a block. Re-validates against the current state of the block being built. **This is where invalid txs that entered via req/resp or block proposals are caught** — their invalidity is reported as part of block validation/attestation.
57
+
58
+ Runs:
59
+ - Timestamp, DoubleSpend, Phases, Gas, BlockHeader
60
+
61
+ Does **not** run:
62
+ - Proof, Data — already verified on entry (by gossip, RPC, or req/resp validators)
63
+
64
+ ### 5. Pending pool migration
65
+
66
+ **Factory**: `createTxValidatorForTransactionsEnteringPendingTxPool`
67
+ **Called from**: `TxPoolV2Impl` (injected as the `createTxValidator` factory via `TxPoolV2Dependencies`)
68
+
69
+ When transactions that arrived via req/resp or block proposals fail to be mined, they may need to be included in our pending pool. These txs only had well-formedness checks on receipt, so the pool runs the state-dependent checks they missed before accepting them.
70
+
71
+ This validator is invoked on **every** transaction potentially entering the pending pool:
72
+ - `addPendingTxs` — validating each tx before adding
73
+ - `prepareForSlot` — unprotecting txs back to pending after a slot ends
74
+ - `handlePrunedBlocks` — unmining txs from pruned blocks back to pending
75
+ - Startup hydration — revalidating persisted non-mined txs on node restart
76
+
77
+ Runs:
78
+ - DoubleSpend, BlockHeader, GasLimits, Timestamp
79
+
80
+ Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects.
81
+
82
+ ## Individual Validators
83
+
84
+ | Validator | What it checks | Benchmarked verification duration |
85
+ |-----------|---------------|---------------|
86
+ | `TxPermittedValidator` | Whether the system is accepting transactions (controlled by config flag) | 1.56 us |
87
+ | `DataTxValidator` | Transaction data integrity — correct structure, non-empty fields | 4.10–18.18 ms |
88
+ | `SizeTxValidator` | Transaction does not exceed maximum size limits | 2.28 us |
89
+ | `MetadataTxValidator` | Chain ID, rollup version, protocol contracts hash, VK tree root | 4.18 us |
90
+ | `TimestampTxValidator` | Transaction has not expired (expiration timestamp vs next slot) | 1.56 us |
91
+ | `DoubleSpendTxValidator` | Nullifiers do not already exist in the nullifier tree | 106.08 us |
92
+ | `GasTxValidator` | Gas limits are within bounds (delegates to `GasLimitsValidator`), max fee per gas meets current block fees, and fee payer has sufficient FeeJuice balance | 1.02 ms |
93
+ | `GasLimitsValidator` | Gas limits are >= fixed minimums and <= AVM max processable L2 gas. Used standalone in pool migration; also called internally by `GasTxValidator` | 3–10 us |
94
+ | `PhasesTxValidator` | Public function calls in setup phase are on the allow list | 10.12–13.12 us |
95
+ | `BlockHeaderTxValidator` | Transaction's anchor block hash exists in the archive tree | 98.88 us |
96
+ | `TxProofValidator` | Client proof verifies correctly | ~250ms |
97
+
98
+ ## Validator Coverage by Entry Point
99
+
100
+ | Validator | Gossip | RPC | Req/resp | Block building | Pool migration |
101
+ |-----------|--------|-----|----------|----------------|----------------|
102
+ | TxPermitted | Stage 1 | Yes | — | — | — |
103
+ | Data | Stage 1 | Yes | Yes | — | — |
104
+ | Size | — | Yes | Yes | — | — |
105
+ | Metadata | Stage 1 | Yes | Yes | — | — |
106
+ | Timestamp | Stage 1 | Yes | — | Yes | Yes |
107
+ | DoubleSpend | Stage 1 | Yes | — | Yes | Yes |
108
+ | Gas (balance + limits) | Stage 1 | Optional* | — | Yes | — |
109
+ | GasLimits (standalone) | — | — | — | — | Yes |
110
+ | Phases | Stage 1 | Yes | — | Yes | — |
111
+ | BlockHeader | Stage 1 | Yes | — | Yes | Yes |
112
+ | Proof | Stage 2 | Optional** | Yes | — | — |
113
+
114
+ \* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `GasLimitsValidator` as its first step, so gas limits are checked wherever `GasTxValidator` runs. Pool migration uses `GasLimitsValidator` standalone because it doesn't need the balance or fee-per-gas checks.
115
+ \** Proof verification is skipped for simulations (no verifier provided).
@@ -1,18 +1,18 @@
1
1
  import type { TxValidationResult, TxValidator } from '@aztec/stdlib/tx';
2
2
 
3
3
  export class AggregateTxValidator<T> implements TxValidator<T> {
4
- #validators: TxValidator<T>[];
4
+ readonly validators: TxValidator<T>[];
5
5
  constructor(...validators: TxValidator<T>[]) {
6
6
  if (validators.length === 0) {
7
7
  throw new Error('At least one validator must be provided');
8
8
  }
9
9
 
10
- this.#validators = validators;
10
+ this.validators = validators;
11
11
  }
12
12
 
13
13
  async validateTx(tx: T): Promise<TxValidationResult> {
14
14
  const aggregate: { result: string; reason?: string[] } = { result: 'valid', reason: [] };
15
- for (const validator of this.#validators) {
15
+ for (const validator of this.validators) {
16
16
  const result = await validator.validateTx(tx);
17
17
  if (result.result === 'invalid') {
18
18
  aggregate.result = 'invalid';