@aztec/p2p 0.0.1-commit.808bf7f90 → 0.0.1-commit.8227e42

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 (75) 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 +3 -10
  5. package/dest/client/interface.d.ts.map +1 -1
  6. package/dest/client/p2p_client.d.ts +2 -10
  7. package/dest/client/p2p_client.d.ts.map +1 -1
  8. package/dest/client/p2p_client.js +2 -57
  9. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +5 -5
  10. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  11. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +9 -1
  12. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  13. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +12 -0
  14. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +2 -2
  15. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  16. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +2 -2
  17. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  18. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +1 -6
  19. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts +2 -2
  20. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts.map +1 -1
  21. package/dest/msg_validators/tx_validator/aggregate_tx_validator.js +3 -3
  22. package/dest/msg_validators/tx_validator/factory.d.ts +114 -6
  23. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  24. package/dest/msg_validators/tx_validator/factory.js +219 -58
  25. package/dest/msg_validators/tx_validator/gas_validator.d.ts +58 -3
  26. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  27. package/dest/msg_validators/tx_validator/gas_validator.js +72 -35
  28. package/dest/msg_validators/tx_validator/index.d.ts +2 -1
  29. package/dest/msg_validators/tx_validator/index.d.ts.map +1 -1
  30. package/dest/msg_validators/tx_validator/index.js +1 -0
  31. package/dest/msg_validators/tx_validator/nullifier_cache.d.ts +14 -0
  32. package/dest/msg_validators/tx_validator/nullifier_cache.d.ts.map +1 -0
  33. package/dest/msg_validators/tx_validator/nullifier_cache.js +24 -0
  34. package/dest/services/dummy_service.d.ts +2 -3
  35. package/dest/services/dummy_service.d.ts.map +1 -1
  36. package/dest/services/dummy_service.js +1 -4
  37. package/dest/services/libp2p/libp2p_service.d.ts +10 -7
  38. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  39. package/dest/services/libp2p/libp2p_service.js +57 -70
  40. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts +1 -1
  41. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts.map +1 -1
  42. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.js +14 -37
  43. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts +17 -11
  44. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts.map +1 -1
  45. package/dest/services/reqresp/batch-tx-requester/peer_collection.js +49 -15
  46. package/dest/services/reqresp/batch-tx-requester/tx_validator.js +2 -2
  47. package/dest/services/service.d.ts +2 -2
  48. package/dest/services/service.d.ts.map +1 -1
  49. package/dest/services/tx_provider.d.ts +3 -3
  50. package/dest/services/tx_provider.d.ts.map +1 -1
  51. package/dest/services/tx_provider.js +4 -4
  52. package/dest/test-helpers/testbench-utils.d.ts +2 -2
  53. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  54. package/package.json +14 -14
  55. package/src/client/factory.ts +13 -33
  56. package/src/client/interface.ts +2 -10
  57. package/src/client/p2p_client.ts +2 -76
  58. package/src/mem_pools/tx_pool_v2/interfaces.ts +4 -4
  59. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +13 -0
  60. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +1 -1
  61. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +2 -8
  62. package/src/msg_validators/tx_validator/README.md +115 -0
  63. package/src/msg_validators/tx_validator/aggregate_tx_validator.ts +3 -3
  64. package/src/msg_validators/tx_validator/factory.ts +353 -77
  65. package/src/msg_validators/tx_validator/gas_validator.ts +84 -29
  66. package/src/msg_validators/tx_validator/index.ts +1 -0
  67. package/src/msg_validators/tx_validator/nullifier_cache.ts +30 -0
  68. package/src/services/dummy_service.ts +1 -5
  69. package/src/services/libp2p/libp2p_service.ts +70 -79
  70. package/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +14 -42
  71. package/src/services/reqresp/batch-tx-requester/peer_collection.ts +63 -24
  72. package/src/services/reqresp/batch-tx-requester/tx_validator.ts +2 -2
  73. package/src/services/service.ts +1 -1
  74. package/src/services/tx_provider.ts +2 -2
  75. package/src/test-helpers/testbench-utils.ts +1 -1
@@ -1,15 +1,15 @@
1
1
  import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
+ import { BlockNumber } from '@aztec/foundation/branded-types';
2
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
3
4
  import { DateProvider } from '@aztec/foundation/timer';
4
5
  import type { AztecAsyncKVStore } from '@aztec/kv-store';
5
6
  import type { DataStoreConfig } from '@aztec/kv-store/config';
6
7
  import { AztecLMDBStoreV2, createStore } from '@aztec/kv-store/lmdb-v2';
7
- import type { BlockHash, L2BlockSource } from '@aztec/stdlib/block';
8
+ import type { L2BlockSource } from '@aztec/stdlib/block';
8
9
  import type { ChainConfig } from '@aztec/stdlib/config';
9
10
  import type { ContractDataSource } from '@aztec/stdlib/contract';
10
11
  import type { AztecNode, ClientProtocolCircuitVerifier, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
11
12
  import { P2PClientType } from '@aztec/stdlib/p2p';
12
- import { MerkleTreeId } from '@aztec/stdlib/trees';
13
13
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
14
14
 
15
15
  import { P2PClient } from '../client/p2p_client.js';
@@ -17,11 +17,8 @@ import type { P2PConfig } from '../config.js';
17
17
  import { AttestationPool, type AttestationPoolApi } from '../mem_pools/attestation_pool/attestation_pool.js';
18
18
  import type { MemPools } from '../mem_pools/interface.js';
19
19
  import type { TxPoolV2 } from '../mem_pools/tx_pool_v2/interfaces.js';
20
- import type { TxMetaData } from '../mem_pools/tx_pool_v2/tx_metadata.js';
21
20
  import { AztecKVTxPoolV2 } from '../mem_pools/tx_pool_v2/tx_pool_v2.js';
22
- import { AggregateTxValidator } from '../msg_validators/tx_validator/aggregate_tx_validator.js';
23
- import { BlockHeaderTxValidator } from '../msg_validators/tx_validator/block_header_validator.js';
24
- import { DoubleSpendTxValidator } from '../msg_validators/tx_validator/double_spend_validator.js';
21
+ import { createTxValidatorForTransactionsEnteringPendingTxPool } from '../msg_validators/index.js';
25
22
  import { DummyP2PService } from '../services/dummy_service.js';
26
23
  import { LibP2PService } from '../services/index.js';
27
24
  import { createFileStoreTxSources } from '../services/tx_collection/file_store_tx_source.js';
@@ -80,32 +77,6 @@ export async function createP2PClient<T extends P2PClientType>(
80
77
  const rollupAddress = inputConfig.l1Contracts.rollupAddress.toString().toLowerCase().replace(/^0x/, '');
81
78
  const txFileStoreBasePath = `aztec-${inputConfig.l1ChainId}-${inputConfig.rollupVersion}-0x${rollupAddress}`;
82
79
 
83
- /** Validator factory for pool re-validation (double-spend + block header only). */
84
- const createPoolTxValidator = async () => {
85
- await worldStateSynchronizer.syncImmediate();
86
- return new AggregateTxValidator<TxMetaData>(
87
- new DoubleSpendTxValidator<TxMetaData>(
88
- {
89
- nullifiersExist: async (nullifiers: Buffer[]) => {
90
- const merkleTree = worldStateSynchronizer.getCommitted();
91
- const indices = await merkleTree.findLeafIndices(MerkleTreeId.NULLIFIER_TREE, nullifiers);
92
- return indices.map(index => index !== undefined);
93
- },
94
- },
95
- bindings,
96
- ),
97
- new BlockHeaderTxValidator<TxMetaData>(
98
- {
99
- getArchiveIndices: (archives: BlockHash[]) => {
100
- const merkleTree = worldStateSynchronizer.getCommitted();
101
- return merkleTree.findLeafIndices(MerkleTreeId.ARCHIVE, archives);
102
- },
103
- },
104
- bindings,
105
- ),
106
- );
107
- };
108
-
109
80
  const txPool =
110
81
  deps.txPool ??
111
82
  new AztecKVTxPoolV2(
@@ -114,7 +85,16 @@ export async function createP2PClient<T extends P2PClientType>(
114
85
  {
115
86
  l2BlockSource: archiver,
116
87
  worldStateSynchronizer,
117
- createTxValidator: createPoolTxValidator,
88
+ createTxValidator: async () => {
89
+ // We accept transactions if they are not expired by the next slot and block number (checked based on the ExpirationTimestamp field)
90
+ const currentBlockNumber = await archiver.getBlockNumber();
91
+ const { ts: nextSlotTimestamp } = epochCache.getEpochAndSlotInNextL1Slot();
92
+ return createTxValidatorForTransactionsEnteringPendingTxPool(
93
+ worldStateSynchronizer,
94
+ nextSlotTimestamp,
95
+ BlockNumber(currentBlockNumber + 1),
96
+ );
97
+ },
118
98
  },
119
99
  telemetry,
120
100
  {
@@ -140,14 +140,6 @@ export type P2P<T extends P2PClientType = P2PClientType.Full> = P2PApiFull<T> &
140
140
  */
141
141
  hasTxsInPool(txHashes: TxHash[]): Promise<boolean[]>;
142
142
 
143
- /**
144
- * Returns transactions in the transaction pool by hash, requesting from the network if not found.
145
- * @param txHashes - Hashes of tx to return.
146
- * @param pinnedPeerId - An optional peer id that will be used to request the tx from (in addition to other random peers).
147
- * @returns An array of tx or undefined.
148
- */
149
- getTxsByHash(txHashes: TxHash[], pinnedPeerId: PeerId | undefined): Promise<(Tx | undefined)[]>;
150
-
151
143
  /**
152
144
  * Returns an archived transaction from the transaction pool by its hash.
153
145
  * @param txHash - Hash of tx to return.
@@ -224,8 +216,8 @@ export type P2P<T extends P2PClientType = P2PClientType.Full> = P2PApiFull<T> &
224
216
 
225
217
  updateP2PConfig(config: Partial<P2PConfig>): Promise<void>;
226
218
 
227
- /** Validates a set of txs. */
228
- validate(txs: Tx[]): Promise<void>;
219
+ /** Validates a set of txs received in a block proposal. */
220
+ validateTxsReceivedInBlockProposal(txs: Tx[]): Promise<void>;
229
221
 
230
222
  /** Clears the db. */
231
223
  clear(): Promise<void>;
@@ -44,7 +44,6 @@ import {
44
44
  type ReqRespSubProtocolHandler,
45
45
  type ReqRespSubProtocolValidators,
46
46
  } from '../services/reqresp/interface.js';
47
- import { chunkTxHashesRequest } from '../services/reqresp/protocols/tx.js';
48
47
  import type {
49
48
  DuplicateAttestationInfo,
50
49
  DuplicateProposalInfo,
@@ -433,36 +432,6 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
433
432
  this.p2pService.registerDuplicateAttestationCallback(callback);
434
433
  }
435
434
 
436
- /**
437
- * Uses the batched Request Response protocol to request a set of transactions from the network.
438
- */
439
- private async requestTxsByHash(txHashes: TxHash[], pinnedPeerId: PeerId | undefined): Promise<Tx[]> {
440
- const timeoutMs = 8000; // Longer timeout for now
441
- const maxRetryAttempts = 10; // Keep retrying within the timeout
442
- const requests = chunkTxHashesRequest(txHashes);
443
- const maxPeers = Math.min(Math.ceil(requests.length / 3), 10);
444
-
445
- const txBatches = await this.p2pService.sendBatchRequest(
446
- ReqRespSubProtocol.TX,
447
- requests,
448
- pinnedPeerId,
449
- timeoutMs,
450
- maxPeers,
451
- maxRetryAttempts,
452
- );
453
-
454
- const txs = txBatches.flat();
455
- if (txs.length > 0) {
456
- await this.txPool.addPendingTxs(txs);
457
- }
458
-
459
- const txHashesStr = txHashes.map(tx => tx.toString()).join(', ');
460
- this.log.debug(`Requested txs ${txHashesStr} (${txs.length} / ${txHashes.length}) from peers`);
461
-
462
- // We return all transactions, even the not found ones to the caller, such they can handle missing items themselves.
463
- return txs;
464
- }
465
-
466
435
  public async getPendingTxs(limit?: number, after?: TxHash): Promise<Tx[]> {
467
436
  if (limit !== undefined && limit <= 0) {
468
437
  throw new TypeError('limit must be greater than 0');
@@ -530,49 +499,6 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
530
499
  return this.txPool.hasTxs(txHashes);
531
500
  }
532
501
 
533
- /**
534
- * Returns transactions in the transaction pool by hash.
535
- * If a transaction is not in the pool, it will be requested from the network.
536
- * @param txHashes - Hashes of the transactions to look for.
537
- * @returns The txs found, or undefined if not found in the order requested.
538
- */
539
- async getTxsByHash(txHashes: TxHash[], pinnedPeerId: PeerId | undefined): Promise<(Tx | undefined)[]> {
540
- const txs = await Promise.all(txHashes.map(txHash => this.txPool.getTxByHash(txHash)));
541
- const missingTxHashes = txs
542
- .map((tx, index) => [tx, index] as const)
543
- .filter(([tx, _index]) => !tx)
544
- .map(([_tx, index]) => txHashes[index]);
545
-
546
- if (missingTxHashes.length === 0) {
547
- return txs as Tx[];
548
- }
549
-
550
- const missingTxs = await this.requestTxsByHash(missingTxHashes, pinnedPeerId);
551
- // TODO: optimize
552
- // Merge the found txs in order
553
- const mergingTxs = txHashes.map(txHash => {
554
- // Is it in the txs list from the mempool?
555
- for (const tx of txs) {
556
- if (tx !== undefined && tx.getTxHash().equals(txHash)) {
557
- return tx;
558
- }
559
- }
560
-
561
- // Is it in the fetched missing txs?
562
- // Note: this is an O(n^2) operation, but we expect the number of missing txs to be small.
563
- for (const tx of missingTxs) {
564
- if (tx.getTxHash().equals(txHash)) {
565
- return tx;
566
- }
567
- }
568
-
569
- // Otherwise return undefined
570
- return undefined;
571
- });
572
-
573
- return mergingTxs;
574
- }
575
-
576
502
  /**
577
503
  * Returns an archived transaction in the transaction pool by its hash.
578
504
  * @param txHash - Hash of the archived transaction to look for.
@@ -839,8 +765,8 @@ export class P2PClient<T extends P2PClientType = P2PClientType.Full>
839
765
  this.log.debug(`Moved from state ${P2PClientState[oldState]} to ${P2PClientState[this.currentState]}`);
840
766
  }
841
767
 
842
- public validate(txs: Tx[]): Promise<void> {
843
- return this.p2pService.validate(txs);
768
+ public validateTxsReceivedInBlockProposal(txs: Tx[]): Promise<void> {
769
+ return this.p2pService.validateTxsReceivedInBlockProposal(txs);
844
770
  }
845
771
 
846
772
  /**
@@ -110,12 +110,12 @@ export interface TxPoolV2 extends TypedEventEmitter<TxPoolV2Events> {
110
110
  addPendingTxs(txs: Tx[], opts?: { source?: string; feeComparisonOnly?: boolean }): Promise<AddTxsResult>;
111
111
 
112
112
  /**
113
- * Checks if a transaction can be added without modifying the pool.
114
- * Performs the same validation as addPendingTxs but doesn't persist changes.
113
+ * Checks if the pool would accept a transaction without modifying state.
114
+ * Used as a pre-check before expensive proof verification.
115
115
  * @param tx - Transaction to check
116
- * @returns Result: 'accepted', 'ignored' (if already in pool or undesirable), or 'rejected' (if validation fails)
116
+ * @returns 'accepted' if the pool would accept, 'ignored' if already in pool or undesirable
117
117
  */
118
- canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored' | 'rejected'>;
118
+ canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored'>;
119
119
 
120
120
  /**
121
121
  * Adds transactions as immediately protected for a given slot.
@@ -2,6 +2,7 @@ import { BlockNumber } from '@aztec/foundation/branded-types';
2
2
  import { Fr } from '@aztec/foundation/curves/bn254';
3
3
  import { ProtocolContractAddress } from '@aztec/protocol-contracts';
4
4
  import { BlockHash, type L2BlockId } from '@aztec/stdlib/block';
5
+ import { Gas } from '@aztec/stdlib/gas';
5
6
  import type { Tx } from '@aztec/stdlib/tx';
6
7
 
7
8
  import { getFeePayerBalanceDelta } from '../../msg_validators/tx_validator/fee_payer_balance.js';
@@ -12,6 +13,8 @@ import { type PreAddResult, TxPoolRejectionCode } from './eviction/interfaces.js
12
13
  export type TxMetaValidationData = {
13
14
  getNonEmptyNullifiers(): Fr[];
14
15
  expirationTimestamp: bigint;
16
+ /** Whether the tx has public calls. Used to select the correct L2 gas minimum. */
17
+ forPublic?: unknown;
15
18
  constants: {
16
19
  anchorBlockHeader: {
17
20
  hash(): Promise<BlockHash>;
@@ -19,6 +22,9 @@ export type TxMetaValidationData = {
19
22
  blockNumber: BlockNumber;
20
23
  };
21
24
  };
25
+ txContext: {
26
+ gasSettings: { gasLimits: Gas };
27
+ };
22
28
  };
23
29
  };
24
30
 
@@ -105,11 +111,15 @@ export async function buildTxMetaData(tx: Tx): Promise<TxMetaData> {
105
111
  data: {
106
112
  getNonEmptyNullifiers: () => nullifierFrs,
107
113
  expirationTimestamp,
114
+ forPublic: !!tx.data.forPublic,
108
115
  constants: {
109
116
  anchorBlockHeader: {
110
117
  hash: () => Promise.resolve(anchorBlockHeaderHashFr),
111
118
  globalVariables: { blockNumber: anchorBlockNumber },
112
119
  },
120
+ txContext: {
121
+ gasSettings: { gasLimits: tx.data.constants.txContext.gasSettings.gasLimits },
122
+ },
113
123
  },
114
124
  },
115
125
  };
@@ -237,6 +247,9 @@ export function stubTxMetaValidationData(overrides: { expirationTimestamp?: bigi
237
247
  hash: () => Promise.resolve(new BlockHash(Fr.ZERO)),
238
248
  globalVariables: { blockNumber: BlockNumber(0) },
239
249
  },
250
+ txContext: {
251
+ gasSettings: { gasLimits: Gas.empty() },
252
+ },
240
253
  },
241
254
  };
242
255
  }
@@ -74,7 +74,7 @@ export class AztecKVTxPoolV2 extends (EventEmitter as new () => TypedEventEmitte
74
74
  return this.#queue.put(() => this.#impl.addPendingTxs(txs, opts));
75
75
  }
76
76
 
77
- canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored' | 'rejected'> {
77
+ canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored'> {
78
78
  return this.#queue.put(() => this.#impl.canAddPendingTx(tx));
79
79
  }
80
80
 
@@ -327,7 +327,7 @@ export class TxPoolV2Impl {
327
327
  return { status: 'accepted' };
328
328
  }
329
329
 
330
- async canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored' | 'rejected'> {
330
+ async canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored'> {
331
331
  const txHashStr = tx.getTxHash().toString();
332
332
 
333
333
  // Check if already in pool
@@ -335,14 +335,8 @@ export class TxPoolV2Impl {
335
335
  return 'ignored';
336
336
  }
337
337
 
338
- // Build metadata and validate using metadata
338
+ // Build metadata and check pre-add rules
339
339
  const meta = await buildTxMetaData(tx);
340
- const validationResult = await this.#validateMeta(meta, undefined, 'can add pending');
341
- if (validationResult !== true) {
342
- return 'rejected';
343
- }
344
-
345
- // Use pre-add rules
346
340
  const poolAccess = this.#createPreAddPoolAccess();
347
341
  const preAddResult = await this.#evictionManager.runPreAddRules(meta, poolAccess);
348
342
 
@@ -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';