@aztec/p2p 0.0.1-commit.f504929 → 0.0.1-commit.f81dbcf

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 (139) 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 +7 -2
  4. package/dest/client/p2p_client.d.ts +1 -1
  5. package/dest/client/p2p_client.d.ts.map +1 -1
  6. package/dest/client/p2p_client.js +8 -26
  7. package/dest/config.d.ts +24 -2
  8. package/dest/config.d.ts.map +1 -1
  9. package/dest/config.js +65 -6
  10. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +1 -1
  11. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  12. package/dest/mem_pools/attestation_pool/attestation_pool.js +1 -5
  13. package/dest/mem_pools/instrumentation.d.ts +2 -4
  14. package/dest/mem_pools/instrumentation.d.ts.map +1 -1
  15. package/dest/mem_pools/instrumentation.js +14 -16
  16. package/dest/mem_pools/tx_pool/priority.d.ts +2 -2
  17. package/dest/mem_pools/tx_pool/priority.d.ts.map +1 -1
  18. package/dest/mem_pools/tx_pool/priority.js +4 -4
  19. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts +1 -1
  20. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts.map +1 -1
  21. package/dest/mem_pools/tx_pool/tx_pool_test_suite.js +3 -1
  22. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts +7 -1
  23. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts.map +1 -1
  24. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts +1 -1
  25. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts.map +1 -1
  26. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.js +8 -6
  27. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts +2 -2
  28. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts.map +1 -1
  29. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.js +2 -2
  30. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +3 -3
  31. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  32. package/dest/mem_pools/tx_pool_v2/interfaces.js +1 -1
  33. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +13 -4
  34. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  35. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +26 -9
  36. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +1 -1
  37. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  38. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +0 -3
  39. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +1 -2
  40. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  41. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +2 -11
  42. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts +5 -4
  43. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts.map +1 -1
  44. package/dest/msg_validators/proposal_validator/block_proposal_validator.js +10 -2
  45. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts +5 -4
  46. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts.map +1 -1
  47. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.js +16 -2
  48. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +12 -9
  49. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  50. package/dest/msg_validators/proposal_validator/proposal_validator.js +46 -44
  51. package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts +1 -1
  52. package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts.map +1 -1
  53. package/dest/msg_validators/tx_validator/allowed_public_setup.js +21 -32
  54. package/dest/msg_validators/tx_validator/allowed_setup_helpers.d.ts +17 -0
  55. package/dest/msg_validators/tx_validator/allowed_setup_helpers.d.ts.map +1 -0
  56. package/dest/msg_validators/tx_validator/allowed_setup_helpers.js +24 -0
  57. package/dest/msg_validators/tx_validator/contract_instance_validator.d.ts +9 -0
  58. package/dest/msg_validators/tx_validator/contract_instance_validator.d.ts.map +1 -0
  59. package/dest/msg_validators/tx_validator/contract_instance_validator.js +48 -0
  60. package/dest/msg_validators/tx_validator/factory.d.ts +15 -4
  61. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  62. package/dest/msg_validators/tx_validator/factory.js +21 -8
  63. package/dest/msg_validators/tx_validator/fee_payer_balance.d.ts +1 -1
  64. package/dest/msg_validators/tx_validator/fee_payer_balance.d.ts.map +1 -1
  65. package/dest/msg_validators/tx_validator/fee_payer_balance.js +6 -2
  66. package/dest/msg_validators/tx_validator/gas_validator.d.ts +13 -4
  67. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  68. package/dest/msg_validators/tx_validator/gas_validator.js +39 -9
  69. package/dest/msg_validators/tx_validator/index.d.ts +2 -1
  70. package/dest/msg_validators/tx_validator/index.d.ts.map +1 -1
  71. package/dest/msg_validators/tx_validator/index.js +1 -0
  72. package/dest/msg_validators/tx_validator/phases_validator.d.ts +1 -1
  73. package/dest/msg_validators/tx_validator/phases_validator.d.ts.map +1 -1
  74. package/dest/msg_validators/tx_validator/phases_validator.js +21 -1
  75. package/dest/services/libp2p/libp2p_service.d.ts +1 -1
  76. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  77. package/dest/services/libp2p/libp2p_service.js +20 -14
  78. package/dest/services/peer-manager/metrics.d.ts +1 -3
  79. package/dest/services/peer-manager/metrics.d.ts.map +1 -1
  80. package/dest/services/peer-manager/metrics.js +0 -6
  81. package/dest/services/peer-manager/peer_manager.d.ts +1 -1
  82. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  83. package/dest/services/peer-manager/peer_manager.js +1 -2
  84. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts +1 -1
  85. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts.map +1 -1
  86. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.js +37 -14
  87. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts +11 -17
  88. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts.map +1 -1
  89. package/dest/services/reqresp/batch-tx-requester/peer_collection.js +15 -49
  90. package/dest/services/reqresp/reqresp.d.ts +1 -1
  91. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  92. package/dest/services/reqresp/reqresp.js +1 -2
  93. package/dest/test-helpers/testbench-utils.d.ts +1 -1
  94. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  95. package/dest/test-helpers/testbench-utils.js +2 -1
  96. package/dest/testbench/p2p_client_testbench_worker.js +2 -1
  97. package/dest/testbench/worker_client_manager.d.ts +3 -1
  98. package/dest/testbench/worker_client_manager.d.ts.map +1 -1
  99. package/dest/testbench/worker_client_manager.js +4 -1
  100. package/package.json +14 -14
  101. package/src/client/factory.ts +7 -1
  102. package/src/client/p2p_client.ts +5 -26
  103. package/src/config.ts +92 -4
  104. package/src/mem_pools/attestation_pool/attestation_pool.ts +4 -5
  105. package/src/mem_pools/instrumentation.ts +13 -17
  106. package/src/mem_pools/tx_pool/priority.ts +4 -4
  107. package/src/mem_pools/tx_pool/tx_pool_test_suite.ts +3 -1
  108. package/src/mem_pools/tx_pool_v2/README.md +9 -1
  109. package/src/mem_pools/tx_pool_v2/eviction/interfaces.ts +11 -1
  110. package/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.ts +15 -6
  111. package/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.ts +2 -1
  112. package/src/mem_pools/tx_pool_v2/interfaces.ts +3 -3
  113. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +37 -7
  114. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +0 -3
  115. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +3 -12
  116. package/src/msg_validators/proposal_validator/block_proposal_validator.ts +13 -3
  117. package/src/msg_validators/proposal_validator/checkpoint_proposal_validator.ts +19 -6
  118. package/src/msg_validators/proposal_validator/proposal_validator.ts +57 -48
  119. package/src/msg_validators/tx_validator/allowed_public_setup.ts +16 -35
  120. package/src/msg_validators/tx_validator/allowed_setup_helpers.ts +31 -0
  121. package/src/msg_validators/tx_validator/contract_instance_validator.ts +56 -0
  122. package/src/msg_validators/tx_validator/factory.ts +22 -2
  123. package/src/msg_validators/tx_validator/fee_payer_balance.ts +6 -2
  124. package/src/msg_validators/tx_validator/gas_validator.ts +41 -8
  125. package/src/msg_validators/tx_validator/index.ts +1 -0
  126. package/src/msg_validators/tx_validator/phases_validator.ts +29 -0
  127. package/src/services/libp2p/libp2p_service.ts +21 -13
  128. package/src/services/peer-manager/metrics.ts +0 -7
  129. package/src/services/peer-manager/peer_manager.ts +1 -2
  130. package/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +42 -14
  131. package/src/services/reqresp/batch-tx-requester/peer_collection.ts +24 -63
  132. package/src/services/reqresp/reqresp.ts +1 -3
  133. package/src/test-helpers/testbench-utils.ts +1 -0
  134. package/src/testbench/p2p_client_testbench_worker.ts +1 -0
  135. package/src/testbench/worker_client_manager.ts +11 -4
  136. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts +0 -24
  137. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts.map +0 -1
  138. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.js +0 -378
  139. package/src/msg_validators/proposal_validator/proposal_validator_test_suite.ts +0 -373
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type ConfigMappingsType,
3
3
  SecretValue,
4
+ bigintConfigHelper,
4
5
  booleanConfigHelper,
5
6
  getConfigFromMappings,
6
7
  getDefaultConfig,
@@ -39,6 +40,18 @@ export interface P2PConfig
39
40
  TxCollectionConfig,
40
41
  TxFileStoreConfig,
41
42
  Pick<SequencerConfig, 'blockDurationMs' | 'expectedBlockProposalsPerSlot' | 'maxTxsPerBlock'> {
43
+ /** Maximum transactions per block for validation. Overrides maxTxsPerBlock for gossip validation when set. */
44
+ validateMaxTxsPerBlock?: number;
45
+
46
+ /** Maximum transactions per checkpoint for validation. Used as fallback for maxTxsPerBlock when that is not set. */
47
+ validateMaxTxsPerCheckpoint?: number;
48
+
49
+ /** Maximum L2 gas per block for validation. When set, txs exceeding this limit are rejected. */
50
+ validateMaxL2BlockGas?: number;
51
+
52
+ /** Maximum DA gas per block for validation. When set, txs exceeding this limit are rejected. */
53
+ validateMaxDABlockGas?: number;
54
+
42
55
  /** A flag dictating whether the P2P subsystem should be enabled. */
43
56
  p2pEnabled: boolean;
44
57
 
@@ -173,7 +186,10 @@ export interface P2PConfig
173
186
  /** Whether transactions are disabled for this node. This means transactions will be rejected at the RPC and P2P layers. */
174
187
  disableTransactions: boolean;
175
188
 
176
- /** The probability that a transaction is discarded (0 = disabled). - For testing purposes only */
189
+ /** True to simulate discarding transactions. - For testing purposes only*/
190
+ dropTransactions: boolean;
191
+
192
+ /** The probability that a transaction is discarded. - For testing purposes only */
177
193
  dropTransactionsProbability: number;
178
194
 
179
195
  /** Whether to delete transactions from the pool after a reorg instead of moving them back to pending. */
@@ -190,11 +206,36 @@ export interface P2PConfig
190
206
 
191
207
  /** Minimum age (ms) a transaction must have been in the pool before it's eligible for block building. */
192
208
  minTxPoolAgeMs: number;
209
+
210
+ /** Minimum percentage fee increase required to replace an existing tx via RPC (0 = no bump). */
211
+ priceBumpPercentage: bigint;
193
212
  }
194
213
 
195
214
  export const DEFAULT_P2P_PORT = 40400;
196
215
 
197
216
  export const p2pConfigMappings: ConfigMappingsType<P2PConfig> = {
217
+ validateMaxTxsPerBlock: {
218
+ env: 'VALIDATOR_MAX_TX_PER_BLOCK',
219
+ description:
220
+ 'Maximum transactions per block for validation. Overrides maxTxsPerBlock for gossip validation when set.',
221
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
222
+ },
223
+ validateMaxTxsPerCheckpoint: {
224
+ env: 'VALIDATOR_MAX_TX_PER_CHECKPOINT',
225
+ description:
226
+ 'Maximum transactions per checkpoint for validation. Used as fallback for maxTxsPerBlock when that is not set.',
227
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
228
+ },
229
+ validateMaxL2BlockGas: {
230
+ env: 'VALIDATOR_MAX_L2_BLOCK_GAS',
231
+ description: 'Maximum L2 gas per block for validation. When set, txs exceeding this limit are rejected.',
232
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
233
+ },
234
+ validateMaxDABlockGas: {
235
+ env: 'VALIDATOR_MAX_DA_BLOCK_GAS',
236
+ description: 'Maximum DA gas per block for validation. When set, txs exceeding this limit are rejected.',
237
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
238
+ },
198
239
  p2pEnabled: {
199
240
  env: 'P2P_ENABLED',
200
241
  description: 'A flag dictating whether the P2P subsystem should be enabled.',
@@ -397,9 +438,9 @@ export const p2pConfigMappings: ConfigMappingsType<P2PConfig> = {
397
438
  env: 'TX_PUBLIC_SETUP_ALLOWLIST',
398
439
  parseEnv: (val: string) => parseAllowList(val),
399
440
  description:
400
- 'Additional entries to extend the default setup allow list. Format: I:address:selector,C:classId:selector',
441
+ 'Additional entries to extend the default setup allow list. Format: I:address:selector[:flags],C:classId:selector[:flags]. Flags: os (onlySelf), rn (rejectNullMsgSender), cl=N (calldataLength), joined with +.',
401
442
  printDefault: () =>
402
- 'Default: AuthRegistry._set_authorized, FeeJuice._increase_public_balance, Token._increase_public_balance, Token.transfer_in_public',
443
+ 'Default: AuthRegistry._set_authorized, AuthRegistry.set_authorized, FeeJuice._increase_public_balance',
403
444
  },
404
445
  maxPendingTxCount: {
405
446
  env: 'P2P_MAX_PENDING_TX_COUNT',
@@ -428,6 +469,11 @@ export const p2pConfigMappings: ConfigMappingsType<P2PConfig> = {
428
469
  description: 'Number of auth attempts to allow before peer is banned. Number is inclusive',
429
470
  ...numberConfigHelper(3),
430
471
  },
472
+ dropTransactions: {
473
+ env: 'P2P_DROP_TX',
474
+ description: 'True to simulate discarding transactions. - For testing purposes only',
475
+ ...booleanConfigHelper(false),
476
+ },
431
477
  dropTransactionsProbability: {
432
478
  env: 'P2P_DROP_TX_CHANCE',
433
479
  description: 'The probability that a transaction is discarded (0 - 1). - For testing purposes only',
@@ -465,6 +511,12 @@ export const p2pConfigMappings: ConfigMappingsType<P2PConfig> = {
465
511
  description: 'Minimum age (ms) a transaction must have been in the pool before it is eligible for block building.',
466
512
  ...numberConfigHelper(2_000),
467
513
  },
514
+ priceBumpPercentage: {
515
+ env: 'P2P_RPC_PRICE_BUMP_PERCENTAGE',
516
+ description:
517
+ 'Minimum percentage fee increase required to replace an existing tx via RPC. Even at 0%, replacement still requires paying at least 1 unit more.',
518
+ ...bigintConfigHelper(10n),
519
+ },
468
520
  ...sharedSequencerConfigMappings,
469
521
  ...p2pReqRespConfigMappings,
470
522
  ...batchTxRequesterConfigMappings,
@@ -522,12 +574,45 @@ export const bootnodeConfigMappings = pickConfigMappings(
522
574
  bootnodeConfigKeys,
523
575
  );
524
576
 
577
+ /**
578
+ * Parses a `+`-separated flags string into validation properties for an allow list entry.
579
+ * Supported flags: `os` (onlySelf), `rn` (rejectNullMsgSender), `cl=N` (calldataLength).
580
+ */
581
+ function parseFlags(
582
+ flags: string,
583
+ entry: string,
584
+ ): { onlySelf?: boolean; rejectNullMsgSender?: boolean; calldataLength?: number } {
585
+ const result: { onlySelf?: boolean; rejectNullMsgSender?: boolean; calldataLength?: number } = {};
586
+ for (const flag of flags.split('+')) {
587
+ if (flag === 'os') {
588
+ result.onlySelf = true;
589
+ } else if (flag === 'rn') {
590
+ result.rejectNullMsgSender = true;
591
+ } else if (flag.startsWith('cl=')) {
592
+ const n = parseInt(flag.slice(3), 10);
593
+ if (isNaN(n) || n < 0) {
594
+ throw new Error(
595
+ `Invalid allow list entry "${entry}": invalid calldataLength in flag "${flag}". Expected a non-negative integer.`,
596
+ );
597
+ }
598
+ result.calldataLength = n;
599
+ } else {
600
+ throw new Error(`Invalid allow list entry "${entry}": unknown flag "${flag}". Supported flags: os, rn, cl=N.`);
601
+ }
602
+ }
603
+ return result;
604
+ }
605
+
525
606
  /**
526
607
  * Parses a string to a list of allowed elements.
527
608
  * Each entry is expected to be of one of the following formats:
528
609
  * `I:${address}:${selector}` — instance (contract address) with function selector
529
610
  * `C:${classId}:${selector}` — class with function selector
530
611
  *
612
+ * An optional flags segment can be appended after the selector:
613
+ * `I:${address}:${selector}:${flags}` or `C:${classId}:${selector}:${flags}`
614
+ * where flags is a `+`-separated list of: `os` (onlySelf), `rn` (rejectNullMsgSender), `cl=N` (calldataLength).
615
+ *
531
616
  * @param value The string to parse
532
617
  * @returns A list of allowed elements
533
618
  */
@@ -543,7 +628,7 @@ export function parseAllowList(value: string): AllowedElement[] {
543
628
  if (!trimmed) {
544
629
  continue;
545
630
  }
546
- const [typeString, identifierString, selectorString] = trimmed.split(':');
631
+ const [typeString, identifierString, selectorString, flagsString] = trimmed.split(':');
547
632
 
548
633
  if (!selectorString) {
549
634
  throw new Error(
@@ -552,16 +637,19 @@ export function parseAllowList(value: string): AllowedElement[] {
552
637
  }
553
638
 
554
639
  const selector = FunctionSelector.fromString(selectorString);
640
+ const flags = flagsString ? parseFlags(flagsString, trimmed) : {};
555
641
 
556
642
  if (typeString === 'I') {
557
643
  entries.push({
558
644
  address: AztecAddress.fromString(identifierString),
559
645
  selector,
646
+ ...flags,
560
647
  });
561
648
  } else if (typeString === 'C') {
562
649
  entries.push({
563
650
  classId: Fr.fromHexString(identifierString),
564
651
  selector,
652
+ ...flags,
565
653
  });
566
654
  } else {
567
655
  throw new Error(
@@ -359,10 +359,11 @@ export class AttestationPool {
359
359
  }
360
360
 
361
361
  const address = sender.toString();
362
- const ownKey = this.getAttestationKey(slotNumber, proposalId, address);
363
362
 
364
- await this.checkpointAttestations.set(ownKey, attestation.toBuffer());
365
- this.metrics.trackMempoolItemAdded(ownKey);
363
+ await this.checkpointAttestations.set(
364
+ this.getAttestationKey(slotNumber, proposalId, address),
365
+ attestation.toBuffer(),
366
+ );
366
367
 
367
368
  this.log.debug(`Added own checkpoint attestation for slot ${slotNumber} from ${address}`, {
368
369
  signature: attestation.signature.toString(),
@@ -428,7 +429,6 @@ export class AttestationPool {
428
429
  const attestationEndKey = new Fr(oldestSlot).toString();
429
430
  for await (const key of this.checkpointAttestations.keysAsync({ end: attestationEndKey })) {
430
431
  await this.checkpointAttestations.delete(key);
431
- this.metrics.trackMempoolItemRemoved(key);
432
432
  numberOfAttestations++;
433
433
  }
434
434
 
@@ -526,7 +526,6 @@ export class AttestationPool {
526
526
 
527
527
  // Add the attestation
528
528
  await this.checkpointAttestations.set(key, attestation.toBuffer());
529
- this.metrics.trackMempoolItemAdded(key);
530
529
 
531
530
  // Track this attestation in the per-signer-per-slot index for duplicate detection
532
531
  const slotSignerKey = this.getSlotSignerKey(slotNumber, signerAddress);
@@ -73,7 +73,7 @@ export class PoolInstrumentation<PoolObject extends Gossipable> {
73
73
  private defaultAttributes;
74
74
  private meter: Meter;
75
75
 
76
- private mempoolItemAddedTimestamp: Map<bigint | string, number> = new Map<bigint | string, number>();
76
+ private txAddedTimestamp: Map<bigint, number> = new Map<bigint, number>();
77
77
 
78
78
  constructor(
79
79
  telemetry: TelemetryClient,
@@ -114,26 +114,22 @@ export class PoolInstrumentation<PoolObject extends Gossipable> {
114
114
  }
115
115
 
116
116
  public transactionsAdded(transactions: Tx[]) {
117
- transactions.forEach(tx => this.trackMempoolItemAdded(tx.txHash.toBigInt()));
118
- }
119
-
120
- public transactionsRemoved(hashes: Iterable<bigint> | Iterable<string>) {
121
- for (const hash of hashes) {
122
- this.trackMempoolItemRemoved(BigInt(hash));
117
+ const timestamp = Date.now();
118
+ for (const transaction of transactions) {
119
+ this.txAddedTimestamp.set(transaction.txHash.toBigInt(), timestamp);
123
120
  }
124
121
  }
125
122
 
126
- public trackMempoolItemAdded(key: bigint | string): void {
127
- this.mempoolItemAddedTimestamp.set(key, Date.now());
128
- }
129
-
130
- public trackMempoolItemRemoved(key: bigint | string): void {
123
+ public transactionsRemoved(hashes: Iterable<bigint> | Iterable<string>) {
131
124
  const timestamp = Date.now();
132
- const addedAt = this.mempoolItemAddedTimestamp.get(key);
133
- if (addedAt !== undefined) {
134
- this.mempoolItemAddedTimestamp.delete(key);
135
- if (addedAt < timestamp) {
136
- this.minedDelay.record(timestamp - addedAt);
125
+ for (const hash of hashes) {
126
+ const key = BigInt(hash);
127
+ const addedAt = this.txAddedTimestamp.get(key);
128
+ if (addedAt !== undefined) {
129
+ this.txAddedTimestamp.delete(key);
130
+ if (addedAt < timestamp) {
131
+ this.minedDelay.record(timestamp - addedAt);
132
+ }
137
133
  }
138
134
  }
139
135
  }
@@ -1,3 +1,4 @@
1
+ import { minBigint } from '@aztec/foundation/bigint';
1
2
  import { Buffer32 } from '@aztec/foundation/buffer';
2
3
  import type { Tx } from '@aztec/stdlib/tx';
3
4
 
@@ -11,10 +12,9 @@ export function getPendingTxPriority(tx: Tx): string {
11
12
  }
12
13
 
13
14
  /**
14
- * Returns the priority of a tx.
15
+ * Returns the priority of a tx based on the L2 priority fee only, capped by the max fees per gas.
15
16
  */
16
17
  export function getTxPriorityFee(tx: Tx): bigint {
17
- const priorityFees = tx.getGasSettings().maxPriorityFeesPerGas;
18
- const totalFees = priorityFees.feePerDaGas + priorityFees.feePerL2Gas;
19
- return totalFees;
18
+ const { maxPriorityFeesPerGas: priorityFees, maxFeesPerGas } = tx.getGasSettings();
19
+ return minBigint(maxFeesPerGas.feePerL2Gas, priorityFees.feePerL2Gas);
20
20
  }
@@ -204,7 +204,9 @@ export function describeTxPool(getTxPool: () => TxPool) {
204
204
 
205
205
  it('returns pending tx hashes sorted by priority', async () => {
206
206
  const withPriorityFee = (tx: Tx, fee: number) => {
207
- unfreeze(tx.data.constants.txContext.gasSettings).maxPriorityFeesPerGas = new GasFees(fee, fee);
207
+ const gs = unfreeze(tx.data.constants.txContext.gasSettings);
208
+ gs.maxPriorityFeesPerGas = new GasFees(fee, fee);
209
+ gs.maxFeesPerGas = new GasFees(fee, fee);
208
210
  return tx;
209
211
  };
210
212
 
@@ -158,7 +158,7 @@ Checked before adding a transaction to the pending pool:
158
158
 
159
159
  | Rule | Purpose |
160
160
  |------|---------|
161
- | `NullifierConflictRule` | Handles transactions with conflicting nullifiers. Higher priority tx wins. |
161
+ | `NullifierConflictRule` | Handles transactions with conflicting nullifiers. Higher priority tx wins. For RPC submissions, a configurable price bump percentage is required. |
162
162
  | `FeePayerBalancePreAddRule` | Ensures fee payer has sufficient balance for all their pending txs. |
163
163
  | `LowPriorityPreAddRule` | Rejects txs when pool is full and new tx has lowest priority. |
164
164
 
@@ -233,6 +233,14 @@ await pool.updateConfig({
233
233
  });
234
234
  ```
235
235
 
236
+ ### Price Bump (RPC Transaction Replacement)
237
+
238
+ When a transaction is submitted via RPC and clashes on nullifiers with an existing pool transaction, the incoming tx must pay at least `priceBumpPercentage`% more in priority fee (i.e. `>= existingFee + existingFee * bump / 100`) to replace it. This prevents spam via small fee increments. The same bump applies when the pool is full and the incoming tx needs to evict the lowest-priority tx.
239
+
240
+ - **Env var**: `P2P_RPC_PRICE_BUMP_PERCENTAGE` (default: 10)
241
+ - **Scope**: RPC submissions only. P2P gossip uses `comparePriority` (fee + hash tiebreaker) with no bump.
242
+ - Even with a 0% bump, a replacement tx must pay at least 1 unit more than the existing fee.
243
+
236
244
  ## Return Values
237
245
 
238
246
  ### AddTxsResult
@@ -100,7 +100,15 @@ export type TxPoolRejectionError =
100
100
  availableBalance: bigint;
101
101
  feeLimit: bigint;
102
102
  }
103
- | { code: typeof TxPoolRejectionCode.NULLIFIER_CONFLICT; message: string; conflictingTxHash: string }
103
+ | {
104
+ code: typeof TxPoolRejectionCode.NULLIFIER_CONFLICT;
105
+ message: string;
106
+ conflictingTxHash: string;
107
+ /** Minimum fee needed to replace the conflicting tx (only set when price bump applies). */
108
+ minimumPriceBumpFee?: bigint;
109
+ /** Incoming tx's priority fee. */
110
+ txPriorityFee?: bigint;
111
+ }
104
112
  | { code: typeof TxPoolRejectionCode.INTERNAL_ERROR; message: string };
105
113
 
106
114
  /**
@@ -121,6 +129,8 @@ export interface PreAddResult {
121
129
  export interface PreAddContext {
122
130
  /** If true, compare priority fee only (no tx hash tiebreaker). Used for RPC submissions. */
123
131
  feeComparisonOnly?: boolean;
132
+ /** Percentage-based price bump required for tx replacement. Only set for RPC submissions. */
133
+ priceBumpPercentage?: bigint;
124
134
  }
125
135
 
126
136
  /**
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from '@aztec/foundation/log';
2
2
 
3
- import { type TxMetaData, comparePriority } from '../tx_metadata.js';
3
+ import { type TxMetaData, comparePriority, getMinimumPriceBumpFee } from '../tx_metadata.js';
4
4
  import {
5
5
  type EvictionConfig,
6
6
  type PreAddContext,
@@ -48,10 +48,14 @@ export class LowPriorityPreAddRule implements PreAddRule {
48
48
  }
49
49
 
50
50
  // Compare incoming tx against lowest priority tx.
51
- // feeOnly mode (RPC): use strict fee comparison only — avoids churn from hash ordering
52
- // Default (gossip): use full comparePriority (fee + tx hash tiebreaker) for determinism
51
+ // feeOnly mode (RPC): use strict fee comparison only — avoids churn from hash ordering.
52
+ // When price bump is also set, require the bumped fee threshold.
53
+ // Default (gossip): use full comparePriority (fee + tx hash tiebreaker) for determinism.
53
54
  const isHigherPriority = context?.feeComparisonOnly
54
- ? incomingMeta.priorityFee > lowestPriorityMeta.priorityFee
55
+ ? context.priceBumpPercentage !== undefined
56
+ ? incomingMeta.priorityFee >=
57
+ getMinimumPriceBumpFee(lowestPriorityMeta.priorityFee, context.priceBumpPercentage)
58
+ : incomingMeta.priorityFee > lowestPriorityMeta.priorityFee
55
59
  : comparePriority(incomingMeta, lowestPriorityMeta) > 0;
56
60
 
57
61
  if (isHigherPriority) {
@@ -66,6 +70,11 @@ export class LowPriorityPreAddRule implements PreAddRule {
66
70
  }
67
71
 
68
72
  // Incoming tx has equal or lower priority - ignore it (it would be evicted anyway)
73
+ const minimumFee =
74
+ context?.feeComparisonOnly && context.priceBumpPercentage !== undefined
75
+ ? getMinimumPriceBumpFee(lowestPriorityMeta.priorityFee, context.priceBumpPercentage)
76
+ : lowestPriorityMeta.priorityFee + 1n;
77
+
69
78
  this.log.debug(
70
79
  `Pool at capacity (${currentCount}/${this.maxPoolSize}), ignoring ${incomingMeta.txHash} ` +
71
80
  `(priority ${incomingMeta.priorityFee}) - lower than existing minimum (priority ${lowestPriorityMeta.priorityFee})`,
@@ -75,8 +84,8 @@ export class LowPriorityPreAddRule implements PreAddRule {
75
84
  txHashesToEvict: [],
76
85
  reason: {
77
86
  code: TxPoolRejectionCode.LOW_PRIORITY_FEE,
78
- message: `Tx does not meet minimum priority fee. Required: ${lowestPriorityMeta.priorityFee + 1n}, got: ${incomingMeta.priorityFee}`,
79
- minimumPriorityFee: lowestPriorityMeta.priorityFee + 1n,
87
+ message: `Tx does not meet minimum priority fee. Required: ${minimumFee}, got: ${incomingMeta.priorityFee}`,
88
+ minimumPriorityFee: minimumFee,
80
89
  txPriorityFee: incomingMeta.priorityFee,
81
90
  },
82
91
  });
@@ -15,11 +15,12 @@ export class NullifierConflictRule implements PreAddRule {
15
15
 
16
16
  private log = createLogger('p2p:tx_pool_v2:nullifier_conflict_rule');
17
17
 
18
- check(incomingMeta: TxMetaData, poolAccess: PreAddPoolAccess, _context?: PreAddContext): Promise<PreAddResult> {
18
+ check(incomingMeta: TxMetaData, poolAccess: PreAddPoolAccess, context?: PreAddContext): Promise<PreAddResult> {
19
19
  const result = checkNullifierConflict(
20
20
  incomingMeta,
21
21
  nullifier => poolAccess.getTxHashByNullifier(nullifier),
22
22
  txHash => poolAccess.getMetadata(txHash),
23
+ context?.priceBumpPercentage,
23
24
  );
24
25
 
25
26
  if (result.shouldIgnore) {
@@ -44,8 +44,8 @@ export type TxPoolV2Config = {
44
44
  minTxPoolAgeMs: number;
45
45
  /** Maximum number of evicted tx hashes to remember for metrics tracking */
46
46
  evictedTxCacheSize: number;
47
- /** The probability (0-1) that a transaction is discarded. 0 disables dropping. For testing purposes only. */
48
- dropTransactionsProbability: number;
47
+ /** Minimum percentage fee increase required to replace an existing tx via RPC (0 = no bump). */
48
+ priceBumpPercentage: bigint;
49
49
  };
50
50
 
51
51
  /**
@@ -56,7 +56,7 @@ export const DEFAULT_TX_POOL_V2_CONFIG: TxPoolV2Config = {
56
56
  archivedTxLimit: 0, // 0 = disabled
57
57
  minTxPoolAgeMs: 2_000,
58
58
  evictedTxCacheSize: 10_000,
59
- dropTransactionsProbability: 0,
59
+ priceBumpPercentage: 10n,
60
60
  };
61
61
 
62
62
  /**
@@ -190,21 +190,38 @@ export function comparePriority(a: PriorityComparable, b: PriorityComparable): n
190
190
  return compareTxHash(a.txHashBigInt, b.txHashBigInt);
191
191
  }
192
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;
203
+ }
204
+
193
205
  /**
194
206
  * Checks for nullifier conflicts between an incoming transaction and existing pool state.
195
207
  *
196
208
  * When the incoming tx shares nullifiers with existing pending txs:
197
- * - If the incoming tx has strictly higher priority, mark conflicting txs for eviction
198
- * - 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`.
199
214
  *
200
215
  * @param incomingMeta - Metadata for the incoming transaction
201
216
  * @param getTxHashByNullifier - Accessor to find which tx uses a nullifier
202
217
  * @param getMetadata - Accessor to get metadata for a tx hash
218
+ * @param priceBumpPercentage - Optional percentage bump required for fee-based replacement
203
219
  */
204
220
  export function checkNullifierConflict(
205
221
  incomingMeta: TxMetaData,
206
222
  getTxHashByNullifier: (nullifier: string) => string | undefined,
207
223
  getMetadata: (txHash: string) => TxMetaData | undefined,
224
+ priceBumpPercentage?: bigint,
208
225
  ): PreAddResult {
209
226
  const txHashesToEvict: string[] = [];
210
227
 
@@ -225,19 +242,32 @@ export function checkNullifierConflict(
225
242
  continue;
226
243
  }
227
244
 
228
- // If incoming tx has strictly higher priority, mark for eviction
229
- // Otherwise, ignore incoming tx (ties go to existing tx)
230
- // Use comparePriority for deterministic ordering (includes txHash as tiebreaker)
231
- 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) {
232
253
  txHashesToEvict.push(conflictingHashStr);
233
254
  } else {
255
+ const minimumFee =
256
+ priceBumpPercentage !== undefined
257
+ ? getMinimumPriceBumpFee(conflictingMeta.priorityFee, priceBumpPercentage)
258
+ : undefined;
234
259
  return {
235
260
  shouldIgnore: true,
236
261
  txHashesToEvict: [],
237
262
  reason: {
238
263
  code: TxPoolRejectionCode.NULLIFIER_CONFLICT,
239
- 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}`,
240
268
  conflictingTxHash: conflictingHashStr,
269
+ minimumPriceBumpFee: minimumFee,
270
+ txPriorityFee: minimumFee !== undefined ? incomingMeta.priorityFee : undefined,
241
271
  },
242
272
  };
243
273
  }
@@ -58,9 +58,6 @@ export class AztecKVTxPoolV2 extends (EventEmitter as new () => TypedEventEmitte
58
58
  const hashes = txHashes.map(h => (typeof h === 'string' ? TxHash.fromString(h) : TxHash.fromBigInt(h)));
59
59
  this.emit('txs-removed', { txHashes: hashes });
60
60
  },
61
- onTxsMined: (txHashes: string[]) => {
62
- this.#metrics?.transactionsRemoved(txHashes);
63
- },
64
61
  };
65
62
 
66
63
  // Create the implementation
@@ -45,7 +45,6 @@ import { TxPoolIndices } from './tx_pool_indices.js';
45
45
  export interface TxPoolV2Callbacks {
46
46
  onTxsAdded: (txs: Tx[], opts: { source?: string }) => void;
47
47
  onTxsRemoved: (txHashes: string[] | bigint[]) => void;
48
- onTxsMined: (txHashes: string[]) => void;
49
48
  }
50
49
 
51
50
  /**
@@ -214,7 +213,9 @@ export class TxPoolV2Impl {
214
213
  // in-memory reads, and buffered DB writes. Nothing here can throw an unhandled exception.
215
214
  const poolAccess = this.#createPreAddPoolAccess();
216
215
  const preAddContext: PreAddContext | undefined =
217
- opts.feeComparisonOnly !== undefined ? { feeComparisonOnly: opts.feeComparisonOnly } : undefined;
216
+ opts.feeComparisonOnly !== undefined
217
+ ? { feeComparisonOnly: opts.feeComparisonOnly, priceBumpPercentage: this.#config.priceBumpPercentage }
218
+ : undefined;
218
219
 
219
220
  await this.#store.transactionAsync(async () => {
220
221
  for (const tx of txs) {
@@ -336,12 +337,6 @@ export class TxPoolV2Impl {
336
337
  }
337
338
  }
338
339
 
339
- // Randomly drop the transaction for testing purposes (report as accepted so it propagates)
340
- if (this.#config.dropTransactionsProbability > 0 && Math.random() < this.#config.dropTransactionsProbability) {
341
- this.#log.debug(`Dropping tx ${txHashStr} (simulated drop for testing)`);
342
- return { status: 'accepted' };
343
- }
344
-
345
340
  // Add the transaction
346
341
  await this.#addTx(tx, 'pending', opts, precomputedMeta);
347
342
  return { status: 'accepted' };
@@ -499,10 +494,6 @@ export class TxPoolV2Impl {
499
494
  await this.#evictionManager.evictAfterNewBlock(block.header, nullifiers, feePayers);
500
495
  });
501
496
 
502
- if (found.length > 0) {
503
- this.#callbacks.onTxsMined(found.map(m => m.txHash));
504
- }
505
-
506
497
  this.#log.info(`Marked ${found.length} txs as mined in block ${blockId.number}`);
507
498
  }
508
499
 
@@ -1,10 +1,20 @@
1
1
  import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
- import type { BlockProposal, P2PValidator } from '@aztec/stdlib/p2p';
2
+ import type { BlockProposal, P2PValidator, ValidationResult } from '@aztec/stdlib/p2p';
3
3
 
4
4
  import { ProposalValidator } from '../proposal_validator/proposal_validator.js';
5
5
 
6
- export class BlockProposalValidator extends ProposalValidator<BlockProposal> implements P2PValidator<BlockProposal> {
6
+ export class BlockProposalValidator implements P2PValidator<BlockProposal> {
7
+ private proposalValidator: ProposalValidator;
8
+
7
9
  constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean; maxTxsPerBlock?: number }) {
8
- super(epochCache, opts, 'p2p:block_proposal_validator');
10
+ this.proposalValidator = new ProposalValidator(epochCache, opts, 'p2p:block_proposal_validator');
11
+ }
12
+
13
+ async validate(proposal: BlockProposal): Promise<ValidationResult> {
14
+ const headerResult = await this.proposalValidator.validate(proposal);
15
+ if (headerResult.result !== 'accept') {
16
+ return headerResult;
17
+ }
18
+ return this.proposalValidator.validateTxs(proposal);
9
19
  }
10
20
  }
@@ -1,13 +1,26 @@
1
1
  import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
- import type { CheckpointProposal, P2PValidator } from '@aztec/stdlib/p2p';
2
+ import type { CheckpointProposal, P2PValidator, ValidationResult } from '@aztec/stdlib/p2p';
3
3
 
4
4
  import { ProposalValidator } from '../proposal_validator/proposal_validator.js';
5
5
 
6
- export class CheckpointProposalValidator
7
- extends ProposalValidator<CheckpointProposal>
8
- implements P2PValidator<CheckpointProposal>
9
- {
6
+ export class CheckpointProposalValidator implements P2PValidator<CheckpointProposal> {
7
+ private proposalValidator: ProposalValidator;
8
+
10
9
  constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean; maxTxsPerBlock?: number }) {
11
- super(epochCache, opts, 'p2p:checkpoint_proposal_validator');
10
+ this.proposalValidator = new ProposalValidator(epochCache, opts, 'p2p:checkpoint_proposal_validator');
11
+ }
12
+
13
+ async validate(proposal: CheckpointProposal): Promise<ValidationResult> {
14
+ const headerResult = await this.proposalValidator.validate(proposal);
15
+ if (headerResult.result !== 'accept') {
16
+ return headerResult;
17
+ }
18
+
19
+ const blockProposal = proposal.getBlockProposal();
20
+ if (blockProposal) {
21
+ return this.proposalValidator.validateTxs(blockProposal);
22
+ }
23
+
24
+ return { result: 'accept' };
12
25
  }
13
26
  }