@aztec/p2p 0.0.1-commit.96dac018d → 0.0.1-commit.993d52e

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 (98) hide show
  1. package/dest/client/factory.d.ts +4 -5
  2. package/dest/client/factory.d.ts.map +1 -1
  3. package/dest/client/factory.js +6 -6
  4. package/dest/client/interface.d.ts +4 -4
  5. package/dest/client/interface.d.ts.map +1 -1
  6. package/dest/client/p2p_client.d.ts +4 -4
  7. package/dest/client/p2p_client.d.ts.map +1 -1
  8. package/dest/client/p2p_client.js +1 -1
  9. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.js +1 -2
  10. package/dest/config.d.ts +10 -3
  11. package/dest/config.d.ts.map +1 -1
  12. package/dest/config.js +11 -1
  13. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts +1 -1
  14. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts.map +1 -1
  15. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.js +2 -0
  16. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts +7 -1
  17. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts.map +1 -1
  18. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts +1 -1
  19. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts.map +1 -1
  20. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.js +8 -6
  21. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts +2 -2
  22. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts.map +1 -1
  23. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.js +2 -2
  24. package/dest/mem_pools/tx_pool_v2/index.d.ts +2 -2
  25. package/dest/mem_pools/tx_pool_v2/index.d.ts.map +1 -1
  26. package/dest/mem_pools/tx_pool_v2/index.js +1 -1
  27. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +3 -3
  28. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  29. package/dest/mem_pools/tx_pool_v2/interfaces.js +1 -1
  30. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +30 -7
  31. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  32. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +62 -16
  33. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +1 -1
  34. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
  35. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +9 -10
  36. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +1 -1
  37. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  38. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +38 -30
  39. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts +2 -1
  40. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts.map +1 -1
  41. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts +2 -1
  42. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts.map +1 -1
  43. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +3 -1
  44. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  45. package/dest/msg_validators/proposal_validator/proposal_validator.js +10 -0
  46. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts +2 -1
  47. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts.map +1 -1
  48. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.js +166 -0
  49. package/dest/services/encoding.d.ts +2 -2
  50. package/dest/services/encoding.d.ts.map +1 -1
  51. package/dest/services/encoding.js +7 -7
  52. package/dest/services/libp2p/libp2p_service.d.ts +6 -7
  53. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  54. package/dest/services/libp2p/libp2p_service.js +11 -12
  55. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts +1 -1
  56. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts.map +1 -1
  57. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.js +37 -14
  58. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts +11 -17
  59. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts.map +1 -1
  60. package/dest/services/reqresp/batch-tx-requester/peer_collection.js +15 -49
  61. package/dest/test-helpers/make-test-p2p-clients.d.ts +5 -6
  62. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  63. package/dest/test-helpers/make-test-p2p-clients.js +1 -2
  64. package/dest/test-helpers/mock-pubsub.d.ts +2 -3
  65. package/dest/test-helpers/mock-pubsub.d.ts.map +1 -1
  66. package/dest/test-helpers/mock-pubsub.js +2 -2
  67. package/dest/test-helpers/reqresp-nodes.d.ts +2 -3
  68. package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
  69. package/dest/test-helpers/reqresp-nodes.js +2 -2
  70. package/dest/testbench/p2p_client_testbench_worker.js +5 -5
  71. package/package.json +14 -14
  72. package/src/client/factory.ts +9 -14
  73. package/src/client/interface.ts +3 -9
  74. package/src/client/p2p_client.ts +2 -12
  75. package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +1 -2
  76. package/src/config.ts +20 -2
  77. package/src/mem_pools/tx_pool_v2/README.md +9 -1
  78. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.ts +3 -0
  79. package/src/mem_pools/tx_pool_v2/eviction/interfaces.ts +11 -1
  80. package/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.ts +15 -6
  81. package/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.ts +2 -1
  82. package/src/mem_pools/tx_pool_v2/index.ts +1 -1
  83. package/src/mem_pools/tx_pool_v2/interfaces.ts +3 -3
  84. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +89 -17
  85. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +11 -11
  86. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +43 -27
  87. package/src/msg_validators/proposal_validator/block_proposal_validator.ts +1 -1
  88. package/src/msg_validators/proposal_validator/checkpoint_proposal_validator.ts +1 -1
  89. package/src/msg_validators/proposal_validator/proposal_validator.ts +15 -1
  90. package/src/msg_validators/proposal_validator/proposal_validator_test_suite.ts +144 -1
  91. package/src/services/encoding.ts +5 -6
  92. package/src/services/libp2p/libp2p_service.ts +10 -12
  93. package/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +42 -14
  94. package/src/services/reqresp/batch-tx-requester/peer_collection.ts +24 -63
  95. package/src/test-helpers/make-test-p2p-clients.ts +0 -2
  96. package/src/test-helpers/mock-pubsub.ts +3 -6
  97. package/src/test-helpers/reqresp-nodes.ts +2 -5
  98. package/src/testbench/p2p_client_testbench_worker.ts +2 -6
@@ -3,7 +3,7 @@ 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
5
  import { Gas } from '@aztec/stdlib/gas';
6
- import type { Tx } from '@aztec/stdlib/tx';
6
+ import { type Tx, TxHash } from '@aztec/stdlib/tx';
7
7
 
8
8
  import { getFeePayerBalanceDelta } from '../../msg_validators/tx_validator/fee_payer_balance.js';
9
9
  import { getTxPriorityFee } from '../tx_pool/priority.js';
@@ -40,6 +40,9 @@ export type TxMetaData = {
40
40
  /** The transaction hash as hex string */
41
41
  readonly txHash: string;
42
42
 
43
+ /** The transaction hash as bigint (for efficient Fr conversion in comparisons) */
44
+ readonly txHashBigInt: bigint;
45
+
43
46
  /** Block ID (number and hash) in which the transaction was mined (undefined if not mined) */
44
47
  minedL2BlockId?: L2BlockId;
45
48
 
@@ -83,7 +86,9 @@ export type TxState = 'pending' | 'protected' | 'mined' | 'deleted';
83
86
  * Fr values are captured in closures for zero-cost re-validation.
84
87
  */
85
88
  export async function buildTxMetaData(tx: Tx): Promise<TxMetaData> {
86
- const txHash = tx.getTxHash().toString();
89
+ const txHashObj = tx.getTxHash();
90
+ const txHash = txHashObj.toString();
91
+ const txHashBigInt = txHashObj.toBigInt();
87
92
  const nullifierFrs = tx.data.getNonEmptyNullifiers();
88
93
  const nullifiers = nullifierFrs.map(n => n.toString());
89
94
  const anchorBlockHeaderHashFr = await tx.data.constants.anchorBlockHeader.hash();
@@ -99,6 +104,7 @@ export async function buildTxMetaData(tx: Tx): Promise<TxMetaData> {
99
104
 
100
105
  return {
101
106
  txHash,
107
+ txHashBigInt,
102
108
  anchorBlockHeaderHash,
103
109
  priorityFee,
104
110
  feePayer,
@@ -134,11 +140,11 @@ const HEX_STRING_BYTES = 98;
134
140
  const BIGINT_BYTES = 32;
135
141
  const FR_BYTES = 80;
136
142
  // Fixed cost: object shell + txHash + anchorBlockHeaderHash + feePayer (3 hex strings)
137
- // + priorityFee + claimAmount + feeLimit + includeByTimestamp (4 bigints)
143
+ // + txHashBigInt + priorityFee + claimAmount + feeLimit + includeByTimestamp (5 bigints)
138
144
  // + receivedAt (number, 8 bytes) + estimatedSizeBytes (number, 8 bytes)
139
145
  // + data closure object (~OBJECT_OVERHEAD + anchorBlockHeaderHashFr Fr + anchorBlockNumber number)
140
146
  const FIXED_METADATA_BYTES =
141
- OBJECT_OVERHEAD + 3 * HEX_STRING_BYTES + 4 * BIGINT_BYTES + 8 + 8 + OBJECT_OVERHEAD + FR_BYTES + 8;
147
+ OBJECT_OVERHEAD + 3 * HEX_STRING_BYTES + 5 * BIGINT_BYTES + 8 + 8 + OBJECT_OVERHEAD + FR_BYTES + 8;
142
148
 
143
149
  /** Estimates the in-memory size of a TxMetaData object based on the number of nullifiers. */
144
150
  function estimateTxMetaDataSize(nullifierCount: number): number {
@@ -146,8 +152,13 @@ function estimateTxMetaDataSize(nullifierCount: number): number {
146
152
  return FIXED_METADATA_BYTES + nullifierCount * (HEX_STRING_BYTES + FR_BYTES);
147
153
  }
148
154
 
155
+ /** Converts a txHash bigint back to the canonical 0x-prefixed 64-char hex string. */
156
+ export function txHashFromBigInt(value: bigint): string {
157
+ return TxHash.fromBigInt(value).toString();
158
+ }
159
+
149
160
  /** Minimal fields required for priority comparison. */
150
- type PriorityComparable = Pick<TxMetaData, 'txHash' | 'priorityFee'>;
161
+ type PriorityComparable = Pick<TxMetaData, 'txHashBigInt' | 'priorityFee'>;
151
162
 
152
163
  /**
153
164
  * Compares two priority fees in ascending order.
@@ -162,10 +173,8 @@ export function compareFee(a: bigint, b: bigint): number {
162
173
  * Uses field element comparison for deterministic ordering.
163
174
  * Returns negative if a < b, positive if a > b, 0 if equal.
164
175
  */
165
- export function compareTxHash(a: string, b: string): number {
166
- const fieldA = Fr.fromHexString(a);
167
- const fieldB = Fr.fromHexString(b);
168
- return fieldA.cmp(fieldB);
176
+ export function compareTxHash(a: bigint, b: bigint): number {
177
+ return Fr.cmpAsBigInt(a, b);
169
178
  }
170
179
 
171
180
  /**
@@ -178,24 +187,41 @@ export function comparePriority(a: PriorityComparable, b: PriorityComparable): n
178
187
  if (feeComparison !== 0) {
179
188
  return feeComparison;
180
189
  }
181
- return compareTxHash(a.txHash, b.txHash);
190
+ return compareTxHash(a.txHashBigInt, b.txHashBigInt);
191
+ }
192
+
193
+ /**
194
+ * Returns the minimum fee required to replace an existing tx with the given price bump percentage.
195
+ * Uses integer arithmetic: `existingFee + existingFee * priceBumpPercentage / 100`.
196
+ */
197
+ export function getMinimumPriceBumpFee(existingFee: bigint, priceBumpPercentage: bigint): bigint {
198
+ const bump = (existingFee * priceBumpPercentage) / 100n;
199
+ // Ensure the minimum bump is at least 1, so that replacement always requires
200
+ // paying strictly more — even with 0% bump or zero existing fee.
201
+ const effectiveBump = bump > 0n ? bump : 1n;
202
+ return existingFee + effectiveBump;
182
203
  }
183
204
 
184
205
  /**
185
206
  * Checks for nullifier conflicts between an incoming transaction and existing pool state.
186
207
  *
187
208
  * When the incoming tx shares nullifiers with existing pending txs:
188
- * - If the incoming tx has strictly higher priority, mark conflicting txs for eviction
189
- * - If any conflicting tx has equal or higher priority, ignore the incoming tx
209
+ * - If the incoming tx meets or exceeds the required priority, mark conflicting txs for eviction
210
+ * - Otherwise, ignore the incoming tx
211
+ *
212
+ * When `priceBumpPercentage` is provided (RPC path), uses fee-only comparison with the
213
+ * percentage bump instead of `comparePriority`.
190
214
  *
191
215
  * @param incomingMeta - Metadata for the incoming transaction
192
216
  * @param getTxHashByNullifier - Accessor to find which tx uses a nullifier
193
217
  * @param getMetadata - Accessor to get metadata for a tx hash
218
+ * @param priceBumpPercentage - Optional percentage bump required for fee-based replacement
194
219
  */
195
220
  export function checkNullifierConflict(
196
221
  incomingMeta: TxMetaData,
197
222
  getTxHashByNullifier: (nullifier: string) => string | undefined,
198
223
  getMetadata: (txHash: string) => TxMetaData | undefined,
224
+ priceBumpPercentage?: bigint,
199
225
  ): PreAddResult {
200
226
  const txHashesToEvict: string[] = [];
201
227
 
@@ -216,19 +242,32 @@ export function checkNullifierConflict(
216
242
  continue;
217
243
  }
218
244
 
219
- // If incoming tx has strictly higher priority, mark for eviction
220
- // Otherwise, ignore incoming tx (ties go to existing tx)
221
- // Use comparePriority for deterministic ordering (includes txHash as tiebreaker)
222
- if (comparePriority(incomingMeta, conflictingMeta) > 0) {
245
+ // When price bump is set (RPC path), require the incoming fee to meet the bumped threshold.
246
+ // Otherwise (P2P path), use full comparePriority with tx hash tiebreaker.
247
+ const isHigherPriority =
248
+ priceBumpPercentage !== undefined
249
+ ? incomingMeta.priorityFee >= getMinimumPriceBumpFee(conflictingMeta.priorityFee, priceBumpPercentage)
250
+ : comparePriority(incomingMeta, conflictingMeta) > 0;
251
+
252
+ if (isHigherPriority) {
223
253
  txHashesToEvict.push(conflictingHashStr);
224
254
  } else {
255
+ const minimumFee =
256
+ priceBumpPercentage !== undefined
257
+ ? getMinimumPriceBumpFee(conflictingMeta.priorityFee, priceBumpPercentage)
258
+ : undefined;
225
259
  return {
226
260
  shouldIgnore: true,
227
261
  txHashesToEvict: [],
228
262
  reason: {
229
263
  code: TxPoolRejectionCode.NULLIFIER_CONFLICT,
230
- message: `Nullifier conflict with existing tx ${conflictingHashStr}`,
264
+ message:
265
+ minimumFee !== undefined
266
+ ? `Nullifier conflict with existing tx ${conflictingHashStr}. Minimum required fee: ${minimumFee}, got: ${incomingMeta.priorityFee}`
267
+ : `Nullifier conflict with existing tx ${conflictingHashStr}`,
231
268
  conflictingTxHash: conflictingHashStr,
269
+ minimumPriceBumpFee: minimumFee,
270
+ txPriorityFee: minimumFee !== undefined ? incomingMeta.priorityFee : undefined,
232
271
  },
233
272
  };
234
273
  }
@@ -253,3 +292,36 @@ export function stubTxMetaValidationData(overrides: { expirationTimestamp?: bigi
253
292
  },
254
293
  };
255
294
  }
295
+
296
+ /** Creates a stub TxMetaData for tests. All fields have sensible defaults and can be overridden. */
297
+ export function stubTxMetaData(
298
+ txHash: string,
299
+ overrides: {
300
+ priorityFee?: bigint;
301
+ feePayer?: string;
302
+ claimAmount?: bigint;
303
+ feeLimit?: bigint;
304
+ nullifiers?: string[];
305
+ expirationTimestamp?: bigint;
306
+ anchorBlockHeaderHash?: string;
307
+ } = {},
308
+ ): TxMetaData {
309
+ const txHashBigInt = Fr.fromHexString(txHash).toBigInt();
310
+ // Normalize to canonical zero-padded hex so txHashFromBigInt(txHashBigInt) === normalizedTxHash
311
+ const normalizedTxHash = txHashFromBigInt(txHashBigInt);
312
+ const expirationTimestamp = overrides.expirationTimestamp ?? 0n;
313
+ return {
314
+ txHash: normalizedTxHash,
315
+ txHashBigInt,
316
+ anchorBlockHeaderHash: overrides.anchorBlockHeaderHash ?? '0x1234',
317
+ priorityFee: overrides.priorityFee ?? 100n,
318
+ feePayer: overrides.feePayer ?? '0xfeepayer',
319
+ claimAmount: overrides.claimAmount ?? 0n,
320
+ feeLimit: overrides.feeLimit ?? 100n,
321
+ nullifiers: overrides.nullifiers ?? [`0x${normalizedTxHash.slice(2)}null1`],
322
+ expirationTimestamp,
323
+ receivedAt: 0,
324
+ estimatedSizeBytes: 0,
325
+ data: stubTxMetaValidationData({ expirationTimestamp }),
326
+ };
327
+ }
@@ -1,7 +1,7 @@
1
1
  import { SlotNumber } from '@aztec/foundation/branded-types';
2
2
  import type { L2BlockId } from '@aztec/stdlib/block';
3
3
 
4
- import { type TxMetaData, type TxState, compareFee, compareTxHash } from './tx_metadata.js';
4
+ import { type TxMetaData, type TxState, compareFee, compareTxHash, txHashFromBigInt } from './tx_metadata.js';
5
5
 
6
6
  /**
7
7
  * Manages in-memory indices for the transaction pool.
@@ -22,8 +22,8 @@ export class TxPoolIndices {
22
22
  #nullifierToTxHash: Map<string, string> = new Map();
23
23
  /** Fee payer to txHashes index (pending txs only) */
24
24
  #feePayerToTxHashes: Map<string, Set<string>> = new Map();
25
- /** Pending txHashes grouped by priority fee */
26
- #pendingByPriority: Map<bigint, Set<string>> = new Map();
25
+ /** Pending txHash bigints grouped by priority fee */
26
+ #pendingByPriority: Map<bigint, Set<bigint>> = new Map();
27
27
  /** Protected transactions: txHash -> slotNumber */
28
28
  #protectedTransactions: Map<string, SlotNumber> = new Map();
29
29
 
@@ -73,17 +73,17 @@ export class TxPoolIndices {
73
73
  * @param order - 'desc' for highest priority first, 'asc' for lowest priority first
74
74
  */
75
75
  *iteratePendingByPriority(order: 'asc' | 'desc', filter?: (hash: string) => boolean): Generator<string> {
76
- // Use compareFee from tx_metadata, swap args for descending order
77
76
  const feeCompareFn = order === 'desc' ? (a: bigint, b: bigint) => compareFee(b, a) : compareFee;
78
- const hashCompareFn = order === 'desc' ? (a: string, b: string) => compareTxHash(b, a) : compareTxHash;
77
+ const hashCompareFn =
78
+ order === 'desc' ? (a: bigint, b: bigint) => compareTxHash(b, a) : (a: bigint, b: bigint) => compareTxHash(a, b);
79
79
 
80
80
  const sortedFees = [...this.#pendingByPriority.keys()].sort(feeCompareFn);
81
81
 
82
82
  for (const fee of sortedFees) {
83
83
  const hashesAtFee = this.#pendingByPriority.get(fee)!;
84
- // Use compareTxHash from tx_metadata, swap args for descending order
85
84
  const sortedHashes = [...hashesAtFee].sort(hashCompareFn);
86
- for (const hash of sortedHashes) {
85
+ for (const hashBigInt of sortedHashes) {
86
+ const hash = txHashFromBigInt(hashBigInt);
87
87
  if (filter === undefined || filter(hash)) {
88
88
  yield hash;
89
89
  }
@@ -265,8 +265,8 @@ export class TxPoolIndices {
265
265
  getPendingTxs(): TxMetaData[] {
266
266
  const result: TxMetaData[] = [];
267
267
  for (const hashSet of this.#pendingByPriority.values()) {
268
- for (const txHash of hashSet) {
269
- const meta = this.#metadata.get(txHash);
268
+ for (const txHashBigInt of hashSet) {
269
+ const meta = this.#metadata.get(txHashFromBigInt(txHashBigInt));
270
270
  if (meta) {
271
271
  result.push(meta);
272
272
  }
@@ -414,7 +414,7 @@ export class TxPoolIndices {
414
414
  prioritySet = new Set();
415
415
  this.#pendingByPriority.set(meta.priorityFee, prioritySet);
416
416
  }
417
- prioritySet.add(meta.txHash);
417
+ prioritySet.add(meta.txHashBigInt);
418
418
  }
419
419
 
420
420
  #removeFromPendingIndices(meta: TxMetaData): void {
@@ -435,7 +435,7 @@ export class TxPoolIndices {
435
435
  // Remove from priority map
436
436
  const hashSet = this.#pendingByPriority.get(meta.priorityFee);
437
437
  if (hashSet) {
438
- hashSet.delete(meta.txHash);
438
+ hashSet.delete(meta.txHashBigInt);
439
439
  if (hashSet.size === 0) {
440
440
  this.#pendingByPriority.delete(meta.priorityFee);
441
441
  }
@@ -187,9 +187,35 @@ 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
- opts.feeComparisonOnly !== undefined ? { feeComparisonOnly: opts.feeComparisonOnly } : undefined;
216
+ opts.feeComparisonOnly !== undefined
217
+ ? { feeComparisonOnly: opts.feeComparisonOnly, priceBumpPercentage: this.#config.priceBumpPercentage }
218
+ : undefined;
193
219
 
194
220
  await this.#store.transactionAsync(async () => {
195
221
  for (const tx of txs) {
@@ -202,22 +228,25 @@ export class TxPoolV2Impl {
202
228
  continue;
203
229
  }
204
230
 
205
- // Check mined status first (applies to all paths)
206
- const minedBlockId = await this.#getMinedBlockId(txHash);
231
+ const { meta, minedBlockId, isValid } = precomputed.get(txHashStr)!;
207
232
  const preProtectedSlot = this.#indices.getProtectionSlot(txHashStr);
208
233
 
209
234
  if (minedBlockId) {
210
235
  // Already mined - add directly (protection already set if pre-protected)
211
- await this.#addTx(tx, { mined: minedBlockId }, opts);
236
+ await this.#addTx(tx, { mined: minedBlockId }, opts, meta);
212
237
  accepted.push(txHash);
213
238
  } else if (preProtectedSlot !== undefined) {
214
239
  // Pre-protected and not mined - add as protected (bypass validation)
215
- await this.#addTx(tx, { protected: preProtectedSlot }, opts);
240
+ await this.#addTx(tx, { protected: preProtectedSlot }, opts, meta);
216
241
  accepted.push(txHash);
242
+ } else if (!isValid) {
243
+ // Failed pre-computed validation
244
+ rejected.push(txHash);
217
245
  } else {
218
- // Regular pending tx - validate and run pre-add rules
246
+ // Regular pending tx - run pre-add rules using pre-computed metadata
219
247
  const result = await this.#tryAddRegularPendingTx(
220
248
  tx,
249
+ meta,
221
250
  opts,
222
251
  poolAccess,
223
252
  acceptedPending,
@@ -227,8 +256,6 @@ export class TxPoolV2Impl {
227
256
  );
228
257
  if (result.status === 'accepted') {
229
258
  acceptedPending.add(txHashStr);
230
- } else if (result.status === 'rejected') {
231
- rejected.push(txHash);
232
259
  } else {
233
260
  ignored.push(txHash);
234
261
  }
@@ -259,27 +286,21 @@ export class TxPoolV2Impl {
259
286
  return { accepted, ignored, rejected, ...(errors.size > 0 ? { errors } : {}) };
260
287
  }
261
288
 
262
- /** Validates and adds a regular pending tx. Returns status. */
289
+ /** Adds a validated pending tx, running pre-add rules and evicting conflicts. */
263
290
  async #tryAddRegularPendingTx(
264
291
  tx: Tx,
292
+ precomputedMeta: TxMetaData,
265
293
  opts: { source?: string },
266
294
  poolAccess: PreAddPoolAccess,
267
295
  acceptedPending: Set<string>,
268
296
  ignored: TxHash[],
269
297
  errors: Map<string, TxPoolRejectionError>,
270
298
  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
- }
299
+ ): Promise<{ status: 'accepted' | 'ignored' }> {
300
+ const txHashStr = tx.getTxHash().toString();
280
301
 
281
302
  // Run pre-add rules
282
- const preAddResult = await this.#evictionManager.runPreAddRules(meta, poolAccess, preAddContext);
303
+ const preAddResult = await this.#evictionManager.runPreAddRules(precomputedMeta, poolAccess, preAddContext);
283
304
 
284
305
  if (preAddResult.shouldIgnore) {
285
306
  this.#log.debug(`Ignoring tx ${txHashStr}: ${preAddResult.reason?.message ?? 'unknown reason'}`);
@@ -316,14 +337,8 @@ export class TxPoolV2Impl {
316
337
  }
317
338
  }
318
339
 
319
- // Randomly drop the transaction for testing purposes (report as accepted so it propagates)
320
- if (this.#config.dropTransactionsProbability > 0 && Math.random() < this.#config.dropTransactionsProbability) {
321
- this.#log.debug(`Dropping tx ${txHashStr} (simulated drop for testing)`);
322
- return { status: 'accepted' };
323
- }
324
-
325
340
  // Add the transaction
326
- await this.#addTx(tx, 'pending', opts);
341
+ await this.#addTx(tx, 'pending', opts, precomputedMeta);
327
342
  return { status: 'accepted' };
328
343
  }
329
344
 
@@ -765,9 +780,10 @@ export class TxPoolV2Impl {
765
780
  tx: Tx,
766
781
  state: 'pending' | { protected: SlotNumber } | { mined: L2BlockId },
767
782
  opts: { source?: string } = {},
783
+ precomputedMeta?: TxMetaData,
768
784
  ): Promise<TxMetaData> {
769
785
  const txHashStr = tx.getTxHash().toString();
770
- const meta = await buildTxMetaData(tx);
786
+ const meta = precomputedMeta ?? (await buildTxMetaData(tx));
771
787
  meta.receivedAt = this.#dateProvider.now();
772
788
 
773
789
  await this.#txsDB.set(txHashStr, tx.toBuffer());
@@ -4,7 +4,7 @@ import type { BlockProposal, P2PValidator } from '@aztec/stdlib/p2p';
4
4
  import { ProposalValidator } from '../proposal_validator/proposal_validator.js';
5
5
 
6
6
  export class BlockProposalValidator extends ProposalValidator<BlockProposal> implements P2PValidator<BlockProposal> {
7
- constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean }) {
7
+ constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean; maxTxsPerBlock?: number }) {
8
8
  super(epochCache, opts, 'p2p:block_proposal_validator');
9
9
  }
10
10
  }
@@ -7,7 +7,7 @@ export class CheckpointProposalValidator
7
7
  extends ProposalValidator<CheckpointProposal>
8
8
  implements P2PValidator<CheckpointProposal>
9
9
  {
10
- constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean }) {
10
+ constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean; maxTxsPerBlock?: number }) {
11
11
  super(epochCache, opts, 'p2p:checkpoint_proposal_validator');
12
12
  }
13
13
  }
@@ -9,10 +9,16 @@ export abstract class ProposalValidator<TProposal extends BlockProposal | Checkp
9
9
  protected epochCache: EpochCacheInterface;
10
10
  protected logger: Logger;
11
11
  protected txsPermitted: boolean;
12
+ protected maxTxsPerBlock?: number;
12
13
 
13
- constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean }, loggerName: string) {
14
+ constructor(
15
+ epochCache: EpochCacheInterface,
16
+ opts: { txsPermitted: boolean; maxTxsPerBlock?: number },
17
+ loggerName: string,
18
+ ) {
14
19
  this.epochCache = epochCache;
15
20
  this.txsPermitted = opts.txsPermitted;
21
+ this.maxTxsPerBlock = opts.maxTxsPerBlock;
16
22
  this.logger = createLogger(loggerName);
17
23
  }
18
24
 
@@ -47,6 +53,14 @@ export abstract class ProposalValidator<TProposal extends BlockProposal | Checkp
47
53
  return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
48
54
  }
49
55
 
56
+ // Max txs per block check
57
+ if (this.maxTxsPerBlock !== undefined && proposal.txHashes.length > this.maxTxsPerBlock) {
58
+ this.logger.warn(
59
+ `Penalizing peer for proposal with ${proposal.txHashes.length} transaction(s) when max is ${this.maxTxsPerBlock}`,
60
+ );
61
+ return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
62
+ }
63
+
50
64
  // Embedded txs must be listed in txHashes
51
65
  const hashSet = new Set(proposal.txHashes.map(h => h.toString()));
52
66
  const missingTxHashes =
@@ -1,4 +1,5 @@
1
1
  import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
+ import { NoCommitteeError } from '@aztec/ethereum/contracts';
2
3
  import type { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer';
3
4
  import type { EthAddress } from '@aztec/foundation/eth-address';
4
5
  import {
@@ -9,12 +10,13 @@ import {
9
10
  } from '@aztec/stdlib/p2p';
10
11
  import type { TxHash } from '@aztec/stdlib/tx';
11
12
 
13
+ import { jest } from '@jest/globals';
12
14
  import type { MockProxy } from 'jest-mock-extended';
13
15
 
14
16
  export interface ProposalValidatorTestParams<TProposal extends BlockProposal | CheckpointProposal> {
15
17
  validatorFactory: (
16
18
  epochCache: EpochCacheInterface,
17
- opts: { txsPermitted: boolean },
19
+ opts: { txsPermitted: boolean; maxTxsPerBlock?: number },
18
20
  ) => { validate: (proposal: TProposal) => Promise<ValidationResult> };
19
21
  makeProposal: (options?: any) => Promise<TProposal>;
20
22
  makeHeader: (epochNumber: number | bigint, slotNumber: number | bigint, blockNumber: number | bigint) => any;
@@ -105,6 +107,26 @@ export function sharedProposalValidatorTests<TProposal extends BlockProposal | C
105
107
  expect(result).toEqual({ result: 'ignore' });
106
108
  });
107
109
 
110
+ it('returns mid tolerance error if proposal has invalid signature', async () => {
111
+ const currentProposer = getSigner();
112
+ const header = makeHeader(1, 100, 100);
113
+ const mockProposal = await makeProposal({
114
+ blockHeader: header,
115
+ lastBlockHeader: header,
116
+ signer: currentProposer,
117
+ });
118
+
119
+ // Override getSender to return undefined (invalid signature)
120
+ jest.spyOn(mockProposal as any, 'getSender').mockReturnValue(undefined);
121
+
122
+ mockGetProposer(getAddress(currentProposer), getAddress());
123
+ const result = await validator.validate(mockProposal);
124
+ expect(result).toEqual({ result: 'reject', severity: PeerErrorSeverity.MidToleranceError });
125
+
126
+ // Should not try to resolve proposer if signature is invalid
127
+ expect(epochCache.getProposerAttesterAddressInSlot).not.toHaveBeenCalled();
128
+ });
129
+
108
130
  it('returns mid tolerance error if proposer is not current proposer for current slot', async () => {
109
131
  const currentProposer = getSigner();
110
132
  const nextProposer = getSigner();
@@ -152,6 +174,34 @@ export function sharedProposalValidatorTests<TProposal extends BlockProposal | C
152
174
  expect(result).toEqual({ result: 'reject', severity: PeerErrorSeverity.MidToleranceError });
153
175
  });
154
176
 
177
+ it('accepts proposal when proposer is undefined (open committee)', async () => {
178
+ const currentProposer = getSigner();
179
+ const header = makeHeader(1, 100, 100);
180
+ const mockProposal = await makeProposal({
181
+ blockHeader: header,
182
+ lastBlockHeader: header,
183
+ signer: currentProposer,
184
+ });
185
+
186
+ epochCache.getProposerAttesterAddressInSlot.mockResolvedValue(undefined);
187
+ const result = await validator.validate(mockProposal);
188
+ expect(result).toEqual({ result: 'accept' });
189
+ });
190
+
191
+ it('returns low tolerance error when getProposerAttesterAddressInSlot throws NoCommitteeError', async () => {
192
+ const currentProposer = getSigner();
193
+ const header = makeHeader(1, 100, 100);
194
+ const mockProposal = await makeProposal({
195
+ blockHeader: header,
196
+ lastBlockHeader: header,
197
+ signer: currentProposer,
198
+ });
199
+
200
+ epochCache.getProposerAttesterAddressInSlot.mockRejectedValue(new NoCommitteeError());
201
+ const result = await validator.validate(mockProposal);
202
+ expect(result).toEqual({ result: 'reject', severity: PeerErrorSeverity.LowToleranceError });
203
+ });
204
+
155
205
  it('returns undefined if proposal is valid for current slot and proposer', async () => {
156
206
  const currentProposer = getSigner();
157
207
  const nextProposer = getSigner();
@@ -226,5 +276,98 @@ export function sharedProposalValidatorTests<TProposal extends BlockProposal | C
226
276
  expect(result).toEqual({ result: 'accept' });
227
277
  });
228
278
  });
279
+
280
+ describe('embedded tx validation', () => {
281
+ it('returns mid tolerance error if embedded txs are not listed in txHashes', async () => {
282
+ const currentProposer = getSigner();
283
+ const txHashes = getTxHashes(2);
284
+ const header = makeHeader(1, 100, 100);
285
+ const mockProposal = await makeProposal({
286
+ blockHeader: header,
287
+ lastBlockHeader: header,
288
+ signer: currentProposer,
289
+ txHashes,
290
+ });
291
+
292
+ // Create a fake tx whose hash is NOT in txHashes
293
+ const fakeTxHash = getTxHashes(1)[0];
294
+ const fakeTx = { getTxHash: () => fakeTxHash, validateTxHash: () => Promise.resolve(true) };
295
+ Object.defineProperty(mockProposal, 'txs', { get: () => [fakeTx], configurable: true });
296
+
297
+ mockGetProposer(getAddress(currentProposer), getAddress());
298
+ const result = await validator.validate(mockProposal);
299
+ expect(result).toEqual({ result: 'reject', severity: PeerErrorSeverity.MidToleranceError });
300
+ });
301
+
302
+ it('returns low tolerance error if embedded tx has invalid tx hash', async () => {
303
+ const currentProposer = getSigner();
304
+ const txHashes = getTxHashes(2);
305
+ const header = makeHeader(1, 100, 100);
306
+ const mockProposal = await makeProposal({
307
+ blockHeader: header,
308
+ lastBlockHeader: header,
309
+ signer: currentProposer,
310
+ txHashes,
311
+ });
312
+
313
+ // Create a fake tx whose hash IS in txHashes but validateTxHash returns false
314
+ const fakeTx = { getTxHash: () => txHashes[0], validateTxHash: () => Promise.resolve(false) };
315
+ Object.defineProperty(mockProposal, 'txs', { get: () => [fakeTx], configurable: true });
316
+
317
+ mockGetProposer(getAddress(currentProposer), getAddress());
318
+ const result = await validator.validate(mockProposal);
319
+ expect(result).toEqual({ result: 'reject', severity: PeerErrorSeverity.LowToleranceError });
320
+ });
321
+ });
322
+
323
+ describe('maxTxsPerBlock validation', () => {
324
+ it('rejects proposal when txHashes exceed maxTxsPerBlock', async () => {
325
+ const validatorWithMaxTxs = validatorFactory(epochCache, { txsPermitted: true, maxTxsPerBlock: 2 });
326
+ const currentProposer = getSigner();
327
+ const header = makeHeader(1, 100, 100);
328
+ const mockProposal = await makeProposal({
329
+ blockHeader: header,
330
+ lastBlockHeader: header,
331
+ signer: currentProposer,
332
+ txHashes: getTxHashes(3),
333
+ });
334
+
335
+ mockGetProposer(getAddress(currentProposer), getAddress());
336
+ const result = await validatorWithMaxTxs.validate(mockProposal);
337
+ expect(result).toEqual({ result: 'reject', severity: PeerErrorSeverity.MidToleranceError });
338
+ });
339
+
340
+ it('accepts proposal when txHashes count equals maxTxsPerBlock', async () => {
341
+ const validatorWithMaxTxs = validatorFactory(epochCache, { txsPermitted: true, maxTxsPerBlock: 2 });
342
+ const currentProposer = getSigner();
343
+ const header = makeHeader(1, 100, 100);
344
+ const mockProposal = await makeProposal({
345
+ blockHeader: header,
346
+ lastBlockHeader: header,
347
+ signer: currentProposer,
348
+ txHashes: getTxHashes(2),
349
+ });
350
+
351
+ mockGetProposer(getAddress(currentProposer), getAddress());
352
+ const result = await validatorWithMaxTxs.validate(mockProposal);
353
+ expect(result).toEqual({ result: 'accept' });
354
+ });
355
+
356
+ it('accepts proposal when maxTxsPerBlock is not set (unlimited)', async () => {
357
+ // Default validator has no maxTxsPerBlock
358
+ const currentProposer = getSigner();
359
+ const header = makeHeader(1, 100, 100);
360
+ const mockProposal = await makeProposal({
361
+ blockHeader: header,
362
+ lastBlockHeader: header,
363
+ signer: currentProposer,
364
+ txHashes: getTxHashes(10),
365
+ });
366
+
367
+ mockGetProposer(getAddress(currentProposer), getAddress());
368
+ const result = await validator.validate(mockProposal);
369
+ expect(result).toEqual({ result: 'accept' });
370
+ });
371
+ });
229
372
  });
230
373
  }
@@ -1,11 +1,11 @@
1
1
  // Taken from lodestar: https://github.com/ChainSafe/lodestar
2
- import { sha256 } from '@aztec/foundation/crypto/sha256';
3
2
  import { createLogger } from '@aztec/foundation/log';
4
3
  import { MAX_TX_SIZE_KB, TopicType, getTopicFromString } from '@aztec/stdlib/p2p';
5
4
 
6
5
  import type { RPC } from '@chainsafe/libp2p-gossipsub/message';
7
6
  import type { DataTransform } from '@chainsafe/libp2p-gossipsub/types';
8
7
  import type { Message } from '@libp2p/interface';
8
+ import { webcrypto } from 'node:crypto';
9
9
  import { compressSync, uncompressSync } from 'snappy';
10
10
  import xxhashFactory from 'xxhash-wasm';
11
11
 
@@ -44,11 +44,10 @@ export function msgIdToStrFn(msgId: Uint8Array): string {
44
44
  * @param message - The libp2p message
45
45
  * @returns The message identifier
46
46
  */
47
- export function getMsgIdFn(message: Message) {
48
- const { topic } = message;
49
-
50
- const vec = [Buffer.from(topic), message.data];
51
- return sha256(Buffer.concat(vec)).subarray(0, 20);
47
+ export async function getMsgIdFn({ topic, data }: Message): Promise<Uint8Array> {
48
+ const buffer = Buffer.concat([Buffer.from(topic), data]);
49
+ const hash = await webcrypto.subtle.digest('SHA-256', buffer);
50
+ return Buffer.from(hash.slice(0, 20));
52
51
  }
53
52
 
54
53
  const DefaultMaxSizesKb: Record<TopicType, number> = {