@aztec/p2p 0.0.1-commit.ff7989d6c → 0.0.1-commit.fff30aa

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 (200) hide show
  1. package/README.md +129 -3
  2. package/dest/client/factory.d.ts +4 -5
  3. package/dest/client/factory.d.ts.map +1 -1
  4. package/dest/client/factory.js +29 -28
  5. package/dest/client/interface.d.ts +6 -13
  6. package/dest/client/interface.d.ts.map +1 -1
  7. package/dest/client/p2p_client.d.ts +5 -13
  8. package/dest/client/p2p_client.d.ts.map +1 -1
  9. package/dest/client/p2p_client.js +19 -88
  10. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.js +1 -2
  11. package/dest/config.d.ts +32 -11
  12. package/dest/config.d.ts.map +1 -1
  13. package/dest/config.js +85 -31
  14. package/dest/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.d.ts +1 -1
  15. package/dest/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.d.ts.map +1 -1
  16. package/dest/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.js +2 -1
  17. package/dest/mem_pools/tx_pool/priority.d.ts +2 -2
  18. package/dest/mem_pools/tx_pool/priority.d.ts.map +1 -1
  19. package/dest/mem_pools/tx_pool/priority.js +4 -4
  20. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts +1 -1
  21. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts.map +1 -1
  22. package/dest/mem_pools/tx_pool/tx_pool_test_suite.js +3 -1
  23. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.d.ts +1 -1
  24. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.d.ts.map +1 -1
  25. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +3 -2
  26. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts +1 -1
  27. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts.map +1 -1
  28. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.js +2 -0
  29. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts +7 -1
  30. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts.map +1 -1
  31. package/dest/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.js +2 -2
  32. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts +1 -1
  33. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts.map +1 -1
  34. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.js +8 -6
  35. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts +2 -2
  36. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts.map +1 -1
  37. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.js +2 -2
  38. package/dest/mem_pools/tx_pool_v2/index.d.ts +2 -2
  39. package/dest/mem_pools/tx_pool_v2/index.d.ts.map +1 -1
  40. package/dest/mem_pools/tx_pool_v2/index.js +1 -1
  41. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +9 -7
  42. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  43. package/dest/mem_pools/tx_pool_v2/interfaces.js +1 -1
  44. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +46 -8
  45. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  46. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +81 -17
  47. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +1 -1
  48. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
  49. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +9 -10
  50. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +5 -3
  51. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  52. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +3 -0
  53. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +2 -2
  54. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  55. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +50 -40
  56. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts +6 -4
  57. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts.map +1 -1
  58. package/dest/msg_validators/proposal_validator/block_proposal_validator.js +10 -2
  59. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts +6 -4
  60. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts.map +1 -1
  61. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.js +16 -2
  62. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +13 -8
  63. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  64. package/dest/msg_validators/proposal_validator/proposal_validator.js +48 -36
  65. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts +2 -2
  66. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts.map +1 -1
  67. package/dest/msg_validators/tx_validator/aggregate_tx_validator.js +3 -3
  68. package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts +2 -1
  69. package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts.map +1 -1
  70. package/dest/msg_validators/tx_validator/allowed_public_setup.js +24 -20
  71. package/dest/msg_validators/tx_validator/allowed_setup_helpers.d.ts +17 -0
  72. package/dest/msg_validators/tx_validator/allowed_setup_helpers.d.ts.map +1 -0
  73. package/dest/msg_validators/tx_validator/allowed_setup_helpers.js +24 -0
  74. package/dest/msg_validators/tx_validator/contract_instance_validator.d.ts +9 -0
  75. package/dest/msg_validators/tx_validator/contract_instance_validator.d.ts.map +1 -0
  76. package/dest/msg_validators/tx_validator/contract_instance_validator.js +48 -0
  77. package/dest/msg_validators/tx_validator/data_validator.d.ts +1 -1
  78. package/dest/msg_validators/tx_validator/data_validator.d.ts.map +1 -1
  79. package/dest/msg_validators/tx_validator/data_validator.js +35 -2
  80. package/dest/msg_validators/tx_validator/factory.d.ts +133 -6
  81. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  82. package/dest/msg_validators/tx_validator/factory.js +247 -60
  83. package/dest/msg_validators/tx_validator/fee_payer_balance.d.ts +1 -1
  84. package/dest/msg_validators/tx_validator/fee_payer_balance.d.ts.map +1 -1
  85. package/dest/msg_validators/tx_validator/fee_payer_balance.js +6 -2
  86. package/dest/msg_validators/tx_validator/gas_validator.d.ts +67 -3
  87. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  88. package/dest/msg_validators/tx_validator/gas_validator.js +104 -37
  89. package/dest/msg_validators/tx_validator/index.d.ts +3 -1
  90. package/dest/msg_validators/tx_validator/index.d.ts.map +1 -1
  91. package/dest/msg_validators/tx_validator/index.js +2 -0
  92. package/dest/msg_validators/tx_validator/nullifier_cache.d.ts +14 -0
  93. package/dest/msg_validators/tx_validator/nullifier_cache.d.ts.map +1 -0
  94. package/dest/msg_validators/tx_validator/nullifier_cache.js +24 -0
  95. package/dest/msg_validators/tx_validator/phases_validator.d.ts +22 -2
  96. package/dest/msg_validators/tx_validator/phases_validator.d.ts.map +1 -1
  97. package/dest/msg_validators/tx_validator/phases_validator.js +72 -24
  98. package/dest/services/dummy_service.d.ts +2 -3
  99. package/dest/services/dummy_service.d.ts.map +1 -1
  100. package/dest/services/dummy_service.js +1 -4
  101. package/dest/services/encoding.d.ts +6 -2
  102. package/dest/services/encoding.d.ts.map +1 -1
  103. package/dest/services/encoding.js +14 -8
  104. package/dest/services/libp2p/libp2p_service.d.ts +15 -13
  105. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  106. package/dest/services/libp2p/libp2p_service.js +92 -92
  107. package/dest/services/reqresp/batch-tx-requester/tx_validator.js +2 -2
  108. package/dest/services/reqresp/rate-limiter/rate_limiter.d.ts +5 -4
  109. package/dest/services/reqresp/rate-limiter/rate_limiter.d.ts.map +1 -1
  110. package/dest/services/reqresp/rate-limiter/rate_limiter.js +10 -8
  111. package/dest/services/reqresp/reqresp.d.ts +1 -1
  112. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  113. package/dest/services/reqresp/reqresp.js +17 -9
  114. package/dest/services/service.d.ts +2 -2
  115. package/dest/services/service.d.ts.map +1 -1
  116. package/dest/services/tx_collection/file_store_tx_source.d.ts +5 -4
  117. package/dest/services/tx_collection/file_store_tx_source.d.ts.map +1 -1
  118. package/dest/services/tx_collection/file_store_tx_source.js +39 -29
  119. package/dest/services/tx_collection/tx_source.d.ts +6 -5
  120. package/dest/services/tx_collection/tx_source.d.ts.map +1 -1
  121. package/dest/services/tx_collection/tx_source.js +9 -7
  122. package/dest/services/tx_provider.d.ts +3 -3
  123. package/dest/services/tx_provider.d.ts.map +1 -1
  124. package/dest/services/tx_provider.js +4 -4
  125. package/dest/test-helpers/make-test-p2p-clients.d.ts +5 -6
  126. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  127. package/dest/test-helpers/make-test-p2p-clients.js +1 -2
  128. package/dest/test-helpers/mock-pubsub.d.ts +2 -3
  129. package/dest/test-helpers/mock-pubsub.d.ts.map +1 -1
  130. package/dest/test-helpers/mock-pubsub.js +2 -2
  131. package/dest/test-helpers/reqresp-nodes.d.ts +2 -3
  132. package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
  133. package/dest/test-helpers/reqresp-nodes.js +2 -2
  134. package/dest/test-helpers/testbench-utils.d.ts +2 -2
  135. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  136. package/dest/test-helpers/testbench-utils.js +2 -1
  137. package/dest/testbench/p2p_client_testbench_worker.js +7 -6
  138. package/dest/testbench/worker_client_manager.d.ts +3 -1
  139. package/dest/testbench/worker_client_manager.d.ts.map +1 -1
  140. package/dest/testbench/worker_client_manager.js +4 -1
  141. package/package.json +14 -14
  142. package/src/client/factory.ts +49 -46
  143. package/src/client/interface.ts +5 -19
  144. package/src/client/p2p_client.ts +20 -118
  145. package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +1 -2
  146. package/src/config.ts +124 -34
  147. package/src/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.ts +2 -1
  148. package/src/mem_pools/tx_pool/priority.ts +4 -4
  149. package/src/mem_pools/tx_pool/tx_pool_test_suite.ts +3 -1
  150. package/src/mem_pools/tx_pool_v2/README.md +9 -1
  151. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +3 -2
  152. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.ts +3 -0
  153. package/src/mem_pools/tx_pool_v2/eviction/interfaces.ts +11 -1
  154. package/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.ts +2 -2
  155. package/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.ts +15 -6
  156. package/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.ts +2 -1
  157. package/src/mem_pools/tx_pool_v2/index.ts +1 -1
  158. package/src/mem_pools/tx_pool_v2/interfaces.ts +9 -7
  159. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +113 -18
  160. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +11 -11
  161. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +14 -2
  162. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +57 -39
  163. package/src/msg_validators/attestation_validator/README.md +49 -0
  164. package/src/msg_validators/proposal_validator/README.md +123 -0
  165. package/src/msg_validators/proposal_validator/block_proposal_validator.ts +14 -4
  166. package/src/msg_validators/proposal_validator/checkpoint_proposal_validator.ts +20 -7
  167. package/src/msg_validators/proposal_validator/proposal_validator.ts +63 -40
  168. package/src/msg_validators/tx_validator/README.md +119 -0
  169. package/src/msg_validators/tx_validator/aggregate_tx_validator.ts +3 -3
  170. package/src/msg_validators/tx_validator/allowed_public_setup.ts +22 -27
  171. package/src/msg_validators/tx_validator/allowed_setup_helpers.ts +31 -0
  172. package/src/msg_validators/tx_validator/contract_instance_validator.ts +56 -0
  173. package/src/msg_validators/tx_validator/data_validator.ts +42 -1
  174. package/src/msg_validators/tx_validator/factory.ts +394 -78
  175. package/src/msg_validators/tx_validator/fee_payer_balance.ts +6 -2
  176. package/src/msg_validators/tx_validator/gas_validator.ts +123 -27
  177. package/src/msg_validators/tx_validator/index.ts +2 -0
  178. package/src/msg_validators/tx_validator/nullifier_cache.ts +30 -0
  179. package/src/msg_validators/tx_validator/phases_validator.ts +82 -27
  180. package/src/services/dummy_service.ts +1 -5
  181. package/src/services/encoding.ts +14 -7
  182. package/src/services/libp2p/libp2p_service.ts +106 -101
  183. package/src/services/reqresp/README.md +229 -0
  184. package/src/services/reqresp/batch-tx-requester/tx_validator.ts +2 -2
  185. package/src/services/reqresp/rate-limiter/rate_limiter.ts +13 -9
  186. package/src/services/reqresp/reqresp.ts +19 -11
  187. package/src/services/service.ts +1 -1
  188. package/src/services/tx_collection/file_store_tx_source.ts +43 -31
  189. package/src/services/tx_collection/tx_source.ts +8 -7
  190. package/src/services/tx_provider.ts +2 -2
  191. package/src/test-helpers/make-test-p2p-clients.ts +0 -2
  192. package/src/test-helpers/mock-pubsub.ts +3 -6
  193. package/src/test-helpers/reqresp-nodes.ts +2 -5
  194. package/src/test-helpers/testbench-utils.ts +2 -1
  195. package/src/testbench/p2p_client_testbench_worker.ts +3 -6
  196. package/src/testbench/worker_client_manager.ts +11 -4
  197. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts +0 -23
  198. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts.map +0 -1
  199. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.js +0 -212
  200. package/src/msg_validators/proposal_validator/proposal_validator_test_suite.ts +0 -230
@@ -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';
@@ -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
  /**
@@ -69,6 +69,8 @@ export type TxPoolV2Dependencies = {
69
69
  worldStateSynchronizer: WorldStateSynchronizer;
70
70
  /** Factory that creates a validator for re-validating pool transactions using metadata */
71
71
  createTxValidator: () => Promise<TxValidator<TxMetaData>>;
72
+ /** Checks whether a tx's setup-phase calls are on the allow list. Precomputed at receipt time. */
73
+ checkAllowedSetupCalls: (tx: Tx) => Promise<boolean>;
72
74
  };
73
75
 
74
76
  /**
@@ -110,12 +112,12 @@ export interface TxPoolV2 extends TypedEventEmitter<TxPoolV2Events> {
110
112
  addPendingTxs(txs: Tx[], opts?: { source?: string; feeComparisonOnly?: boolean }): Promise<AddTxsResult>;
111
113
 
112
114
  /**
113
- * Checks if a transaction can be added without modifying the pool.
114
- * Performs the same validation as addPendingTxs but doesn't persist changes.
115
+ * Checks if the pool would accept a transaction without modifying state.
116
+ * Used as a pre-check before expensive proof verification.
115
117
  * @param tx - Transaction to check
116
- * @returns Result: 'accepted', 'ignored' (if already in pool or undesirable), or 'rejected' (if validation fails)
118
+ * @returns 'accepted' if the pool would accept, 'ignored' if already in pool or undesirable
117
119
  */
118
- canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored' | 'rejected'>;
120
+ canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored'>;
119
121
 
120
122
  /**
121
123
  * Adds transactions as immediately protected for a given slot.
@@ -2,7 +2,8 @@ import { BlockNumber } from '@aztec/foundation/branded-types';
2
2
  import { Fr } from '@aztec/foundation/curves/bn254';
3
3
  import { ProtocolContractAddress } from '@aztec/protocol-contracts';
4
4
  import { BlockHash, type L2BlockId } from '@aztec/stdlib/block';
5
- import type { Tx } from '@aztec/stdlib/tx';
5
+ import { Gas } from '@aztec/stdlib/gas';
6
+ import { type Tx, TxHash } from '@aztec/stdlib/tx';
6
7
 
7
8
  import { getFeePayerBalanceDelta } from '../../msg_validators/tx_validator/fee_payer_balance.js';
8
9
  import { getTxPriorityFee } from '../tx_pool/priority.js';
@@ -12,6 +13,8 @@ import { type PreAddResult, TxPoolRejectionCode } from './eviction/interfaces.js
12
13
  export type TxMetaValidationData = {
13
14
  getNonEmptyNullifiers(): Fr[];
14
15
  expirationTimestamp: bigint;
16
+ /** Whether the tx has public calls. Used to select the correct L2 gas minimum. */
17
+ forPublic?: unknown;
15
18
  constants: {
16
19
  anchorBlockHeader: {
17
20
  hash(): Promise<BlockHash>;
@@ -19,6 +22,9 @@ export type TxMetaValidationData = {
19
22
  blockNumber: BlockNumber;
20
23
  };
21
24
  };
25
+ txContext: {
26
+ gasSettings: { gasLimits: Gas };
27
+ };
22
28
  };
23
29
  };
24
30
 
@@ -34,6 +40,9 @@ export type TxMetaData = {
34
40
  /** The transaction hash as hex string */
35
41
  readonly txHash: string;
36
42
 
43
+ /** The transaction hash as bigint (for efficient Fr conversion in comparisons) */
44
+ readonly txHashBigInt: bigint;
45
+
37
46
  /** Block ID (number and hash) in which the transaction was mined (undefined if not mined) */
38
47
  minedL2BlockId?: L2BlockId;
39
48
 
@@ -58,6 +67,9 @@ export type TxMetaData = {
58
67
  /** Timestamp by which the transaction must be included (for expiration checks) */
59
68
  readonly expirationTimestamp: bigint;
60
69
 
70
+ /** Whether the tx's setup-phase calls pass the allow list check. Computed at receipt time. */
71
+ readonly allowedSetupCalls: boolean;
72
+
61
73
  /** Validator-compatible data, providing the same access patterns as Tx.data */
62
74
  readonly data: TxMetaValidationData;
63
75
 
@@ -75,9 +87,15 @@ export type TxState = 'pending' | 'protected' | 'mined' | 'deleted';
75
87
  * Builds TxMetaData from a full Tx object.
76
88
  * Extracts all relevant fields for efficient in-memory storage and querying.
77
89
  * Fr values are captured in closures for zero-cost re-validation.
90
+ *
91
+ * @param allowedSetupCalls - Whether the tx's setup-phase calls pass the allow list.
92
+ * For gossip/RPC txs this is always `true` (already validated by PhasesTxValidator).
93
+ * For req/resp txs this should be computed by the caller using the phases validator.
78
94
  */
79
- export async function buildTxMetaData(tx: Tx): Promise<TxMetaData> {
80
- const txHash = tx.getTxHash().toString();
95
+ export async function buildTxMetaData(tx: Tx, allowedSetupCalls: boolean = true): Promise<TxMetaData> {
96
+ const txHashObj = tx.getTxHash();
97
+ const txHash = txHashObj.toString();
98
+ const txHashBigInt = txHashObj.toBigInt();
81
99
  const nullifierFrs = tx.data.getNonEmptyNullifiers();
82
100
  const nullifiers = nullifierFrs.map(n => n.toString());
83
101
  const anchorBlockHeaderHashFr = await tx.data.constants.anchorBlockHeader.hash();
@@ -93,6 +111,7 @@ export async function buildTxMetaData(tx: Tx): Promise<TxMetaData> {
93
111
 
94
112
  return {
95
113
  txHash,
114
+ txHashBigInt,
96
115
  anchorBlockHeaderHash,
97
116
  priorityFee,
98
117
  feePayer,
@@ -100,16 +119,21 @@ export async function buildTxMetaData(tx: Tx): Promise<TxMetaData> {
100
119
  feeLimit,
101
120
  nullifiers,
102
121
  expirationTimestamp,
122
+ allowedSetupCalls,
103
123
  receivedAt: 0,
104
124
  estimatedSizeBytes,
105
125
  data: {
106
126
  getNonEmptyNullifiers: () => nullifierFrs,
107
127
  expirationTimestamp,
128
+ forPublic: !!tx.data.forPublic,
108
129
  constants: {
109
130
  anchorBlockHeader: {
110
131
  hash: () => Promise.resolve(anchorBlockHeaderHashFr),
111
132
  globalVariables: { blockNumber: anchorBlockNumber },
112
133
  },
134
+ txContext: {
135
+ gasSettings: { gasLimits: tx.data.constants.txContext.gasSettings.gasLimits },
136
+ },
113
137
  },
114
138
  },
115
139
  };
@@ -124,11 +148,11 @@ const HEX_STRING_BYTES = 98;
124
148
  const BIGINT_BYTES = 32;
125
149
  const FR_BYTES = 80;
126
150
  // Fixed cost: object shell + txHash + anchorBlockHeaderHash + feePayer (3 hex strings)
127
- // + priorityFee + claimAmount + feeLimit + includeByTimestamp (4 bigints)
151
+ // + txHashBigInt + priorityFee + claimAmount + feeLimit + includeByTimestamp (5 bigints)
128
152
  // + receivedAt (number, 8 bytes) + estimatedSizeBytes (number, 8 bytes)
129
153
  // + data closure object (~OBJECT_OVERHEAD + anchorBlockHeaderHashFr Fr + anchorBlockNumber number)
130
154
  const FIXED_METADATA_BYTES =
131
- OBJECT_OVERHEAD + 3 * HEX_STRING_BYTES + 4 * BIGINT_BYTES + 8 + 8 + OBJECT_OVERHEAD + FR_BYTES + 8;
155
+ OBJECT_OVERHEAD + 3 * HEX_STRING_BYTES + 5 * BIGINT_BYTES + 8 + 8 + OBJECT_OVERHEAD + FR_BYTES + 8;
132
156
 
133
157
  /** Estimates the in-memory size of a TxMetaData object based on the number of nullifiers. */
134
158
  function estimateTxMetaDataSize(nullifierCount: number): number {
@@ -136,8 +160,13 @@ function estimateTxMetaDataSize(nullifierCount: number): number {
136
160
  return FIXED_METADATA_BYTES + nullifierCount * (HEX_STRING_BYTES + FR_BYTES);
137
161
  }
138
162
 
163
+ /** Converts a txHash bigint back to the canonical 0x-prefixed 64-char hex string. */
164
+ export function txHashFromBigInt(value: bigint): string {
165
+ return TxHash.fromBigInt(value).toString();
166
+ }
167
+
139
168
  /** Minimal fields required for priority comparison. */
140
- type PriorityComparable = Pick<TxMetaData, 'txHash' | 'priorityFee'>;
169
+ type PriorityComparable = Pick<TxMetaData, 'txHashBigInt' | 'priorityFee'>;
141
170
 
142
171
  /**
143
172
  * Compares two priority fees in ascending order.
@@ -152,10 +181,8 @@ export function compareFee(a: bigint, b: bigint): number {
152
181
  * Uses field element comparison for deterministic ordering.
153
182
  * Returns negative if a < b, positive if a > b, 0 if equal.
154
183
  */
155
- export function compareTxHash(a: string, b: string): number {
156
- const fieldA = Fr.fromHexString(a);
157
- const fieldB = Fr.fromHexString(b);
158
- return fieldA.cmp(fieldB);
184
+ export function compareTxHash(a: bigint, b: bigint): number {
185
+ return Fr.cmpAsBigInt(a, b);
159
186
  }
160
187
 
161
188
  /**
@@ -168,24 +195,41 @@ export function comparePriority(a: PriorityComparable, b: PriorityComparable): n
168
195
  if (feeComparison !== 0) {
169
196
  return feeComparison;
170
197
  }
171
- return compareTxHash(a.txHash, b.txHash);
198
+ return compareTxHash(a.txHashBigInt, b.txHashBigInt);
199
+ }
200
+
201
+ /**
202
+ * Returns the minimum fee required to replace an existing tx with the given price bump percentage.
203
+ * Uses integer arithmetic: `existingFee + existingFee * priceBumpPercentage / 100`.
204
+ */
205
+ export function getMinimumPriceBumpFee(existingFee: bigint, priceBumpPercentage: bigint): bigint {
206
+ const bump = (existingFee * priceBumpPercentage) / 100n;
207
+ // Ensure the minimum bump is at least 1, so that replacement always requires
208
+ // paying strictly more — even with 0% bump or zero existing fee.
209
+ const effectiveBump = bump > 0n ? bump : 1n;
210
+ return existingFee + effectiveBump;
172
211
  }
173
212
 
174
213
  /**
175
214
  * Checks for nullifier conflicts between an incoming transaction and existing pool state.
176
215
  *
177
216
  * When the incoming tx shares nullifiers with existing pending txs:
178
- * - If the incoming tx has strictly higher priority, mark conflicting txs for eviction
179
- * - If any conflicting tx has equal or higher priority, ignore the incoming tx
217
+ * - If the incoming tx meets or exceeds the required priority, mark conflicting txs for eviction
218
+ * - Otherwise, ignore the incoming tx
219
+ *
220
+ * When `priceBumpPercentage` is provided (RPC path), uses fee-only comparison with the
221
+ * percentage bump instead of `comparePriority`.
180
222
  *
181
223
  * @param incomingMeta - Metadata for the incoming transaction
182
224
  * @param getTxHashByNullifier - Accessor to find which tx uses a nullifier
183
225
  * @param getMetadata - Accessor to get metadata for a tx hash
226
+ * @param priceBumpPercentage - Optional percentage bump required for fee-based replacement
184
227
  */
185
228
  export function checkNullifierConflict(
186
229
  incomingMeta: TxMetaData,
187
230
  getTxHashByNullifier: (nullifier: string) => string | undefined,
188
231
  getMetadata: (txHash: string) => TxMetaData | undefined,
232
+ priceBumpPercentage?: bigint,
189
233
  ): PreAddResult {
190
234
  const txHashesToEvict: string[] = [];
191
235
 
@@ -206,19 +250,32 @@ export function checkNullifierConflict(
206
250
  continue;
207
251
  }
208
252
 
209
- // If incoming tx has strictly higher priority, mark for eviction
210
- // Otherwise, ignore incoming tx (ties go to existing tx)
211
- // Use comparePriority for deterministic ordering (includes txHash as tiebreaker)
212
- if (comparePriority(incomingMeta, conflictingMeta) > 0) {
253
+ // When price bump is set (RPC path), require the incoming fee to meet the bumped threshold.
254
+ // Otherwise (P2P path), use full comparePriority with tx hash tiebreaker.
255
+ const isHigherPriority =
256
+ priceBumpPercentage !== undefined
257
+ ? incomingMeta.priorityFee >= getMinimumPriceBumpFee(conflictingMeta.priorityFee, priceBumpPercentage)
258
+ : comparePriority(incomingMeta, conflictingMeta) > 0;
259
+
260
+ if (isHigherPriority) {
213
261
  txHashesToEvict.push(conflictingHashStr);
214
262
  } else {
263
+ const minimumFee =
264
+ priceBumpPercentage !== undefined
265
+ ? getMinimumPriceBumpFee(conflictingMeta.priorityFee, priceBumpPercentage)
266
+ : undefined;
215
267
  return {
216
268
  shouldIgnore: true,
217
269
  txHashesToEvict: [],
218
270
  reason: {
219
271
  code: TxPoolRejectionCode.NULLIFIER_CONFLICT,
220
- message: `Nullifier conflict with existing tx ${conflictingHashStr}`,
272
+ message:
273
+ minimumFee !== undefined
274
+ ? `Nullifier conflict with existing tx ${conflictingHashStr}. Minimum required fee: ${minimumFee}, got: ${incomingMeta.priorityFee}`
275
+ : `Nullifier conflict with existing tx ${conflictingHashStr}`,
221
276
  conflictingTxHash: conflictingHashStr,
277
+ minimumPriceBumpFee: minimumFee,
278
+ txPriorityFee: minimumFee !== undefined ? incomingMeta.priorityFee : undefined,
222
279
  },
223
280
  };
224
281
  }
@@ -237,6 +294,44 @@ export function stubTxMetaValidationData(overrides: { expirationTimestamp?: bigi
237
294
  hash: () => Promise.resolve(new BlockHash(Fr.ZERO)),
238
295
  globalVariables: { blockNumber: BlockNumber(0) },
239
296
  },
297
+ txContext: {
298
+ gasSettings: { gasLimits: Gas.empty() },
299
+ },
240
300
  },
241
301
  };
242
302
  }
303
+
304
+ /** Creates a stub TxMetaData for tests. All fields have sensible defaults and can be overridden. */
305
+ export function stubTxMetaData(
306
+ txHash: string,
307
+ overrides: {
308
+ priorityFee?: bigint;
309
+ feePayer?: string;
310
+ claimAmount?: bigint;
311
+ feeLimit?: bigint;
312
+ nullifiers?: string[];
313
+ expirationTimestamp?: bigint;
314
+ anchorBlockHeaderHash?: string;
315
+ allowedSetupCalls?: boolean;
316
+ } = {},
317
+ ): TxMetaData {
318
+ const txHashBigInt = Fr.fromHexString(txHash).toBigInt();
319
+ // Normalize to canonical zero-padded hex so txHashFromBigInt(txHashBigInt) === normalizedTxHash
320
+ const normalizedTxHash = txHashFromBigInt(txHashBigInt);
321
+ const expirationTimestamp = overrides.expirationTimestamp ?? 0n;
322
+ return {
323
+ txHash: normalizedTxHash,
324
+ txHashBigInt,
325
+ anchorBlockHeaderHash: overrides.anchorBlockHeaderHash ?? '0x1234',
326
+ priorityFee: overrides.priorityFee ?? 100n,
327
+ feePayer: overrides.feePayer ?? '0xfeepayer',
328
+ claimAmount: overrides.claimAmount ?? 0n,
329
+ feeLimit: overrides.feeLimit ?? 100n,
330
+ nullifiers: overrides.nullifiers ?? [`0x${normalizedTxHash.slice(2)}null1`],
331
+ expirationTimestamp,
332
+ allowedSetupCalls: overrides.allowedSetupCalls ?? true,
333
+ receivedAt: 0,
334
+ estimatedSizeBytes: 0,
335
+ data: stubTxMetaValidationData({ expirationTimestamp }),
336
+ };
337
+ }
@@ -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
  }
@@ -11,7 +11,14 @@ import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-clien
11
11
  import EventEmitter from 'node:events';
12
12
 
13
13
  import { PoolInstrumentation, PoolName } from '../instrumentation.js';
14
- import type { AddTxsResult, TxPoolV2, TxPoolV2Config, TxPoolV2Dependencies, TxPoolV2Events } from './interfaces.js';
14
+ import type {
15
+ AddTxsResult,
16
+ PoolReadAccess,
17
+ TxPoolV2,
18
+ TxPoolV2Config,
19
+ TxPoolV2Dependencies,
20
+ TxPoolV2Events,
21
+ } from './interfaces.js';
15
22
  import type { TxState } from './tx_metadata.js';
16
23
  import { TxPoolV2Impl } from './tx_pool_v2_impl.js';
17
24
 
@@ -74,7 +81,7 @@ export class AztecKVTxPoolV2 extends (EventEmitter as new () => TypedEventEmitte
74
81
  return this.#queue.put(() => this.#impl.addPendingTxs(txs, opts));
75
82
  }
76
83
 
77
- canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored' | 'rejected'> {
84
+ canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored'> {
78
85
  return this.#queue.put(() => this.#impl.canAddPendingTx(tx));
79
86
  }
80
87
 
@@ -162,6 +169,11 @@ export class AztecKVTxPoolV2 extends (EventEmitter as new () => TypedEventEmitte
162
169
  return this.#queue.put(() => Promise.resolve(this.#impl.getLowestPriorityPending(limit)));
163
170
  }
164
171
 
172
+ /** Returns read-only access to the pool. Used for testing. */
173
+ getPoolReadAccess(): PoolReadAccess {
174
+ return this.#impl.getPoolReadAccess();
175
+ }
176
+
165
177
  // === Configuration ===
166
178
 
167
179
  updateConfig(config: Partial<TxPoolV2Config>): Promise<void> {
@@ -61,6 +61,7 @@ export class TxPoolV2Impl {
61
61
  #l2BlockSource: L2BlockSource;
62
62
  #worldStateSynchronizer: WorldStateSynchronizer;
63
63
  #createTxValidator: TxPoolV2Dependencies['createTxValidator'];
64
+ #checkAllowedSetupCalls: TxPoolV2Dependencies['checkAllowedSetupCalls'];
64
65
 
65
66
  // === In-Memory Indices ===
66
67
  #indices: TxPoolIndices = new TxPoolIndices();
@@ -92,6 +93,7 @@ export class TxPoolV2Impl {
92
93
  this.#l2BlockSource = deps.l2BlockSource;
93
94
  this.#worldStateSynchronizer = deps.worldStateSynchronizer;
94
95
  this.#createTxValidator = deps.createTxValidator;
96
+ this.#checkAllowedSetupCalls = deps.checkAllowedSetupCalls;
95
97
 
96
98
  this.#config = { ...DEFAULT_TX_POOL_V2_CONFIG, ...config };
97
99
  this.#archive = new TxArchive(archiveStore, this.#config.archivedTxLimit, log);
@@ -187,9 +189,35 @@ export class TxPoolV2Impl {
187
189
  const errors = new Map<string, TxPoolRejectionError>();
188
190
  const acceptedPending = new Set<string>();
189
191
 
192
+ // Phase 1: Pre-compute all throwable I/O outside the transaction.
193
+ // If any pre-computation throws, the entire call fails before mutations happen.
194
+ const precomputed = new Map<string, { meta: TxMetaData; minedBlockId: L2BlockId | undefined; isValid: boolean }>();
195
+
196
+ const validator = await this.#createTxValidator();
197
+
198
+ for (const tx of txs) {
199
+ const txHash = tx.getTxHash();
200
+ const txHashStr = txHash.toString();
201
+
202
+ const meta = await buildTxMetaData(tx);
203
+ const minedBlockId = await this.#getMinedBlockId(txHash);
204
+
205
+ // Validate non-mined txs (mined and pre-protected txs bypass validation inside the transaction)
206
+ let isValid = true;
207
+ if (!minedBlockId) {
208
+ isValid = await this.#validateMeta(meta, validator);
209
+ }
210
+
211
+ precomputed.set(txHashStr, { meta, minedBlockId, isValid });
212
+ }
213
+
214
+ // Phase 2: Apply mutations inside the transaction using only pre-computed results,
215
+ // in-memory reads, and buffered DB writes. Nothing here can throw an unhandled exception.
190
216
  const poolAccess = this.#createPreAddPoolAccess();
191
217
  const preAddContext: PreAddContext | undefined =
192
- opts.feeComparisonOnly !== undefined ? { feeComparisonOnly: opts.feeComparisonOnly } : undefined;
218
+ opts.feeComparisonOnly !== undefined
219
+ ? { feeComparisonOnly: opts.feeComparisonOnly, priceBumpPercentage: this.#config.priceBumpPercentage }
220
+ : undefined;
193
221
 
194
222
  await this.#store.transactionAsync(async () => {
195
223
  for (const tx of txs) {
@@ -202,22 +230,25 @@ export class TxPoolV2Impl {
202
230
  continue;
203
231
  }
204
232
 
205
- // Check mined status first (applies to all paths)
206
- const minedBlockId = await this.#getMinedBlockId(txHash);
233
+ const { meta, minedBlockId, isValid } = precomputed.get(txHashStr)!;
207
234
  const preProtectedSlot = this.#indices.getProtectionSlot(txHashStr);
208
235
 
209
236
  if (minedBlockId) {
210
237
  // Already mined - add directly (protection already set if pre-protected)
211
- await this.#addTx(tx, { mined: minedBlockId }, opts);
238
+ await this.#addTx(tx, { mined: minedBlockId }, opts, meta);
212
239
  accepted.push(txHash);
213
240
  } else if (preProtectedSlot !== undefined) {
214
241
  // Pre-protected and not mined - add as protected (bypass validation)
215
- await this.#addTx(tx, { protected: preProtectedSlot }, opts);
242
+ await this.#addTx(tx, { protected: preProtectedSlot }, opts, meta);
216
243
  accepted.push(txHash);
244
+ } else if (!isValid) {
245
+ // Failed pre-computed validation
246
+ rejected.push(txHash);
217
247
  } else {
218
- // Regular pending tx - validate and run pre-add rules
248
+ // Regular pending tx - run pre-add rules using pre-computed metadata
219
249
  const result = await this.#tryAddRegularPendingTx(
220
250
  tx,
251
+ meta,
221
252
  opts,
222
253
  poolAccess,
223
254
  acceptedPending,
@@ -227,8 +258,6 @@ export class TxPoolV2Impl {
227
258
  );
228
259
  if (result.status === 'accepted') {
229
260
  acceptedPending.add(txHashStr);
230
- } else if (result.status === 'rejected') {
231
- rejected.push(txHash);
232
261
  } else {
233
262
  ignored.push(txHash);
234
263
  }
@@ -259,27 +288,21 @@ export class TxPoolV2Impl {
259
288
  return { accepted, ignored, rejected, ...(errors.size > 0 ? { errors } : {}) };
260
289
  }
261
290
 
262
- /** Validates and adds a regular pending tx. Returns status. */
291
+ /** Adds a validated pending tx, running pre-add rules and evicting conflicts. */
263
292
  async #tryAddRegularPendingTx(
264
293
  tx: Tx,
294
+ precomputedMeta: TxMetaData,
265
295
  opts: { source?: string },
266
296
  poolAccess: PreAddPoolAccess,
267
297
  acceptedPending: Set<string>,
268
298
  ignored: TxHash[],
269
299
  errors: Map<string, TxPoolRejectionError>,
270
300
  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
- }
301
+ ): Promise<{ status: 'accepted' | 'ignored' }> {
302
+ const txHashStr = tx.getTxHash().toString();
280
303
 
281
304
  // Run pre-add rules
282
- const preAddResult = await this.#evictionManager.runPreAddRules(meta, poolAccess, preAddContext);
305
+ const preAddResult = await this.#evictionManager.runPreAddRules(precomputedMeta, poolAccess, preAddContext);
283
306
 
284
307
  if (preAddResult.shouldIgnore) {
285
308
  this.#log.debug(`Ignoring tx ${txHashStr}: ${preAddResult.reason?.message ?? 'unknown reason'}`);
@@ -316,18 +339,12 @@ export class TxPoolV2Impl {
316
339
  }
317
340
  }
318
341
 
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
342
  // Add the transaction
326
- await this.#addTx(tx, 'pending', opts);
343
+ await this.#addTx(tx, 'pending', opts, precomputedMeta);
327
344
  return { status: 'accepted' };
328
345
  }
329
346
 
330
- async canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored' | 'rejected'> {
347
+ async canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored'> {
331
348
  const txHashStr = tx.getTxHash().toString();
332
349
 
333
350
  // Check if already in pool
@@ -335,14 +352,8 @@ export class TxPoolV2Impl {
335
352
  return 'ignored';
336
353
  }
337
354
 
338
- // Build metadata and validate using metadata
355
+ // Build metadata and check pre-add rules
339
356
  const meta = await buildTxMetaData(tx);
340
- const validationResult = await this.#validateMeta(meta, undefined, 'can add pending');
341
- if (validationResult !== true) {
342
- return 'rejected';
343
- }
344
-
345
- // Use pre-add rules
346
357
  const poolAccess = this.#createPreAddPoolAccess();
347
358
  const preAddResult = await this.#evictionManager.runPreAddRules(meta, poolAccess);
348
359
 
@@ -352,20 +363,25 @@ export class TxPoolV2Impl {
352
363
  async addProtectedTxs(txs: Tx[], block: BlockHeader, opts: { source?: string }): Promise<void> {
353
364
  const slotNumber = block.globalVariables.slotNumber;
354
365
 
366
+ // Precompute setup-call allow-list flags outside the store transaction
367
+ const allowedFlags = await Promise.all(txs.map(tx => this.#checkAllowedSetupCalls(tx)));
368
+
355
369
  await this.#store.transactionAsync(async () => {
356
- for (const tx of txs) {
370
+ for (let i = 0; i < txs.length; i++) {
371
+ const tx = txs[i];
357
372
  const txHash = tx.getTxHash();
358
373
  const txHashStr = txHash.toString();
359
374
  const isNew = !this.#indices.has(txHashStr);
360
375
  const minedBlockId = await this.#getMinedBlockId(txHash);
361
376
 
362
377
  if (isNew) {
378
+ const meta = await buildTxMetaData(tx, allowedFlags[i]);
363
379
  // New tx - add as mined or protected (callback emitted by #addTx)
364
380
  if (minedBlockId) {
365
- await this.#addTx(tx, { mined: minedBlockId }, opts);
381
+ await this.#addTx(tx, { mined: minedBlockId }, opts, meta);
366
382
  this.#indices.setProtection(txHashStr, slotNumber);
367
383
  } else {
368
- await this.#addTx(tx, { protected: slotNumber }, opts);
384
+ await this.#addTx(tx, { protected: slotNumber }, opts, meta);
369
385
  }
370
386
  } else {
371
387
  // Existing tx - update protection and mined status
@@ -771,9 +787,10 @@ export class TxPoolV2Impl {
771
787
  tx: Tx,
772
788
  state: 'pending' | { protected: SlotNumber } | { mined: L2BlockId },
773
789
  opts: { source?: string } = {},
790
+ precomputedMeta?: TxMetaData,
774
791
  ): Promise<TxMetaData> {
775
792
  const txHashStr = tx.getTxHash().toString();
776
- const meta = await buildTxMetaData(tx);
793
+ const meta = precomputedMeta ?? (await buildTxMetaData(tx));
777
794
  meta.receivedAt = this.#dateProvider.now();
778
795
 
779
796
  await this.#txsDB.set(txHashStr, tx.toBuffer());
@@ -955,7 +972,8 @@ export class TxPoolV2Impl {
955
972
 
956
973
  try {
957
974
  const tx = Tx.fromBuffer(buffer);
958
- const meta = await buildTxMetaData(tx);
975
+ const allowedSetupCalls = await this.#checkAllowedSetupCalls(tx);
976
+ const meta = await buildTxMetaData(tx, allowedSetupCalls);
959
977
  loaded.push({ tx, meta });
960
978
  } catch (err) {
961
979
  this.#log.warn(`Failed to deserialize tx ${txHashStr}, deleting`, { err });