@aztec/p2p 0.0.1-commit.358457c → 0.0.1-commit.3895657bc

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 (146) hide show
  1. package/dest/client/factory.d.ts +5 -6
  2. package/dest/client/factory.d.ts.map +1 -1
  3. package/dest/client/factory.js +7 -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 -25
  9. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.js +1 -2
  10. package/dest/config.d.ts +20 -11
  11. package/dest/config.d.ts.map +1 -1
  12. package/dest/config.js +66 -32
  13. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +1 -1
  14. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  15. package/dest/mem_pools/attestation_pool/attestation_pool.js +5 -1
  16. package/dest/mem_pools/instrumentation.d.ts +4 -2
  17. package/dest/mem_pools/instrumentation.d.ts.map +1 -1
  18. package/dest/mem_pools/instrumentation.js +16 -14
  19. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts +1 -1
  20. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts.map +1 -1
  21. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.js +2 -0
  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/index.d.ts +2 -2
  31. package/dest/mem_pools/tx_pool_v2/index.d.ts.map +1 -1
  32. package/dest/mem_pools/tx_pool_v2/index.js +1 -1
  33. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +3 -1
  34. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  35. package/dest/mem_pools/tx_pool_v2/interfaces.js +2 -1
  36. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +32 -10
  37. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  38. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +62 -16
  39. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +1 -1
  40. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
  41. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +26 -44
  42. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +1 -1
  43. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  44. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +3 -0
  45. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +2 -1
  46. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  47. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +41 -23
  48. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts +6 -4
  49. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts.map +1 -1
  50. package/dest/msg_validators/proposal_validator/block_proposal_validator.js +10 -2
  51. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts +6 -4
  52. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts.map +1 -1
  53. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.js +16 -2
  54. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +13 -8
  55. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  56. package/dest/msg_validators/proposal_validator/proposal_validator.js +48 -36
  57. package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts +2 -1
  58. package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts.map +1 -1
  59. package/dest/msg_validators/tx_validator/allowed_public_setup.js +24 -20
  60. package/dest/msg_validators/tx_validator/allowed_setup_helpers.d.ts +17 -0
  61. package/dest/msg_validators/tx_validator/allowed_setup_helpers.d.ts.map +1 -0
  62. package/dest/msg_validators/tx_validator/allowed_setup_helpers.js +24 -0
  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/index.d.ts +2 -1
  67. package/dest/msg_validators/tx_validator/index.d.ts.map +1 -1
  68. package/dest/msg_validators/tx_validator/index.js +1 -0
  69. package/dest/msg_validators/tx_validator/metadata_validator.d.ts +1 -1
  70. package/dest/msg_validators/tx_validator/metadata_validator.d.ts.map +1 -1
  71. package/dest/msg_validators/tx_validator/metadata_validator.js +4 -4
  72. package/dest/msg_validators/tx_validator/phases_validator.d.ts +2 -2
  73. package/dest/msg_validators/tx_validator/phases_validator.d.ts.map +1 -1
  74. package/dest/msg_validators/tx_validator/phases_validator.js +44 -23
  75. package/dest/services/encoding.d.ts +2 -2
  76. package/dest/services/encoding.d.ts.map +1 -1
  77. package/dest/services/encoding.js +7 -7
  78. package/dest/services/libp2p/libp2p_service.d.ts +6 -7
  79. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  80. package/dest/services/libp2p/libp2p_service.js +22 -19
  81. package/dest/services/peer-manager/metrics.d.ts +3 -1
  82. package/dest/services/peer-manager/metrics.d.ts.map +1 -1
  83. package/dest/services/peer-manager/metrics.js +6 -0
  84. package/dest/services/peer-manager/peer_manager.d.ts +1 -1
  85. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  86. package/dest/services/peer-manager/peer_manager.js +2 -1
  87. package/dest/services/reqresp/reqresp.d.ts +1 -1
  88. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  89. package/dest/services/reqresp/reqresp.js +2 -1
  90. package/dest/test-helpers/make-test-p2p-clients.d.ts +5 -6
  91. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  92. package/dest/test-helpers/make-test-p2p-clients.js +1 -2
  93. package/dest/test-helpers/mock-pubsub.d.ts +2 -3
  94. package/dest/test-helpers/mock-pubsub.d.ts.map +1 -1
  95. package/dest/test-helpers/mock-pubsub.js +2 -2
  96. package/dest/test-helpers/reqresp-nodes.d.ts +2 -3
  97. package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
  98. package/dest/test-helpers/reqresp-nodes.js +2 -2
  99. package/dest/testbench/p2p_client_testbench_worker.js +7 -6
  100. package/dest/testbench/worker_client_manager.d.ts +3 -1
  101. package/dest/testbench/worker_client_manager.d.ts.map +1 -1
  102. package/dest/testbench/worker_client_manager.js +6 -2
  103. package/dest/util.d.ts +1 -1
  104. package/package.json +14 -14
  105. package/src/client/factory.ts +10 -14
  106. package/src/client/interface.ts +3 -9
  107. package/src/client/p2p_client.ts +2 -34
  108. package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +2 -3
  109. package/src/config.ts +91 -34
  110. package/src/mem_pools/attestation_pool/attestation_pool.ts +5 -4
  111. package/src/mem_pools/instrumentation.ts +17 -13
  112. package/src/mem_pools/tx_pool_v2/README.md +9 -1
  113. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.ts +3 -0
  114. package/src/mem_pools/tx_pool_v2/eviction/interfaces.ts +11 -1
  115. package/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.ts +15 -6
  116. package/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.ts +2 -1
  117. package/src/mem_pools/tx_pool_v2/index.ts +1 -1
  118. package/src/mem_pools/tx_pool_v2/interfaces.ts +3 -0
  119. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +91 -19
  120. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +29 -43
  121. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +3 -0
  122. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +48 -21
  123. package/src/msg_validators/proposal_validator/block_proposal_validator.ts +14 -4
  124. package/src/msg_validators/proposal_validator/checkpoint_proposal_validator.ts +20 -7
  125. package/src/msg_validators/proposal_validator/proposal_validator.ts +63 -40
  126. package/src/msg_validators/tx_validator/allowed_public_setup.ts +22 -27
  127. package/src/msg_validators/tx_validator/allowed_setup_helpers.ts +31 -0
  128. package/src/msg_validators/tx_validator/fee_payer_balance.ts +6 -2
  129. package/src/msg_validators/tx_validator/index.ts +1 -0
  130. package/src/msg_validators/tx_validator/metadata_validator.ts +12 -4
  131. package/src/msg_validators/tx_validator/phases_validator.ts +51 -26
  132. package/src/services/encoding.ts +5 -6
  133. package/src/services/libp2p/libp2p_service.ts +20 -17
  134. package/src/services/peer-manager/metrics.ts +7 -0
  135. package/src/services/peer-manager/peer_manager.ts +2 -1
  136. package/src/services/reqresp/reqresp.ts +3 -1
  137. package/src/test-helpers/make-test-p2p-clients.ts +1 -3
  138. package/src/test-helpers/mock-pubsub.ts +3 -6
  139. package/src/test-helpers/reqresp-nodes.ts +3 -6
  140. package/src/testbench/p2p_client_testbench_worker.ts +4 -7
  141. package/src/testbench/worker_client_manager.ts +13 -5
  142. package/src/util.ts +1 -1
  143. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts +0 -23
  144. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts.map +0 -1
  145. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.js +0 -212
  146. package/src/msg_validators/proposal_validator/proposal_validator_test_suite.ts +0 -230
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,
@@ -10,7 +11,6 @@ import {
10
11
  secretStringConfigHelper,
11
12
  } from '@aztec/foundation/config';
12
13
  import { Fr } from '@aztec/foundation/curves/bn254';
13
- import { type DataStoreConfig, dataConfigMappings } from '@aztec/kv-store/config';
14
14
  import { FunctionSelector } from '@aztec/stdlib/abi/function-selector';
15
15
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
16
16
  import {
@@ -20,6 +20,7 @@ import {
20
20
  chainConfigMappings,
21
21
  sharedSequencerConfigMappings,
22
22
  } from '@aztec/stdlib/config';
23
+ import { type DataStoreConfig, dataConfigMappings } from '@aztec/stdlib/kv-store';
23
24
 
24
25
  import {
25
26
  type BatchTxRequesterConfig,
@@ -38,7 +39,10 @@ export interface P2PConfig
38
39
  ChainConfig,
39
40
  TxCollectionConfig,
40
41
  TxFileStoreConfig,
41
- Pick<SequencerConfig, 'blockDurationMs' | 'expectedBlockProposalsPerSlot'> {
42
+ Pick<SequencerConfig, 'blockDurationMs' | 'expectedBlockProposalsPerSlot' | 'maxTxsPerBlock'> {
43
+ /** Maximum transactions per block for validation. Overrides maxTxsPerBlock for gossip validation when set. */
44
+ validateMaxTxsPerBlock?: number;
45
+
42
46
  /** A flag dictating whether the P2P subsystem should be enabled. */
43
47
  p2pEnabled: boolean;
44
48
 
@@ -150,8 +154,8 @@ export interface P2PConfig
150
154
  /** The maximum possible size of the P2P DB in KB. Overwrites the general dataStoreMapSizeKb. */
151
155
  p2pStoreMapSizeKb?: number;
152
156
 
153
- /** Which calls are allowed in the public setup phase of a tx. */
154
- txPublicSetupAllowList: AllowedElement[];
157
+ /** Additional entries to extend the default setup allow list. */
158
+ txPublicSetupAllowListExtend: AllowedElement[];
155
159
 
156
160
  /** The maximum number of pending txs before evicting lower priority txs. */
157
161
  maxPendingTxCount: number;
@@ -190,11 +194,20 @@ export interface P2PConfig
190
194
 
191
195
  /** Minimum age (ms) a transaction must have been in the pool before it's eligible for block building. */
192
196
  minTxPoolAgeMs: number;
197
+
198
+ /** Minimum percentage fee increase required to replace an existing tx via RPC (0 = no bump). */
199
+ priceBumpPercentage: bigint;
193
200
  }
194
201
 
195
202
  export const DEFAULT_P2P_PORT = 40400;
196
203
 
197
204
  export const p2pConfigMappings: ConfigMappingsType<P2PConfig> = {
205
+ validateMaxTxsPerBlock: {
206
+ env: 'VALIDATOR_MAX_TX_PER_BLOCK',
207
+ description:
208
+ 'Maximum transactions per block for validation. Overrides maxTxsPerBlock for gossip validation when set.',
209
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
210
+ },
198
211
  p2pEnabled: {
199
212
  env: 'P2P_ENABLED',
200
213
  description: 'A flag dictating whether the P2P subsystem should be enabled.',
@@ -393,12 +406,13 @@ export const p2pConfigMappings: ConfigMappingsType<P2PConfig> = {
393
406
  parseEnv: (val: string | undefined) => (val ? +val : undefined),
394
407
  description: 'The maximum possible size of the P2P DB in KB. Overwrites the general dataStoreMapSizeKb.',
395
408
  },
396
- txPublicSetupAllowList: {
409
+ txPublicSetupAllowListExtend: {
397
410
  env: 'TX_PUBLIC_SETUP_ALLOWLIST',
398
411
  parseEnv: (val: string) => parseAllowList(val),
399
- description: 'The list of functions calls allowed to run in setup',
412
+ description:
413
+ '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 +.',
400
414
  printDefault: () =>
401
- 'AuthRegistry, FeeJuice.increase_public_balance, Token.increase_public_balance, FPC.prepare_fee',
415
+ 'Default: AuthRegistry._set_authorized, AuthRegistry.set_authorized, FeeJuice._increase_public_balance',
402
416
  },
403
417
  maxPendingTxCount: {
404
418
  env: 'P2P_MAX_PENDING_TX_COUNT',
@@ -464,6 +478,12 @@ export const p2pConfigMappings: ConfigMappingsType<P2PConfig> = {
464
478
  description: 'Minimum age (ms) a transaction must have been in the pool before it is eligible for block building.',
465
479
  ...numberConfigHelper(2_000),
466
480
  },
481
+ priceBumpPercentage: {
482
+ env: 'P2P_RPC_PRICE_BUMP_PERCENTAGE',
483
+ description:
484
+ 'Minimum percentage fee increase required to replace an existing tx via RPC. Even at 0%, replacement still requires paying at least 1 unit more.',
485
+ ...bigintConfigHelper(10n),
486
+ },
467
487
  ...sharedSequencerConfigMappings,
468
488
  ...p2pReqRespConfigMappings,
469
489
  ...batchTxRequesterConfigMappings,
@@ -521,13 +541,44 @@ export const bootnodeConfigMappings = pickConfigMappings(
521
541
  bootnodeConfigKeys,
522
542
  );
523
543
 
544
+ /**
545
+ * Parses a `+`-separated flags string into validation properties for an allow list entry.
546
+ * Supported flags: `os` (onlySelf), `rn` (rejectNullMsgSender), `cl=N` (calldataLength).
547
+ */
548
+ function parseFlags(
549
+ flags: string,
550
+ entry: string,
551
+ ): { onlySelf?: boolean; rejectNullMsgSender?: boolean; calldataLength?: number } {
552
+ const result: { onlySelf?: boolean; rejectNullMsgSender?: boolean; calldataLength?: number } = {};
553
+ for (const flag of flags.split('+')) {
554
+ if (flag === 'os') {
555
+ result.onlySelf = true;
556
+ } else if (flag === 'rn') {
557
+ result.rejectNullMsgSender = true;
558
+ } else if (flag.startsWith('cl=')) {
559
+ const n = parseInt(flag.slice(3), 10);
560
+ if (isNaN(n) || n < 0) {
561
+ throw new Error(
562
+ `Invalid allow list entry "${entry}": invalid calldataLength in flag "${flag}". Expected a non-negative integer.`,
563
+ );
564
+ }
565
+ result.calldataLength = n;
566
+ } else {
567
+ throw new Error(`Invalid allow list entry "${entry}": unknown flag "${flag}". Supported flags: os, rn, cl=N.`);
568
+ }
569
+ }
570
+ return result;
571
+ }
572
+
524
573
  /**
525
574
  * Parses a string to a list of allowed elements.
526
- * Each encoded is expected to be of one of the following formats
527
- * `I:${address}`
528
- * `I:${address}:${selector}`
529
- * `C:${classId}`
530
- * `C:${classId}:${selector}`
575
+ * Each entry is expected to be of one of the following formats:
576
+ * `I:${address}:${selector}` — instance (contract address) with function selector
577
+ * `C:${classId}:${selector}` — class with function selector
578
+ *
579
+ * An optional flags segment can be appended after the selector:
580
+ * `I:${address}:${selector}:${flags}` or `C:${classId}:${selector}:${flags}`
581
+ * where flags is a `+`-separated list of: `os` (onlySelf), `rn` (rejectNullMsgSender), `cl=N` (calldataLength).
531
582
  *
532
583
  * @param value The string to parse
533
584
  * @returns A list of allowed elements
@@ -540,31 +591,37 @@ export function parseAllowList(value: string): AllowedElement[] {
540
591
  }
541
592
 
542
593
  for (const val of value.split(',')) {
543
- const [typeString, identifierString, selectorString] = val.split(':');
544
- const selector = selectorString !== undefined ? FunctionSelector.fromString(selectorString) : undefined;
594
+ const trimmed = val.trim();
595
+ if (!trimmed) {
596
+ continue;
597
+ }
598
+ const [typeString, identifierString, selectorString, flagsString] = trimmed.split(':');
599
+
600
+ if (!selectorString) {
601
+ throw new Error(
602
+ `Invalid allow list entry "${trimmed}": selector is required. Expected format: I:address:selector or C:classId:selector`,
603
+ );
604
+ }
605
+
606
+ const selector = FunctionSelector.fromString(selectorString);
607
+ const flags = flagsString ? parseFlags(flagsString, trimmed) : {};
545
608
 
546
609
  if (typeString === 'I') {
547
- if (selector) {
548
- entries.push({
549
- address: AztecAddress.fromString(identifierString),
550
- selector,
551
- });
552
- } else {
553
- entries.push({
554
- address: AztecAddress.fromString(identifierString),
555
- });
556
- }
610
+ entries.push({
611
+ address: AztecAddress.fromString(identifierString),
612
+ selector,
613
+ ...flags,
614
+ });
557
615
  } else if (typeString === 'C') {
558
- if (selector) {
559
- entries.push({
560
- classId: Fr.fromHexString(identifierString),
561
- selector,
562
- });
563
- } else {
564
- entries.push({
565
- classId: Fr.fromHexString(identifierString),
566
- });
567
- }
616
+ entries.push({
617
+ classId: Fr.fromHexString(identifierString),
618
+ selector,
619
+ ...flags,
620
+ });
621
+ } else {
622
+ throw new Error(
623
+ `Invalid allow list entry "${trimmed}": unknown type "${typeString}". Expected "I" (instance) or "C" (class).`,
624
+ );
568
625
  }
569
626
  }
570
627
 
@@ -359,11 +359,10 @@ export class AttestationPool {
359
359
  }
360
360
 
361
361
  const address = sender.toString();
362
+ const ownKey = this.getAttestationKey(slotNumber, proposalId, address);
362
363
 
363
- await this.checkpointAttestations.set(
364
- this.getAttestationKey(slotNumber, proposalId, address),
365
- attestation.toBuffer(),
366
- );
364
+ await this.checkpointAttestations.set(ownKey, attestation.toBuffer());
365
+ this.metrics.trackMempoolItemAdded(ownKey);
367
366
 
368
367
  this.log.debug(`Added own checkpoint attestation for slot ${slotNumber} from ${address}`, {
369
368
  signature: attestation.signature.toString(),
@@ -429,6 +428,7 @@ export class AttestationPool {
429
428
  const attestationEndKey = new Fr(oldestSlot).toString();
430
429
  for await (const key of this.checkpointAttestations.keysAsync({ end: attestationEndKey })) {
431
430
  await this.checkpointAttestations.delete(key);
431
+ this.metrics.trackMempoolItemRemoved(key);
432
432
  numberOfAttestations++;
433
433
  }
434
434
 
@@ -526,6 +526,7 @@ export class AttestationPool {
526
526
 
527
527
  // Add the attestation
528
528
  await this.checkpointAttestations.set(key, attestation.toBuffer());
529
+ this.metrics.trackMempoolItemAdded(key);
529
530
 
530
531
  // Track this attestation in the per-signer-per-slot index for duplicate detection
531
532
  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 txAddedTimestamp: Map<bigint, number> = new Map<bigint, number>();
76
+ private mempoolItemAddedTimestamp: Map<bigint | string, number> = new Map<bigint | string, number>();
77
77
 
78
78
  constructor(
79
79
  telemetry: TelemetryClient,
@@ -114,22 +114,26 @@ export class PoolInstrumentation<PoolObject extends Gossipable> {
114
114
  }
115
115
 
116
116
  public transactionsAdded(transactions: Tx[]) {
117
- const timestamp = Date.now();
118
- for (const transaction of transactions) {
119
- this.txAddedTimestamp.set(transaction.txHash.toBigInt(), timestamp);
120
- }
117
+ transactions.forEach(tx => this.trackMempoolItemAdded(tx.txHash.toBigInt()));
121
118
  }
122
119
 
123
120
  public transactionsRemoved(hashes: Iterable<bigint> | Iterable<string>) {
124
- const timestamp = Date.now();
125
121
  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
- }
122
+ this.trackMempoolItemRemoved(BigInt(hash));
123
+ }
124
+ }
125
+
126
+ public trackMempoolItemAdded(key: bigint | string): void {
127
+ this.mempoolItemAddedTimestamp.set(key, Date.now());
128
+ }
129
+
130
+ public trackMempoolItemRemoved(key: bigint | string): void {
131
+ 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);
133
137
  }
134
138
  }
135
139
  }
@@ -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
@@ -35,6 +35,7 @@ export class FeePayerBalancePreAddRule implements PreAddRule {
35
35
  // Create combined list with incoming tx
36
36
  const allTxs: Array<{
37
37
  txHash: string;
38
+ txHashBigInt: bigint;
38
39
  priorityFee: bigint;
39
40
  feeLimit: bigint;
40
41
  claimAmount: bigint;
@@ -42,6 +43,7 @@ export class FeePayerBalancePreAddRule implements PreAddRule {
42
43
  }> = [
43
44
  ...existingTxs.map(t => ({
44
45
  txHash: t.txHash,
46
+ txHashBigInt: t.txHashBigInt,
45
47
  priorityFee: t.priorityFee,
46
48
  feeLimit: t.feeLimit,
47
49
  claimAmount: t.claimAmount,
@@ -49,6 +51,7 @@ export class FeePayerBalancePreAddRule implements PreAddRule {
49
51
  })),
50
52
  {
51
53
  txHash: incomingMeta.txHash,
54
+ txHashBigInt: incomingMeta.txHashBigInt,
52
55
  priorityFee: incomingMeta.priorityFee,
53
56
  feeLimit: incomingMeta.feeLimit,
54
57
  claimAmount: incomingMeta.claimAmount,
@@ -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) {
@@ -7,6 +7,6 @@ export {
7
7
  type PoolReadAccess,
8
8
  DEFAULT_TX_POOL_V2_CONFIG,
9
9
  } from './interfaces.js';
10
- export { type TxMetaData, type TxState, buildTxMetaData, comparePriority } from './tx_metadata.js';
10
+ export { type TxMetaData, type TxState, buildTxMetaData, comparePriority, stubTxMetaData } from './tx_metadata.js';
11
11
  export { TxArchive } from './archive/index.js';
12
12
  export { DeletedPool } from './deleted_pool.js';
@@ -46,6 +46,8 @@ export type TxPoolV2Config = {
46
46
  evictedTxCacheSize: number;
47
47
  /** The probability (0-1) that a transaction is discarded. 0 disables dropping. For testing purposes only. */
48
48
  dropTransactionsProbability: number;
49
+ /** Minimum percentage fee increase required to replace an existing tx via RPC (0 = no bump). */
50
+ priceBumpPercentage: bigint;
49
51
  };
50
52
 
51
53
  /**
@@ -57,6 +59,7 @@ export const DEFAULT_TX_POOL_V2_CONFIG: TxPoolV2Config = {
57
59
  minTxPoolAgeMs: 2_000,
58
60
  evictedTxCacheSize: 10_000,
59
61
  dropTransactionsProbability: 0,
62
+ priceBumpPercentage: 10n,
60
63
  };
61
64
 
62
65
  /**
@@ -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,14 +152,19 @@ 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
+ export type PriorityComparable = Pick<TxMetaData, 'txHash' | 'txHashBigInt' | 'priorityFee'>;
151
162
 
152
163
  /**
153
164
  * Compares two priority fees in ascending order.
154
165
  * Returns negative if a < b, positive if a > b, 0 if equal.
155
166
  */
156
- export function compareFee(a: bigint, b: bigint): number {
167
+ export function compareFee(a: bigint, b: bigint): -1 | 0 | 1 {
157
168
  return a < b ? -1 : a > b ? 1 : 0;
158
169
  }
159
170
 
@@ -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): -1 | 0 | 1 {
177
+ return Fr.cmpAsBigInt(a, b);
169
178
  }
170
179
 
171
180
  /**
@@ -173,29 +182,46 @@ export function compareTxHash(a: string, b: string): number {
173
182
  * Returns negative if a < b, positive if a > b, 0 if equal.
174
183
  * Use with sort() for ascending order, or negate/reverse for descending.
175
184
  */
176
- export function comparePriority(a: PriorityComparable, b: PriorityComparable): number {
185
+ export function comparePriority(a: PriorityComparable, b: PriorityComparable): -1 | 0 | 1 {
177
186
  const feeComparison = compareFee(a.priorityFee, b.priorityFee);
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
+ }