@aztec/p2p 0.0.1-commit.8f9871590 → 0.0.1-commit.9117c5f5a

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 (218) hide show
  1. package/dest/client/factory.d.ts +6 -6
  2. package/dest/client/factory.d.ts.map +1 -1
  3. package/dest/client/factory.js +18 -28
  4. package/dest/client/interface.d.ts +10 -19
  5. package/dest/client/interface.d.ts.map +1 -1
  6. package/dest/client/p2p_client.d.ts +7 -18
  7. package/dest/client/p2p_client.d.ts.map +1 -1
  8. package/dest/client/p2p_client.js +41 -72
  9. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.js +6 -7
  10. package/dest/config.d.ts +4 -6
  11. package/dest/config.d.ts.map +1 -1
  12. package/dest/config.js +0 -5
  13. package/dest/errors/tx-pool.error.d.ts +8 -0
  14. package/dest/errors/tx-pool.error.d.ts.map +1 -0
  15. package/dest/errors/tx-pool.error.js +9 -0
  16. package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts +3 -1
  17. package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts.map +1 -1
  18. package/dest/mem_pools/tx_pool_v2/deleted_pool.js +9 -0
  19. package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.d.ts +3 -3
  20. package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.d.ts.map +1 -1
  21. package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.js +18 -9
  22. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +3 -3
  23. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts +3 -3
  24. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.d.ts.map +1 -1
  25. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.js +12 -4
  26. package/dest/mem_pools/tx_pool_v2/eviction/index.d.ts +2 -2
  27. package/dest/mem_pools/tx_pool_v2/eviction/index.d.ts.map +1 -1
  28. package/dest/mem_pools/tx_pool_v2/eviction/index.js +1 -1
  29. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts +48 -5
  30. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts.map +1 -1
  31. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.js +8 -0
  32. package/dest/mem_pools/tx_pool_v2/eviction/invalid_txs_after_mining_rule.js +2 -2
  33. package/dest/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.js +5 -5
  34. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.d.ts +2 -2
  35. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.d.ts.map +1 -1
  36. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.js +12 -6
  37. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts +4 -4
  38. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts.map +1 -1
  39. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.js +14 -4
  40. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts +3 -3
  41. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts.map +1 -1
  42. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.js +2 -2
  43. package/dest/mem_pools/tx_pool_v2/index.d.ts +2 -2
  44. package/dest/mem_pools/tx_pool_v2/index.d.ts.map +1 -1
  45. package/dest/mem_pools/tx_pool_v2/index.js +1 -1
  46. package/dest/mem_pools/tx_pool_v2/instrumentation.d.ts +15 -0
  47. package/dest/mem_pools/tx_pool_v2/instrumentation.d.ts.map +1 -0
  48. package/dest/mem_pools/tx_pool_v2/instrumentation.js +43 -0
  49. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +16 -6
  50. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  51. package/dest/mem_pools/tx_pool_v2/interfaces.js +3 -1
  52. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +29 -5
  53. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  54. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +71 -6
  55. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +5 -2
  56. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
  57. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +21 -12
  58. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +6 -3
  59. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  60. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +6 -5
  61. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +11 -5
  62. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  63. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +274 -149
  64. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts +2 -2
  65. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts.map +1 -1
  66. package/dest/msg_validators/tx_validator/aggregate_tx_validator.js +3 -3
  67. package/dest/msg_validators/tx_validator/factory.d.ts +114 -6
  68. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  69. package/dest/msg_validators/tx_validator/factory.js +219 -58
  70. package/dest/msg_validators/tx_validator/gas_validator.d.ts +58 -3
  71. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  72. package/dest/msg_validators/tx_validator/gas_validator.js +73 -36
  73. package/dest/msg_validators/tx_validator/index.d.ts +2 -1
  74. package/dest/msg_validators/tx_validator/index.d.ts.map +1 -1
  75. package/dest/msg_validators/tx_validator/index.js +1 -0
  76. package/dest/msg_validators/tx_validator/nullifier_cache.d.ts +14 -0
  77. package/dest/msg_validators/tx_validator/nullifier_cache.d.ts.map +1 -0
  78. package/dest/msg_validators/tx_validator/nullifier_cache.js +24 -0
  79. package/dest/services/dummy_service.d.ts +4 -4
  80. package/dest/services/dummy_service.d.ts.map +1 -1
  81. package/dest/services/dummy_service.js +4 -4
  82. package/dest/services/encoding.d.ts +2 -2
  83. package/dest/services/encoding.d.ts.map +1 -1
  84. package/dest/services/encoding.js +9 -8
  85. package/dest/services/gossipsub/topic_score_params.d.ts +18 -6
  86. package/dest/services/gossipsub/topic_score_params.d.ts.map +1 -1
  87. package/dest/services/gossipsub/topic_score_params.js +32 -10
  88. package/dest/services/libp2p/libp2p_service.d.ts +16 -13
  89. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  90. package/dest/services/libp2p/libp2p_service.js +69 -81
  91. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts +4 -3
  92. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts.map +1 -1
  93. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.js +19 -46
  94. package/dest/services/reqresp/batch-tx-requester/interface.d.ts +2 -6
  95. package/dest/services/reqresp/batch-tx-requester/interface.d.ts.map +1 -1
  96. package/dest/services/reqresp/batch-tx-requester/missing_txs.d.ts +10 -13
  97. package/dest/services/reqresp/batch-tx-requester/missing_txs.d.ts.map +1 -1
  98. package/dest/services/reqresp/batch-tx-requester/missing_txs.js +25 -46
  99. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts +17 -11
  100. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts.map +1 -1
  101. package/dest/services/reqresp/batch-tx-requester/peer_collection.js +49 -15
  102. package/dest/services/reqresp/batch-tx-requester/tx_validator.js +2 -2
  103. package/dest/services/reqresp/reqresp.d.ts +1 -1
  104. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  105. package/dest/services/reqresp/reqresp.js +2 -1
  106. package/dest/services/service.d.ts +5 -3
  107. package/dest/services/service.d.ts.map +1 -1
  108. package/dest/services/tx_collection/fast_tx_collection.d.ts +1 -1
  109. package/dest/services/tx_collection/fast_tx_collection.d.ts.map +1 -1
  110. package/dest/services/tx_collection/fast_tx_collection.js +39 -33
  111. package/dest/services/tx_collection/file_store_tx_collection.d.ts +1 -1
  112. package/dest/services/tx_collection/file_store_tx_collection.d.ts.map +1 -1
  113. package/dest/services/tx_collection/file_store_tx_collection.js +4 -2
  114. package/dest/services/tx_collection/file_store_tx_source.d.ts +15 -6
  115. package/dest/services/tx_collection/file_store_tx_source.d.ts.map +1 -1
  116. package/dest/services/tx_collection/file_store_tx_source.js +47 -16
  117. package/dest/services/tx_collection/instrumentation.d.ts +1 -1
  118. package/dest/services/tx_collection/instrumentation.d.ts.map +1 -1
  119. package/dest/services/tx_collection/instrumentation.js +2 -1
  120. package/dest/services/tx_collection/missing_txs_tracker.d.ts +32 -0
  121. package/dest/services/tx_collection/missing_txs_tracker.d.ts.map +1 -0
  122. package/dest/services/tx_collection/missing_txs_tracker.js +27 -0
  123. package/dest/services/tx_collection/proposal_tx_collector.d.ts +7 -6
  124. package/dest/services/tx_collection/proposal_tx_collector.d.ts.map +1 -1
  125. package/dest/services/tx_collection/proposal_tx_collector.js +5 -4
  126. package/dest/services/tx_collection/slow_tx_collection.d.ts +2 -2
  127. package/dest/services/tx_collection/slow_tx_collection.d.ts.map +1 -1
  128. package/dest/services/tx_collection/slow_tx_collection.js +10 -8
  129. package/dest/services/tx_collection/tx_collection.d.ts +5 -4
  130. package/dest/services/tx_collection/tx_collection.d.ts.map +1 -1
  131. package/dest/services/tx_collection/tx_collection_sink.d.ts +6 -5
  132. package/dest/services/tx_collection/tx_collection_sink.d.ts.map +1 -1
  133. package/dest/services/tx_collection/tx_collection_sink.js +13 -22
  134. package/dest/services/tx_collection/tx_source.d.ts +8 -3
  135. package/dest/services/tx_collection/tx_source.d.ts.map +1 -1
  136. package/dest/services/tx_collection/tx_source.js +19 -2
  137. package/dest/services/tx_file_store/tx_file_store.js +1 -1
  138. package/dest/services/tx_provider.d.ts +3 -3
  139. package/dest/services/tx_provider.d.ts.map +1 -1
  140. package/dest/services/tx_provider.js +4 -4
  141. package/dest/test-helpers/make-test-p2p-clients.d.ts +5 -6
  142. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  143. package/dest/test-helpers/make-test-p2p-clients.js +1 -2
  144. package/dest/test-helpers/mock-pubsub.d.ts +4 -4
  145. package/dest/test-helpers/mock-pubsub.d.ts.map +1 -1
  146. package/dest/test-helpers/mock-pubsub.js +8 -2
  147. package/dest/test-helpers/reqresp-nodes.d.ts +2 -3
  148. package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
  149. package/dest/test-helpers/reqresp-nodes.js +2 -2
  150. package/dest/test-helpers/testbench-utils.d.ts +6 -3
  151. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  152. package/dest/test-helpers/testbench-utils.js +1 -1
  153. package/dest/testbench/p2p_client_testbench_worker.d.ts +2 -2
  154. package/dest/testbench/p2p_client_testbench_worker.d.ts.map +1 -1
  155. package/dest/testbench/p2p_client_testbench_worker.js +11 -11
  156. package/dest/util.d.ts +2 -2
  157. package/dest/util.d.ts.map +1 -1
  158. package/package.json +14 -14
  159. package/src/client/factory.ts +27 -48
  160. package/src/client/interface.ts +11 -20
  161. package/src/client/p2p_client.ts +47 -104
  162. package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +19 -10
  163. package/src/config.ts +2 -10
  164. package/src/errors/tx-pool.error.ts +12 -0
  165. package/src/mem_pools/tx_pool_v2/deleted_pool.ts +11 -0
  166. package/src/mem_pools/tx_pool_v2/eviction/eviction_manager.ts +21 -8
  167. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +3 -3
  168. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.ts +18 -4
  169. package/src/mem_pools/tx_pool_v2/eviction/index.ts +4 -0
  170. package/src/mem_pools/tx_pool_v2/eviction/interfaces.ts +49 -4
  171. package/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_mining_rule.ts +2 -2
  172. package/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.ts +5 -5
  173. package/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.ts +12 -9
  174. package/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.ts +24 -6
  175. package/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.ts +3 -3
  176. package/src/mem_pools/tx_pool_v2/index.ts +1 -1
  177. package/src/mem_pools/tx_pool_v2/instrumentation.ts +69 -0
  178. package/src/mem_pools/tx_pool_v2/interfaces.ts +15 -6
  179. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +96 -10
  180. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +25 -14
  181. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +12 -7
  182. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +303 -144
  183. package/src/msg_validators/tx_validator/README.md +115 -0
  184. package/src/msg_validators/tx_validator/aggregate_tx_validator.ts +3 -3
  185. package/src/msg_validators/tx_validator/factory.ts +353 -77
  186. package/src/msg_validators/tx_validator/gas_validator.ts +90 -27
  187. package/src/msg_validators/tx_validator/index.ts +1 -0
  188. package/src/msg_validators/tx_validator/nullifier_cache.ts +30 -0
  189. package/src/services/dummy_service.ts +6 -6
  190. package/src/services/encoding.ts +7 -7
  191. package/src/services/gossipsub/README.md +29 -14
  192. package/src/services/gossipsub/topic_score_params.ts +49 -13
  193. package/src/services/libp2p/libp2p_service.ts +80 -90
  194. package/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +20 -48
  195. package/src/services/reqresp/batch-tx-requester/interface.ts +1 -5
  196. package/src/services/reqresp/batch-tx-requester/missing_txs.ts +23 -71
  197. package/src/services/reqresp/batch-tx-requester/peer_collection.ts +63 -24
  198. package/src/services/reqresp/batch-tx-requester/tx_validator.ts +2 -2
  199. package/src/services/reqresp/reqresp.ts +3 -1
  200. package/src/services/service.ts +11 -2
  201. package/src/services/tx_collection/fast_tx_collection.ts +51 -30
  202. package/src/services/tx_collection/file_store_tx_collection.ts +7 -3
  203. package/src/services/tx_collection/file_store_tx_source.ts +61 -17
  204. package/src/services/tx_collection/instrumentation.ts +7 -1
  205. package/src/services/tx_collection/missing_txs_tracker.ts +52 -0
  206. package/src/services/tx_collection/proposal_tx_collector.ts +8 -7
  207. package/src/services/tx_collection/slow_tx_collection.ts +8 -9
  208. package/src/services/tx_collection/tx_collection.ts +4 -3
  209. package/src/services/tx_collection/tx_collection_sink.ts +15 -29
  210. package/src/services/tx_collection/tx_source.ts +22 -3
  211. package/src/services/tx_file_store/tx_file_store.ts +1 -1
  212. package/src/services/tx_provider.ts +2 -2
  213. package/src/test-helpers/make-test-p2p-clients.ts +0 -2
  214. package/src/test-helpers/mock-pubsub.ts +13 -6
  215. package/src/test-helpers/reqresp-nodes.ts +2 -5
  216. package/src/test-helpers/testbench-utils.ts +3 -3
  217. package/src/testbench/p2p_client_testbench_worker.ts +20 -17
  218. package/src/util.ts +7 -1
@@ -9,6 +9,7 @@ import type { L2Block, L2BlockId, L2BlockSource } from '@aztec/stdlib/block';
9
9
  import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
10
10
  import { DatabasePublicStateSource } from '@aztec/stdlib/trees';
11
11
  import { BlockHeader, Tx, TxHash, type TxValidator } from '@aztec/stdlib/tx';
12
+ import type { TelemetryClient } from '@aztec/telemetry-client';
12
13
 
13
14
  import { TxArchive } from './archive/index.js';
14
15
  import { DeletedPool } from './deleted_pool.js';
@@ -22,8 +23,12 @@ import {
22
23
  LowPriorityPreAddRule,
23
24
  NullifierConflictRule,
24
25
  type PoolOperations,
26
+ type PreAddContext,
25
27
  type PreAddPoolAccess,
28
+ TxPoolRejectionCode,
29
+ type TxPoolRejectionError,
26
30
  } from './eviction/index.js';
31
+ import { TxPoolV2Instrumentation } from './instrumentation.js';
27
32
  import {
28
33
  type AddTxsResult,
29
34
  DEFAULT_TX_POOL_V2_CONFIG,
@@ -66,6 +71,8 @@ export class TxPoolV2Impl {
66
71
  #deletedPool: DeletedPool;
67
72
  #evictionManager: EvictionManager;
68
73
  #dateProvider: DateProvider;
74
+ #instrumentation: TxPoolV2Instrumentation;
75
+ #evictedTxHashes: Set<string> = new Set();
69
76
  #log: Logger;
70
77
  #callbacks: TxPoolV2Callbacks;
71
78
 
@@ -74,6 +81,7 @@ export class TxPoolV2Impl {
74
81
  archiveStore: AztecAsyncKVStore,
75
82
  deps: TxPoolV2Dependencies,
76
83
  callbacks: TxPoolV2Callbacks,
84
+ telemetry: TelemetryClient,
77
85
  config: Partial<TxPoolV2Config> = {},
78
86
  dateProvider: DateProvider,
79
87
  log: Logger,
@@ -89,6 +97,7 @@ export class TxPoolV2Impl {
89
97
  this.#archive = new TxArchive(archiveStore, this.#config.archivedTxLimit, log);
90
98
  this.#deletedPool = new DeletedPool(store, this.#txsDB, log);
91
99
  this.#dateProvider = dateProvider;
100
+ this.#instrumentation = new TxPoolV2Instrumentation(telemetry, () => this.#indices.getTotalMetadataBytes());
92
101
  this.#log = log;
93
102
  this.#callbacks = callbacks;
94
103
 
@@ -171,13 +180,40 @@ export class TxPoolV2Impl {
171
180
  this.#log.info(`Deleted ${toDelete.length} invalid/rejected transactions on startup`, { txHashes: toDelete });
172
181
  }
173
182
 
174
- async addPendingTxs(txs: Tx[], opts: { source?: string }): Promise<AddTxsResult> {
183
+ async addPendingTxs(txs: Tx[], opts: { source?: string; feeComparisonOnly?: boolean }): Promise<AddTxsResult> {
175
184
  const accepted: TxHash[] = [];
176
185
  const ignored: TxHash[] = [];
177
186
  const rejected: TxHash[] = [];
187
+ const errors = new Map<string, TxPoolRejectionError>();
178
188
  const acceptedPending = new Set<string>();
179
189
 
190
+ // Phase 1: Pre-compute all throwable I/O outside the transaction.
191
+ // If any pre-computation throws, the entire call fails before mutations happen.
192
+ const precomputed = new Map<string, { meta: TxMetaData; minedBlockId: L2BlockId | undefined; isValid: boolean }>();
193
+
194
+ const validator = await this.#createTxValidator();
195
+
196
+ for (const tx of txs) {
197
+ const txHash = tx.getTxHash();
198
+ const txHashStr = txHash.toString();
199
+
200
+ const meta = await buildTxMetaData(tx);
201
+ const minedBlockId = await this.#getMinedBlockId(txHash);
202
+
203
+ // Validate non-mined txs (mined and pre-protected txs bypass validation inside the transaction)
204
+ let isValid = true;
205
+ if (!minedBlockId) {
206
+ isValid = await this.#validateMeta(meta, validator);
207
+ }
208
+
209
+ precomputed.set(txHashStr, { meta, minedBlockId, isValid });
210
+ }
211
+
212
+ // Phase 2: Apply mutations inside the transaction using only pre-computed results,
213
+ // in-memory reads, and buffered DB writes. Nothing here can throw an unhandled exception.
180
214
  const poolAccess = this.#createPreAddPoolAccess();
215
+ const preAddContext: PreAddContext | undefined =
216
+ opts.feeComparisonOnly !== undefined ? { feeComparisonOnly: opts.feeComparisonOnly } : undefined;
181
217
 
182
218
  await this.#store.transactionAsync(async () => {
183
219
  for (const tx of txs) {
@@ -190,30 +226,46 @@ export class TxPoolV2Impl {
190
226
  continue;
191
227
  }
192
228
 
193
- // Check mined status first (applies to all paths)
194
- const minedBlockId = await this.#getMinedBlockId(txHash);
229
+ const { meta, minedBlockId, isValid } = precomputed.get(txHashStr)!;
195
230
  const preProtectedSlot = this.#indices.getProtectionSlot(txHashStr);
196
231
 
197
232
  if (minedBlockId) {
198
233
  // Already mined - add directly (protection already set if pre-protected)
199
- await this.#addTx(tx, { mined: minedBlockId }, opts);
234
+ await this.#addTx(tx, { mined: minedBlockId }, opts, meta);
200
235
  accepted.push(txHash);
201
236
  } else if (preProtectedSlot !== undefined) {
202
237
  // Pre-protected and not mined - add as protected (bypass validation)
203
- await this.#addTx(tx, { protected: preProtectedSlot }, opts);
238
+ await this.#addTx(tx, { protected: preProtectedSlot }, opts, meta);
204
239
  accepted.push(txHash);
240
+ } else if (!isValid) {
241
+ // Failed pre-computed validation
242
+ rejected.push(txHash);
205
243
  } else {
206
- // Regular pending tx - validate and run pre-add rules
207
- const result = await this.#tryAddRegularPendingTx(tx, opts, poolAccess, acceptedPending, ignored);
244
+ // Regular pending tx - run pre-add rules using pre-computed metadata
245
+ const result = await this.#tryAddRegularPendingTx(
246
+ tx,
247
+ meta,
248
+ opts,
249
+ poolAccess,
250
+ acceptedPending,
251
+ ignored,
252
+ errors,
253
+ preAddContext,
254
+ );
208
255
  if (result.status === 'accepted') {
209
256
  acceptedPending.add(txHashStr);
210
- } else if (result.status === 'rejected') {
211
- rejected.push(txHash);
212
257
  } else {
213
258
  ignored.push(txHash);
214
259
  }
215
260
  }
216
261
  }
262
+
263
+ // Run post-add eviction rules for pending txs (inside transaction for atomicity)
264
+ if (acceptedPending.size > 0) {
265
+ const feePayers = Array.from(acceptedPending).map(txHash => this.#indices.getMetadata(txHash)!.feePayer);
266
+ const uniqueFeePayers = new Set<string>(feePayers);
267
+ await this.#evictionManager.evictAfterNewTxs(Array.from(acceptedPending), [...uniqueFeePayers]);
268
+ }
217
269
  });
218
270
 
219
271
  // Build final accepted list for pending txs (excludes intra-batch evictions)
@@ -221,61 +273,80 @@ export class TxPoolV2Impl {
221
273
  accepted.push(TxHash.fromString(txHashStr));
222
274
  }
223
275
 
224
- // Run post-add eviction rules for pending txs
225
- if (acceptedPending.size > 0) {
226
- const feePayers = Array.from(acceptedPending).map(txHash => this.#indices.getMetadata(txHash)!.feePayer);
227
- const uniqueFeePayers = new Set<string>(feePayers);
228
- await this.#evictionManager.evictAfterNewTxs(Array.from(acceptedPending), [...uniqueFeePayers]);
276
+ // Record metrics
277
+ if (ignored.length > 0) {
278
+ this.#instrumentation.recordIgnored(ignored.length);
279
+ }
280
+ if (rejected.length > 0) {
281
+ this.#instrumentation.recordRejected(rejected.length);
229
282
  }
230
283
 
231
- return { accepted, ignored, rejected };
284
+ return { accepted, ignored, rejected, ...(errors.size > 0 ? { errors } : {}) };
232
285
  }
233
286
 
234
- /** Validates and adds a regular pending tx. Returns status. */
287
+ /** Adds a validated pending tx, running pre-add rules and evicting conflicts. */
235
288
  async #tryAddRegularPendingTx(
236
289
  tx: Tx,
290
+ precomputedMeta: TxMetaData,
237
291
  opts: { source?: string },
238
292
  poolAccess: PreAddPoolAccess,
239
293
  acceptedPending: Set<string>,
240
294
  ignored: TxHash[],
241
- ): Promise<{ status: 'accepted' | 'ignored' | 'rejected' }> {
242
- const txHash = tx.getTxHash();
243
- const txHashStr = txHash.toString();
244
-
245
- // Build metadata and validate using metadata
246
- const meta = await buildTxMetaData(tx);
247
- if (!(await this.#validateMeta(meta))) {
248
- return { status: 'rejected' };
249
- }
295
+ errors: Map<string, TxPoolRejectionError>,
296
+ preAddContext?: PreAddContext,
297
+ ): Promise<{ status: 'accepted' | 'ignored' }> {
298
+ const txHashStr = tx.getTxHash().toString();
250
299
 
251
300
  // Run pre-add rules
252
- const preAddResult = await this.#evictionManager.runPreAddRules(meta, poolAccess);
301
+ const preAddResult = await this.#evictionManager.runPreAddRules(precomputedMeta, poolAccess, preAddContext);
253
302
 
254
303
  if (preAddResult.shouldIgnore) {
255
- this.#log.debug(`Ignoring tx ${txHashStr}: ${preAddResult.reason}`);
304
+ this.#log.debug(`Ignoring tx ${txHashStr}: ${preAddResult.reason?.message ?? 'unknown reason'}`);
305
+ if (preAddResult.reason && preAddResult.reason.code !== TxPoolRejectionCode.INTERNAL_ERROR) {
306
+ errors.set(txHashStr, preAddResult.reason);
307
+ }
256
308
  return { status: 'ignored' };
257
309
  }
258
310
 
259
- // Evict conflicts
260
- for (const evictHashStr of preAddResult.txHashesToEvict) {
261
- await this.#deleteTx(evictHashStr);
262
- this.#log.debug(`Evicted tx ${evictHashStr} due to higher-fee tx ${txHashStr}`, {
263
- evictedTxHash: evictHashStr,
264
- replacementTxHash: txHashStr,
265
- });
266
- if (acceptedPending.has(evictHashStr)) {
267
- // Evicted tx was from this batch - mark as ignored in result
268
- acceptedPending.delete(evictHashStr);
269
- ignored.push(TxHash.fromString(evictHashStr));
311
+ // Evict conflicts, grouped by rule name for metrics
312
+ if (preAddResult.evictions && preAddResult.evictions.length > 0) {
313
+ const byReason = new Map<string, string[]>();
314
+ for (const { txHash: evictHash, reason } of preAddResult.evictions) {
315
+ const group = byReason.get(reason);
316
+ if (group) {
317
+ group.push(evictHash);
318
+ } else {
319
+ byReason.set(reason, [evictHash]);
320
+ }
321
+ }
322
+ for (const [reason, hashes] of byReason) {
323
+ await this.#evictTxs(hashes, reason);
324
+ }
325
+ for (const evictHashStr of preAddResult.txHashesToEvict) {
326
+ this.#log.debug(`Evicted tx ${evictHashStr} due to higher-fee tx ${txHashStr}`, {
327
+ evictedTxHash: evictHashStr,
328
+ replacementTxHash: txHashStr,
329
+ });
330
+ if (acceptedPending.has(evictHashStr)) {
331
+ // Evicted tx was from this batch - mark as ignored in result
332
+ acceptedPending.delete(evictHashStr);
333
+ ignored.push(TxHash.fromString(evictHashStr));
334
+ }
270
335
  }
271
336
  }
272
337
 
338
+ // Randomly drop the transaction for testing purposes (report as accepted so it propagates)
339
+ if (this.#config.dropTransactionsProbability > 0 && Math.random() < this.#config.dropTransactionsProbability) {
340
+ this.#log.debug(`Dropping tx ${txHashStr} (simulated drop for testing)`);
341
+ return { status: 'accepted' };
342
+ }
343
+
273
344
  // Add the transaction
274
- await this.#addTx(tx, 'pending', opts);
345
+ await this.#addTx(tx, 'pending', opts, precomputedMeta);
275
346
  return { status: 'accepted' };
276
347
  }
277
348
 
278
- async canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored' | 'rejected'> {
349
+ async canAddPendingTx(tx: Tx): Promise<'accepted' | 'ignored'> {
279
350
  const txHashStr = tx.getTxHash().toString();
280
351
 
281
352
  // Check if already in pool
@@ -283,14 +354,8 @@ export class TxPoolV2Impl {
283
354
  return 'ignored';
284
355
  }
285
356
 
286
- // Build metadata and validate using metadata
357
+ // Build metadata and check pre-add rules
287
358
  const meta = await buildTxMetaData(tx);
288
- const validationResult = await this.#validateMeta(meta, undefined, 'can add pending');
289
- if (validationResult !== true) {
290
- return 'rejected';
291
- }
292
-
293
- // Use pre-add rules
294
359
  const poolAccess = this.#createPreAddPoolAccess();
295
360
  const preAddResult = await this.#evictionManager.runPreAddRules(meta, poolAccess);
296
361
 
@@ -327,23 +392,58 @@ export class TxPoolV2Impl {
327
392
  });
328
393
  }
329
394
 
330
- protectTxs(txHashes: TxHash[], block: BlockHeader): TxHash[] {
395
+ async protectTxs(txHashes: TxHash[], block: BlockHeader): Promise<TxHash[]> {
331
396
  const slotNumber = block.globalVariables.slotNumber;
332
397
  const missing: TxHash[] = [];
398
+ let softDeletedHits = 0;
399
+ let missingPreviouslyEvicted = 0;
333
400
 
334
- for (const txHash of txHashes) {
335
- const txHashStr = txHash.toString();
401
+ await this.#store.transactionAsync(async () => {
402
+ for (const txHash of txHashes) {
403
+ const txHashStr = txHash.toString();
336
404
 
337
- if (this.#indices.has(txHashStr)) {
338
- // Update protection for existing tx
339
- this.#indices.updateProtection(txHashStr, slotNumber);
340
- } else {
341
- // Pre-record protection for tx we don't have yet
342
- this.#indices.setProtection(txHashStr, slotNumber);
343
- missing.push(txHash);
405
+ if (this.#indices.has(txHashStr)) {
406
+ // Update protection for existing tx
407
+ this.#indices.updateProtection(txHashStr, slotNumber);
408
+ } else if (this.#deletedPool.isSoftDeleted(txHashStr)) {
409
+ // Resurrect soft-deleted tx as protected
410
+ const buffer = await this.#txsDB.getAsync(txHashStr);
411
+ if (buffer) {
412
+ const tx = Tx.fromBuffer(buffer);
413
+ await this.#addTx(tx, { protected: slotNumber });
414
+ softDeletedHits++;
415
+ } else {
416
+ // Data missing despite soft-delete flag — treat as truly missing
417
+ this.#indices.setProtection(txHashStr, slotNumber);
418
+ missing.push(txHash);
419
+ }
420
+ } else {
421
+ // Truly missing — pre-record protection for tx we don't have yet
422
+ this.#indices.setProtection(txHashStr, slotNumber);
423
+ missing.push(txHash);
424
+ if (this.#evictedTxHashes.has(txHashStr)) {
425
+ missingPreviouslyEvicted++;
426
+ }
427
+ }
344
428
  }
429
+ });
430
+
431
+ // Record metrics
432
+ if (softDeletedHits > 0) {
433
+ this.#instrumentation.recordSoftDeletedHits(softDeletedHits);
434
+ }
435
+ if (missing.length > 0) {
436
+ this.#log.debug(`protectTxs missing tx hashes: ${missing.map(h => h.toString()).join(', ')}`);
437
+ this.#instrumentation.recordMissingOnProtect(missing.length);
438
+ }
439
+ if (missingPreviouslyEvicted > 0) {
440
+ this.#instrumentation.recordMissingPreviouslyEvicted(missingPreviouslyEvicted);
345
441
  }
346
442
 
443
+ this.#log.info(
444
+ `Protected ${txHashes.length} txs, missing: ${missing.length}, soft-deleted hits: ${softDeletedHits}`,
445
+ );
446
+
347
447
  return missing;
348
448
  }
349
449
 
@@ -387,57 +487,63 @@ export class TxPoolV2Impl {
387
487
  }
388
488
  }
389
489
 
390
- // Step 4: Mark txs as mined (only those we have in the pool)
391
- for (const meta of found) {
392
- this.#indices.markAsMined(meta, blockId);
393
- await this.#deletedPool.clearIfMinedHigher(meta.txHash, blockId.number);
394
- }
490
+ await this.#store.transactionAsync(async () => {
491
+ // Step 4: Mark txs as mined (only those we have in the pool)
492
+ for (const meta of found) {
493
+ this.#indices.markAsMined(meta, blockId);
494
+ await this.#deletedPool.clearIfMinedHigher(meta.txHash, blockId.number);
495
+ }
395
496
 
396
- // Step 5: Run eviction rules (remove pending txs with conflicting nullifiers/expired timestamps)
397
- await this.#evictionManager.evictAfterNewBlock(block.header, nullifiers, feePayers);
497
+ // Step 5: Run post-event eviction rules (inside transaction for atomicity)
498
+ await this.#evictionManager.evictAfterNewBlock(block.header, nullifiers, feePayers);
499
+ });
398
500
 
399
501
  this.#log.info(`Marked ${found.length} txs as mined in block ${blockId.number}`);
400
502
  }
401
503
 
402
504
  async prepareForSlot(slotNumber: SlotNumber): Promise<void> {
403
- // Step 0: Clean up slot-deleted txs from previous slots
404
- await this.#deletedPool.cleanupSlotDeleted(slotNumber);
505
+ await this.#store.transactionAsync(async () => {
506
+ // Step 0: Clean up slot-deleted txs from previous slots
507
+ await this.#deletedPool.cleanupSlotDeleted(slotNumber);
405
508
 
406
- // Step 1: Find expired protected txs
407
- const expiredProtected = this.#indices.findExpiredProtectedTxs(slotNumber);
509
+ // Step 1: Find expired protected txs
510
+ const expiredProtected = this.#indices.findExpiredProtectedTxs(slotNumber);
408
511
 
409
- // Step 2: Clear protection for all expired entries (including those without metadata)
410
- this.#indices.clearProtection(expiredProtected);
512
+ // Step 2: Clear protection for all expired entries (including those without metadata)
513
+ this.#indices.clearProtection(expiredProtected);
411
514
 
412
- // Step 3: Filter to only txs that have metadata and are not mined
413
- const txsToRestore = this.#indices.filterRestorable(expiredProtected);
414
- if (txsToRestore.length === 0) {
415
- return;
416
- }
515
+ // Step 3: Filter to only txs that have metadata and are not mined
516
+ const txsToRestore = this.#indices.filterRestorable(expiredProtected);
517
+ if (txsToRestore.length === 0) {
518
+ this.#log.debug(`Preparing for slot ${slotNumber}, no txs to unprotect`);
519
+ return;
520
+ }
417
521
 
418
- this.#log.info(`Preparing for slot ${slotNumber}: unprotecting ${txsToRestore.length} txs`);
522
+ this.#log.info(`Preparing for slot ${slotNumber}: unprotecting ${txsToRestore.length} txs`);
419
523
 
420
- // Step 4: Validate for pending pool
421
- const { valid, invalid } = await this.#revalidateMetadata(txsToRestore, 'during prepareForSlot');
524
+ // Step 4: Validate for pending pool
525
+ const { valid, invalid } = await this.#revalidateMetadata(txsToRestore, 'during prepareForSlot');
422
526
 
423
- // Step 5: Resolve nullifier conflicts and add winners to pending indices
424
- const { added, toEvict } = this.#applyNullifierConflictResolution(valid);
527
+ // Step 5: Resolve nullifier conflicts and add winners to pending indices
528
+ const { added, toEvict } = this.#applyNullifierConflictResolution(valid);
425
529
 
426
- // Step 6: Delete invalid and evicted txs
427
- await this.#deleteTxsBatch([...invalid, ...toEvict]);
530
+ // Step 6: Delete invalid txs and evict conflict losers
531
+ await this.#deleteTxsBatch(invalid);
532
+ await this.#evictTxs(toEvict, 'NullifierConflict');
428
533
 
429
- // Step 7: Run eviction rules (enforce pool size limit)
430
- if (added.length > 0) {
431
- const feePayers = added.map(meta => meta.feePayer);
432
- const uniqueFeePayers = new Set<string>(feePayers);
433
- await this.#evictionManager.evictAfterNewTxs(
434
- added.map(m => m.txHash),
435
- [...uniqueFeePayers],
436
- );
437
- }
534
+ // Step 7: Run eviction rules (enforce pool size limit)
535
+ if (added.length > 0) {
536
+ const feePayers = added.map(meta => meta.feePayer);
537
+ const uniqueFeePayers = new Set<string>(feePayers);
538
+ await this.#evictionManager.evictAfterNewTxs(
539
+ added.map(m => m.txHash),
540
+ [...uniqueFeePayers],
541
+ );
542
+ }
543
+ });
438
544
  }
439
545
 
440
- async handlePrunedBlocks(latestBlock: L2BlockId): Promise<void> {
546
+ async handlePrunedBlocks(latestBlock: L2BlockId, options?: { deleteAllTxs?: boolean }): Promise<void> {
441
547
  // Step 1: Find transactions mined after the prune point
442
548
  const txsToUnmine = this.#indices.findTxsMinedAfter(latestBlock.number);
443
549
  if (txsToUnmine.length === 0) {
@@ -447,46 +553,60 @@ export class TxPoolV2Impl {
447
553
 
448
554
  this.#log.info(`Handling prune to block ${latestBlock.number}: un-mining ${txsToUnmine.length} txs`);
449
555
 
450
- // Step 2: Mark ALL un-mined txs with their original mined block number
451
- // This ensures they get soft-deleted if removed later, and only hard-deleted
452
- // when their original mined block is finalized
453
- await this.#deletedPool.markFromPrunedBlock(
454
- txsToUnmine.map(m => ({
455
- txHash: m.txHash,
456
- minedAtBlock: BlockNumber(m.minedL2BlockId!.number),
457
- })),
458
- );
556
+ await this.#store.transactionAsync(async () => {
557
+ // Step 2: Mark ALL un-mined txs with their original mined block number
558
+ // This ensures they get soft-deleted if removed later, and only hard-deleted
559
+ // when their original mined block is finalized
560
+ await this.#deletedPool.markFromPrunedBlock(
561
+ txsToUnmine.map(m => ({
562
+ txHash: m.txHash,
563
+ minedAtBlock: BlockNumber(m.minedL2BlockId!.number),
564
+ })),
565
+ );
459
566
 
460
- // Step 3: Unmine - clear mined status from metadata
461
- for (const meta of txsToUnmine) {
462
- this.#indices.markAsUnmined(meta);
463
- }
567
+ // Step 3: Unmine - clear mined status from metadata
568
+ for (const meta of txsToUnmine) {
569
+ this.#indices.markAsUnmined(meta);
570
+ }
464
571
 
465
- // Step 4: Filter out protected txs (they'll be handled by prepareForSlot)
466
- const unprotectedTxs = this.#indices.filterUnprotected(txsToUnmine);
572
+ // If deleteAllTxs is set (epoch prune), delete all un-mined txs and return early
573
+ if (options?.deleteAllTxs) {
574
+ const allTxHashes = txsToUnmine.map(m => m.txHash);
575
+ await this.#deleteTxsBatch(allTxHashes);
576
+ this.#log.info(
577
+ `Handled prune to block ${latestBlock.number} with deleteAllTxs: deleted ${allTxHashes.length} txs`,
578
+ );
579
+ return;
580
+ }
467
581
 
468
- // Step 4: Validate for pending pool
469
- const { valid, invalid } = await this.#revalidateMetadata(unprotectedTxs, 'during handlePrunedBlocks');
582
+ // Step 4: Filter out protected txs (they'll be handled by prepareForSlot)
583
+ const unprotectedTxs = this.#indices.filterUnprotected(txsToUnmine);
470
584
 
471
- // Step 6: Resolve nullifier conflicts and add winners to pending indices
472
- const { toEvict } = this.#applyNullifierConflictResolution(valid);
585
+ // Step 5: Validate for pending pool
586
+ const { valid, invalid } = await this.#revalidateMetadata(unprotectedTxs, 'during handlePrunedBlocks');
473
587
 
474
- // Step 7: Delete invalid and evicted txs
475
- await this.#deleteTxsBatch([...invalid, ...toEvict]);
588
+ // Step 6: Resolve nullifier conflicts and add winners to pending indices
589
+ const { toEvict } = this.#applyNullifierConflictResolution(valid);
476
590
 
477
- this.#log.info(
478
- `Handled prune to block ${latestBlock.number}: ${valid.length} txs restored to pending, ${invalid.length} invalid, ${toEvict.length} evicted due to nullifier conflicts`,
479
- { txHashesRestored: valid.map(m => m.txHash), txHashesInvalid: invalid, txHashesEvicted: toEvict },
480
- );
591
+ // Step 7: Delete invalid txs and evict conflict losers
592
+ await this.#deleteTxsBatch(invalid);
593
+ await this.#evictTxs(toEvict, 'NullifierConflict');
481
594
 
482
- // Step 8: Run eviction rules for ALL pending txs (not just restored ones)
483
- // This handles cases like existing pending txs with invalid fee payer balances
484
- await this.#evictionManager.evictAfterChainPrune(latestBlock.number);
595
+ this.#log.info(
596
+ `Handled prune to block ${latestBlock.number}: ${valid.length} txs restored to pending, ${invalid.length} invalid, ${toEvict.length} evicted due to nullifier conflicts`,
597
+ { txHashesRestored: valid.map(m => m.txHash), txHashesInvalid: invalid, txHashesEvicted: toEvict },
598
+ );
599
+
600
+ // Step 8: Run eviction rules for ALL pending txs (not just restored ones)
601
+ // This handles cases like existing pending txs with invalid fee payer balances
602
+ await this.#evictionManager.evictAfterChainPrune(latestBlock.number);
603
+ });
485
604
  }
486
605
 
487
606
  async handleFailedExecution(txHashes: TxHash[]): Promise<void> {
488
- // Delete failed txs
489
- await this.#deleteTxsBatch(txHashes.map(h => h.toString()));
607
+ await this.#store.transactionAsync(async () => {
608
+ await this.#deleteTxsBatch(txHashes.map(h => h.toString()));
609
+ });
490
610
 
491
611
  this.#log.info(`Deleted ${txHashes.length} failed txs`, { txHashes: txHashes.map(h => h.toString()) });
492
612
  }
@@ -497,27 +617,29 @@ export class TxPoolV2Impl {
497
617
  // Step 1: Find mined txs at or before finalized block
498
618
  const minedTxsToFinalize = this.#indices.findTxsMinedAtOrBefore(blockNumber);
499
619
 
500
- // Step 2: Collect mined txs for archiving (before deletion)
501
- const txsToArchive: Tx[] = [];
502
- if (this.#archive.isEnabled()) {
503
- for (const txHashStr of minedTxsToFinalize) {
504
- const buffer = await this.#txsDB.getAsync(txHashStr);
505
- if (buffer) {
506
- txsToArchive.push(Tx.fromBuffer(buffer));
620
+ await this.#store.transactionAsync(async () => {
621
+ // Step 2: Collect mined txs for archiving (before deletion)
622
+ const txsToArchive: Tx[] = [];
623
+ if (this.#archive.isEnabled()) {
624
+ for (const txHashStr of minedTxsToFinalize) {
625
+ const buffer = await this.#txsDB.getAsync(txHashStr);
626
+ if (buffer) {
627
+ txsToArchive.push(Tx.fromBuffer(buffer));
628
+ }
507
629
  }
508
630
  }
509
- }
510
631
 
511
- // Step 3: Delete mined txs from active pool
512
- await this.#deleteTxsBatch(minedTxsToFinalize);
632
+ // Step 3: Delete mined txs from active pool
633
+ await this.#deleteTxsBatch(minedTxsToFinalize);
513
634
 
514
- // Step 4: Finalize soft-deleted txs
515
- await this.#deletedPool.finalizeBlock(blockNumber);
635
+ // Step 4: Finalize soft-deleted txs
636
+ await this.#deletedPool.finalizeBlock(blockNumber);
516
637
 
517
- // Step 5: Archive mined txs
518
- if (txsToArchive.length > 0) {
519
- await this.#archive.archiveTxs(txsToArchive);
520
- }
638
+ // Step 5: Archive mined txs
639
+ if (txsToArchive.length > 0) {
640
+ await this.#archive.archiveTxs(txsToArchive);
641
+ }
642
+ });
521
643
 
522
644
  if (minedTxsToFinalize.length > 0) {
523
645
  this.#log.info(`Finalized ${minedTxsToFinalize.length} mined txs from blocks up to ${blockNumber}`, {
@@ -637,8 +759,17 @@ export class TxPoolV2Impl {
637
759
 
638
760
  // === Metrics ===
639
761
 
640
- countTxs(): { pending: number; protected: number; mined: number } {
641
- return this.#indices.countTxs();
762
+ countTxs(): {
763
+ pending: number;
764
+ protected: number;
765
+ mined: number;
766
+ softDeleted: number;
767
+ totalMetadataBytes: number;
768
+ } {
769
+ return {
770
+ ...this.#indices.countTxs(),
771
+ softDeleted: this.#deletedPool.getSoftDeletedCount(),
772
+ };
642
773
  }
643
774
 
644
775
  // ============================================================================
@@ -653,9 +784,10 @@ export class TxPoolV2Impl {
653
784
  tx: Tx,
654
785
  state: 'pending' | { protected: SlotNumber } | { mined: L2BlockId },
655
786
  opts: { source?: string } = {},
787
+ precomputedMeta?: TxMetaData,
656
788
  ): Promise<TxMetaData> {
657
789
  const txHashStr = tx.getTxHash().toString();
658
- const meta = await buildTxMetaData(tx);
790
+ const meta = precomputedMeta ?? (await buildTxMetaData(tx));
659
791
  meta.receivedAt = this.#dateProvider.now();
660
792
 
661
793
  await this.#txsDB.set(txHashStr, tx.toBuffer());
@@ -672,9 +804,11 @@ export class TxPoolV2Impl {
672
804
  }
673
805
 
674
806
  const stateStr = typeof state === 'string' ? state : Object.keys(state)[0];
675
- this.#log.verbose(`Added ${stateStr} tx ${txHashStr}`, {
807
+ this.#log.debug(`Added tx ${txHashStr} as ${stateStr}`, {
676
808
  eventName: 'tx-added-to-pool',
809
+ txHash: txHashStr,
677
810
  state: stateStr,
811
+ source: opts.source,
678
812
  });
679
813
 
680
814
  return meta;
@@ -702,6 +836,29 @@ export class TxPoolV2Impl {
702
836
  }
703
837
  }
704
838
 
839
+ /** Evicts transactions: records eviction metric with reason, caches hashes, then deletes. */
840
+ async #evictTxs(txHashes: string[], reason: string): Promise<void> {
841
+ if (txHashes.length === 0) {
842
+ return;
843
+ }
844
+ this.#instrumentation.recordEvictions(txHashes.length, reason);
845
+ for (const txHashStr of txHashes) {
846
+ this.#log.debug(`Evicting tx ${txHashStr}`, { txHash: txHashStr, reason });
847
+ this.#addToEvictedCache(txHashStr);
848
+ }
849
+ await this.#deleteTxsBatch(txHashes);
850
+ }
851
+
852
+ /** Adds a tx hash to the bounded evicted cache, evicting the oldest entry if at capacity. */
853
+ #addToEvictedCache(txHashStr: string): void {
854
+ if (this.#evictedTxHashes.size >= this.#config.evictedTxCacheSize) {
855
+ // FIFO eviction: remove the first (oldest) entry
856
+ const oldest = this.#evictedTxHashes.values().next().value!;
857
+ this.#evictedTxHashes.delete(oldest);
858
+ }
859
+ this.#evictedTxHashes.add(txHashStr);
860
+ }
861
+
705
862
  // ============================================================================
706
863
  // PRIVATE HELPERS - Validation & Conflict Resolution
707
864
  // ============================================================================
@@ -857,7 +1014,9 @@ export class TxPoolV2Impl {
857
1014
  if (preAddResult.shouldIgnore) {
858
1015
  // Transaction rejected - mark for deletion from DB
859
1016
  rejected.push(meta.txHash);
860
- this.#log.debug(`Rejected tx ${meta.txHash} during rebuild: ${preAddResult.reason}`);
1017
+ this.#log.debug(
1018
+ `Rejected tx ${meta.txHash} during rebuild: ${preAddResult.reason?.message ?? 'unknown reason'}`,
1019
+ );
861
1020
  continue;
862
1021
  }
863
1022
 
@@ -893,7 +1052,7 @@ export class TxPoolV2Impl {
893
1052
  getFeePayerPendingTxs: (feePayer: string) => this.#indices.getFeePayerPendingTxs(feePayer),
894
1053
  getPendingTxCount: () => this.#indices.getPendingTxCount(),
895
1054
  getLowestPriorityPending: (limit: number) => this.#indices.getLowestPriorityPending(limit),
896
- deleteTxs: (txHashes: string[]) => this.#deleteTxsBatch(txHashes),
1055
+ deleteTxs: (txHashes: string[], reason?: string) => this.#evictTxs(txHashes, reason ?? 'unknown'),
897
1056
  };
898
1057
  }
899
1058