@aztec/p2p 0.0.1-commit.b9865e97 → 0.0.1-commit.be03c316

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 (153) hide show
  1. package/README.md +7 -6
  2. package/dest/bootstrap/bootstrap.d.ts +1 -1
  3. package/dest/bootstrap/bootstrap.d.ts.map +1 -1
  4. package/dest/bootstrap/bootstrap.js +20 -12
  5. package/dest/client/factory.d.ts +2 -2
  6. package/dest/client/factory.d.ts.map +1 -1
  7. package/dest/client/factory.js +10 -5
  8. package/dest/client/interface.d.ts +36 -8
  9. package/dest/client/interface.d.ts.map +1 -1
  10. package/dest/client/p2p_client.d.ts +23 -12
  11. package/dest/client/p2p_client.d.ts.map +1 -1
  12. package/dest/client/p2p_client.js +38 -35
  13. package/dest/config.d.ts +19 -13
  14. package/dest/config.d.ts.map +1 -1
  15. package/dest/config.js +27 -26
  16. package/dest/errors/reqresp.error.d.ts +13 -1
  17. package/dest/errors/reqresp.error.d.ts.map +1 -1
  18. package/dest/errors/reqresp.error.js +14 -0
  19. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +4 -2
  20. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  21. package/dest/mem_pools/attestation_pool/attestation_pool.js +9 -2
  22. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +1 -1
  23. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
  24. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +43 -0
  25. package/dest/mem_pools/instrumentation.d.ts +9 -4
  26. package/dest/mem_pools/instrumentation.d.ts.map +1 -1
  27. package/dest/mem_pools/instrumentation.js +11 -8
  28. package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts +3 -3
  29. package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts.map +1 -1
  30. package/dest/mem_pools/tx_pool_v2/deleted_pool.js +6 -6
  31. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +1 -1
  32. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +30 -5
  33. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  34. package/dest/mem_pools/tx_pool_v2/interfaces.js +2 -1
  35. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +15 -1
  36. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
  37. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +31 -0
  38. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +9 -3
  39. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  40. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +19 -8
  41. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +17 -4
  42. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  43. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +133 -34
  44. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts +6 -5
  45. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts.map +1 -1
  46. package/dest/msg_validators/attestation_validator/attestation_validator.js +24 -26
  47. package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.d.ts +4 -4
  48. package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.d.ts.map +1 -1
  49. package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.js +2 -2
  50. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts +4 -3
  51. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts.map +1 -1
  52. package/dest/msg_validators/proposal_validator/block_proposal_validator.js +2 -2
  53. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts +4 -3
  54. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts.map +1 -1
  55. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.js +8 -2
  56. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +19 -5
  57. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  58. package/dest/msg_validators/proposal_validator/proposal_validator.js +66 -30
  59. package/dest/msg_validators/tx_validator/factory.d.ts +9 -11
  60. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  61. package/dest/msg_validators/tx_validator/factory.js +15 -6
  62. package/dest/msg_validators/tx_validator/gas_validator.d.ts +12 -7
  63. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  64. package/dest/msg_validators/tx_validator/gas_validator.js +15 -15
  65. package/dest/msg_validators/tx_validator/phases_validator.js +2 -2
  66. package/dest/services/discv5/discV5_service.d.ts +12 -2
  67. package/dest/services/discv5/discV5_service.d.ts.map +1 -1
  68. package/dest/services/discv5/discV5_service.js +84 -15
  69. package/dest/services/discv5/persisted_enr_store.d.ts +38 -0
  70. package/dest/services/discv5/persisted_enr_store.d.ts.map +1 -0
  71. package/dest/services/discv5/persisted_enr_store.js +145 -0
  72. package/dest/services/dummy_service.d.ts +6 -2
  73. package/dest/services/dummy_service.d.ts.map +1 -1
  74. package/dest/services/dummy_service.js +3 -0
  75. package/dest/services/encoding.d.ts +19 -6
  76. package/dest/services/encoding.d.ts.map +1 -1
  77. package/dest/services/encoding.js +25 -15
  78. package/dest/services/gossipsub/topic_score_params.d.ts +6 -23
  79. package/dest/services/gossipsub/topic_score_params.d.ts.map +1 -1
  80. package/dest/services/gossipsub/topic_score_params.js +4 -31
  81. package/dest/services/libp2p/libp2p_service.d.ts +11 -3
  82. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  83. package/dest/services/libp2p/libp2p_service.js +135 -53
  84. package/dest/services/peer-manager/peer_manager.d.ts +15 -2
  85. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  86. package/dest/services/peer-manager/peer_manager.js +46 -18
  87. package/dest/services/peer-manager/peer_scoring.d.ts +28 -1
  88. package/dest/services/peer-manager/peer_scoring.d.ts.map +1 -1
  89. package/dest/services/peer-manager/peer_scoring.js +70 -3
  90. package/dest/services/reqresp/config.d.ts +13 -1
  91. package/dest/services/reqresp/config.d.ts.map +1 -1
  92. package/dest/services/reqresp/config.js +11 -0
  93. package/dest/services/reqresp/rate-limiter/rate_limits.js +3 -3
  94. package/dest/services/reqresp/reqresp.d.ts +1 -1
  95. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  96. package/dest/services/reqresp/reqresp.js +34 -3
  97. package/dest/services/service.d.ts +17 -1
  98. package/dest/services/service.d.ts.map +1 -1
  99. package/dest/services/tx_collection/tx_source.d.ts +1 -1
  100. package/dest/services/tx_collection/tx_source.d.ts.map +1 -1
  101. package/dest/services/tx_collection/tx_source.js +4 -1
  102. package/dest/test-helpers/testbench-utils.d.ts +4 -1
  103. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  104. package/dest/test-helpers/testbench-utils.js +9 -0
  105. package/dest/testbench/p2p_client_testbench_worker.d.ts +1 -1
  106. package/dest/testbench/p2p_client_testbench_worker.d.ts.map +1 -1
  107. package/dest/testbench/p2p_client_testbench_worker.js +19 -2
  108. package/package.json +16 -17
  109. package/src/bootstrap/bootstrap.ts +15 -9
  110. package/src/client/factory.ts +9 -5
  111. package/src/client/interface.ts +29 -6
  112. package/src/client/p2p_client.ts +46 -48
  113. package/src/config.ts +50 -38
  114. package/src/errors/reqresp.error.ts +19 -0
  115. package/src/mem_pools/attestation_pool/attestation_pool.ts +12 -2
  116. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +40 -1
  117. package/src/mem_pools/instrumentation.ts +13 -11
  118. package/src/mem_pools/tx_pool_v2/deleted_pool.ts +7 -7
  119. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +1 -1
  120. package/src/mem_pools/tx_pool_v2/interfaces.ts +28 -4
  121. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +35 -0
  122. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +22 -8
  123. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +158 -33
  124. package/src/msg_validators/attestation_validator/attestation_validator.ts +26 -26
  125. package/src/msg_validators/attestation_validator/fisherman_attestation_validator.ts +4 -3
  126. package/src/msg_validators/proposal_validator/block_proposal_validator.ts +4 -2
  127. package/src/msg_validators/proposal_validator/checkpoint_proposal_validator.ts +10 -2
  128. package/src/msg_validators/proposal_validator/proposal_validator.ts +69 -30
  129. package/src/msg_validators/tx_validator/README.md +2 -0
  130. package/src/msg_validators/tx_validator/factory.ts +19 -13
  131. package/src/msg_validators/tx_validator/gas_validator.ts +16 -20
  132. package/src/msg_validators/tx_validator/phases_validator.ts +2 -2
  133. package/src/services/discv5/discV5_service.ts +88 -9
  134. package/src/services/discv5/persisted_enr_store.ts +158 -0
  135. package/src/services/dummy_service.ts +6 -0
  136. package/src/services/encoding.ts +28 -20
  137. package/src/services/gossipsub/README.md +33 -11
  138. package/src/services/gossipsub/topic_score_params.ts +9 -50
  139. package/src/services/libp2p/libp2p_service.ts +159 -56
  140. package/src/services/peer-manager/peer_manager.ts +48 -17
  141. package/src/services/peer-manager/peer_scoring.ts +75 -3
  142. package/src/services/reqresp/README.md +2 -23
  143. package/src/services/reqresp/config.ts +17 -0
  144. package/src/services/reqresp/rate-limiter/rate_limits.ts +3 -3
  145. package/src/services/reqresp/reqresp.ts +42 -2
  146. package/src/services/service.ts +19 -0
  147. package/src/services/tx_collection/tx_source.ts +2 -1
  148. package/src/test-helpers/testbench-utils.ts +12 -0
  149. package/src/testbench/p2p_client_testbench_worker.ts +22 -2
  150. package/dest/msg_validators/clock_tolerance.d.ts +0 -32
  151. package/dest/msg_validators/clock_tolerance.d.ts.map +0 -1
  152. package/dest/msg_validators/clock_tolerance.js +0 -90
  153. package/src/msg_validators/clock_tolerance.ts +0 -121
@@ -51,10 +51,18 @@ const FINALIZE_BLOCK_CHUNK_SIZE = 100;
51
51
  /**
52
52
  * Callbacks for the implementation to notify the outer class about events and metrics.
53
53
  */
54
+ /** A tx that has just transitioned to mined, with the time it spent in the pool. */
55
+ export interface MinedTxInfo {
56
+ txHash: string;
57
+ /** Wall-clock ms from receipt into the pool to being marked mined. Undefined when the
58
+ * receive time is unknown (e.g. a tx hydrated from the DB on restart, receivedAt === 0). */
59
+ minedDelayMs?: number;
60
+ }
61
+
54
62
  export interface TxPoolV2Callbacks {
55
63
  onTxsAdded: (txs: Tx[], opts: { source?: string }) => void;
56
64
  onTxsRemoved: (txHashes: string[] | bigint[]) => void;
57
- onTxsMined: (txHashes: string[]) => void;
65
+ onTxsMined: (minedTxs: MinedTxInfo[]) => void;
58
66
  }
59
67
 
60
68
  /**
@@ -65,7 +73,10 @@ export interface TxPoolV2Callbacks {
65
73
  export class TxPoolV2Impl {
66
74
  // === Persistence ===
67
75
  #store: AztecAsyncKVStore;
76
+ /** Tx data without its proof. Proofs live in #proofsDB so a tx can be read without loading its proof. */
68
77
  #txsDB: AztecAsyncMap<string, Buffer>;
78
+ /** Tx proofs, keyed by tx hash. Written and deleted in lockstep with #txsDB. */
79
+ #proofsDB: AztecAsyncMap<string, Buffer>;
69
80
 
70
81
  // === Dependencies ===
71
82
  #l2BlockSource: L2BlockSource;
@@ -99,6 +110,7 @@ export class TxPoolV2Impl {
99
110
  ) {
100
111
  this.#store = store;
101
112
  this.#txsDB = store.openMap('txs');
113
+ this.#proofsDB = store.openMap('tx_proofs');
102
114
 
103
115
  this.#l2BlockSource = deps.l2BlockSource;
104
116
  this.#worldStateSynchronizer = deps.worldStateSynchronizer;
@@ -108,7 +120,7 @@ export class TxPoolV2Impl {
108
120
  this.#config = { ...DEFAULT_TX_POOL_V2_CONFIG, ...config };
109
121
  this.#evictedTxHashes = FifoSet.withLimit<string>(this.#config.evictedTxCacheSize);
110
122
  this.#archive = new TxArchive(archiveStore, this.#config.archivedTxLimit, log);
111
- this.#deletedPool = new DeletedPool(store, this.#txsDB, log);
123
+ this.#deletedPool = new DeletedPool(store, txHashStr => this.#deleteTxData(txHashStr), log);
112
124
  this.#dateProvider = dateProvider;
113
125
  this.#instrumentation = new TxPoolV2Instrumentation(telemetry, () => this.#indices.getTotalMetadataBytes());
114
126
  this.#log = log;
@@ -216,7 +228,7 @@ export class TxPoolV2Impl {
216
228
  }
217
229
  await this.#store.transactionAsync(async () => {
218
230
  for (const txHashStr of toDelete) {
219
- await this.#txsDB.delete(txHashStr);
231
+ await this.#deleteTxData(txHashStr);
220
232
  }
221
233
  });
222
234
  this.#log.info(`Deleted ${toDelete.length} invalid/rejected transactions on startup`, { txHashes: toDelete });
@@ -462,10 +474,11 @@ export class TxPoolV2Impl {
462
474
  // Update protection for existing tx
463
475
  this.#indices.updateProtection(txHashStr, slotNumber);
464
476
  } else if (this.#deletedPool.isSoftDeleted(txHashStr)) {
465
- // Resurrect soft-deleted tx as protected
477
+ // Resurrect soft-deleted tx as protected. Load the proof too: #addTx rewrites both entries, so re-adding
478
+ // a proofless tx would wipe the stored proof.
466
479
  const buffer = await this.#txsDB.getAsync(txHashStr);
467
480
  if (buffer) {
468
- const tx = Tx.fromBuffer(buffer);
481
+ const tx = await this.#loadTxWithProof(txHashStr, buffer);
469
482
  await this.#addTx(tx, { protected: slotNumber });
470
483
  softDeletedHits++;
471
484
  } else {
@@ -555,7 +568,15 @@ export class TxPoolV2Impl {
555
568
  });
556
569
 
557
570
  if (found.length > 0) {
558
- this.#callbacks.onTxsMined(found.map(m => m.txHash));
571
+ // receivedAt is 0 for txs hydrated from the DB on restart (true receive time lost) — leave
572
+ // their delay undefined so the metric isn't polluted by epoch-sized values.
573
+ const now = this.#dateProvider.now();
574
+ this.#callbacks.onTxsMined(
575
+ found.map(m => ({
576
+ txHash: m.txHash,
577
+ minedDelayMs: m.receivedAt > 0 ? now - m.receivedAt : undefined,
578
+ })),
579
+ );
559
580
  }
560
581
 
561
582
  this.#log.info(`Marked ${found.length} txs as mined in block ${blockId.number}`);
@@ -580,29 +601,62 @@ export class TxPoolV2Impl {
580
601
  }
581
602
 
582
603
  this.#log.info(`Preparing for slot ${slotNumber}: unprotecting ${txsToRestore.length} txs`);
604
+ await this.#restoreUnprotectedToPending(txsToRestore, 'during prepareForSlot');
605
+ });
606
+ }
583
607
 
584
- // Step 4: Validate for pending pool
585
- const { valid, invalid } = await this.#revalidateMetadata(txsToRestore, 'during prepareForSlot');
608
+ async unprotectTxs(txHashes: TxHash[], slotNumber: SlotNumber): Promise<void> {
609
+ const hashStrs = txHashes.map(h => h.toString());
586
610
 
587
- // Step 5: Resolve nullifier conflicts and add winners to pending indices
588
- const { added, toEvict } = this.#applyNullifierConflictResolution(valid);
611
+ await this.#store.transactionAsync(async () => {
612
+ // Only release entries still recorded at this exact slot. A tx that another, still-live proposal
613
+ // raised to a higher slot via updateProtection must keep that protection. Mined txs have no
614
+ // protection entry and so are never matched here.
615
+ const matching = this.#indices.findProtectedTxsAtSlot(hashStrs, slotNumber);
616
+ if (matching.length === 0) {
617
+ this.#log.debug(`Unprotecting txs for slot ${slotNumber}: no matching protections to release`);
618
+ return;
619
+ }
589
620
 
590
- // Step 6: Delete invalid txs and evict conflict losers
591
- await this.#deleteTxsBatch(invalid);
592
- await this.#evictTxs(toEvict, 'NullifierConflict');
621
+ this.#indices.clearProtection(matching);
593
622
 
594
- // Step 7: Run eviction rules (enforce pool size limit)
595
- if (added.length > 0) {
596
- const feePayers = added.map(meta => meta.feePayer);
597
- const uniqueFeePayers = new Set<string>(feePayers);
598
- await this.#evictionManager.evictAfterNewTxs(
599
- added.map(m => m.txHash),
600
- [...uniqueFeePayers],
601
- );
623
+ const txsToRestore = this.#indices.filterRestorable(matching);
624
+ if (txsToRestore.length === 0) {
625
+ return;
602
626
  }
627
+
628
+ this.#log.info(`Unprotecting ${txsToRestore.length} txs from failed proposal at slot ${slotNumber}`);
629
+ await this.#restoreUnprotectedToPending(txsToRestore, 'during unprotectTxs');
603
630
  });
604
631
  }
605
632
 
633
+ /**
634
+ * Returns just-unprotected txs to the pending pool: revalidates them, resolves nullifier
635
+ * conflicts, deletes losers, then enforces the pool size limit. Must run inside a store
636
+ * transaction; callers clear the protection entries before invoking.
637
+ */
638
+ async #restoreUnprotectedToPending(txsToRestore: TxMetaData[], context: string): Promise<void> {
639
+ // Step 1: Validate for pending pool
640
+ const { valid, invalid } = await this.#revalidateMetadata(txsToRestore, context);
641
+
642
+ // Step 2: Resolve nullifier conflicts and add winners to pending indices
643
+ const { added, toEvict } = this.#applyNullifierConflictResolution(valid);
644
+
645
+ // Step 3: Delete invalid txs and evict conflict losers
646
+ await this.#deleteTxsBatch(invalid);
647
+ await this.#evictTxs(toEvict, 'NullifierConflict');
648
+
649
+ // Step 4: Run eviction rules (enforce pool size limit)
650
+ if (added.length > 0) {
651
+ const feePayers = added.map(meta => meta.feePayer);
652
+ const uniqueFeePayers = new Set<string>(feePayers);
653
+ await this.#evictionManager.evictAfterNewTxs(
654
+ added.map(m => m.txHash),
655
+ [...uniqueFeePayers],
656
+ );
657
+ }
658
+ }
659
+
606
660
  async handlePrunedBlocks(latestBlock: L2BlockId, options?: { deleteAllTxs?: boolean }): Promise<void> {
607
661
  // Step 1: Find transactions mined after the prune point
608
662
  const txsToUnmine = this.#indices.findTxsMinedAfter(latestBlock.number);
@@ -674,8 +728,12 @@ export class TxPoolV2Impl {
674
728
  async handleFinalizedBlock(block: BlockHeader): Promise<void> {
675
729
  const blockNumber = block.globalVariables.blockNumber;
676
730
 
677
- // Step 1: Find mined txs at or before finalized block
678
- const minedTxsToFinalize = this.#indices.findTxsMinedAtOrBefore(blockNumber);
731
+ // Hold finalized txs for a configurable margin behind the finalized tip so a prover still
732
+ // proving an epoch with already-finalized blocks isn't starved of its txs. 0 deletes at the finalized tip.
733
+ const cutoffBlock = await this.#finalizationCutoffBlock(block);
734
+
735
+ // Step 1: Find mined txs at or before the cutoff block
736
+ const minedTxsToFinalize = this.#indices.findTxsMinedAtOrBefore(cutoffBlock);
679
737
 
680
738
  // Step 2: Archive in chunks if archiving is enabled. Hydrating an entire epoch's worth of
681
739
  // mined txs at once would OOM under load. When archiving is disabled there is no need to hydrate the txs at all.
@@ -699,32 +757,84 @@ export class TxPoolV2Impl {
699
757
  // transaction. Only tx hashes are touched here, so memory is bounded and atomicity is preserved.
700
758
  await this.#store.transactionAsync(async () => {
701
759
  await this.#deleteTxsBatch(minedTxsToFinalize);
702
- await this.#deletedPool.finalizeBlock(blockNumber);
760
+ await this.#deletedPool.finalizeBlock(cutoffBlock);
703
761
  });
704
762
 
705
763
  if (minedTxsToFinalize.length > 0) {
706
- this.#log.info(`Finalized ${minedTxsToFinalize.length} mined txs from blocks up to ${blockNumber}`, {
764
+ this.#log.info(`Finalized ${minedTxsToFinalize.length} mined txs from blocks up to ${cutoffBlock}`, {
707
765
  txHashes: minedTxsToFinalize,
766
+ finalizedBlockNumber: blockNumber,
767
+ cutoffBlock,
708
768
  });
709
769
  }
710
770
  }
711
771
 
772
+ /**
773
+ * Resolves the block up to which finalized txs may be deleted, given `keepFinalizedTxsForSlots`.
774
+ * With a margin of 0 this is just the finalized block. Otherwise we keep txs whose block is within
775
+ * the margin (in slots) of the finalized tip: take the target slot `finalizedSlot - margin`, find the
776
+ * latest checkpoint at or before it with a single reverse range query (`limit: 1`), and use that
777
+ * checkpoint's last block as the cutoff. This rounds to a checkpoint boundary, so it can retain slightly
778
+ * more than the configured margin but never less, and never deletes past the finalized block.
779
+ */
780
+ async #finalizationCutoffBlock(block: BlockHeader): Promise<BlockNumber> {
781
+ const keepSlots = this.#config.keepFinalizedTxsForSlots;
782
+ if (keepSlots <= 0) {
783
+ return block.globalVariables.blockNumber;
784
+ }
785
+
786
+ // The goal here really is to ensure we keep transactions long enough for them to be proven.
787
+ // So we could be smart and calculate the number of slots until the end of the proof submission window.
788
+ // Our approach here is a lot simpler however and we just keep transactions for the configured number
789
+ // of slots past L1 finalisation. This means we may keep transactions in the mined pool for longer
790
+ // than strictly necessary.
791
+ const targetSlot = block.getSlot() - keepSlots;
792
+ if (targetSlot < 0) {
793
+ // The margin reaches past genesis: keep everything.
794
+ return BlockNumber(0);
795
+ }
796
+
797
+ const [checkpoint] = await this.#l2BlockSource.getCheckpointsData({
798
+ fromSlot: SlotNumber(targetSlot),
799
+ limit: 1,
800
+ reverse: true,
801
+ });
802
+ // No checkpoint at or before the target slot (e.g. early chain): keep everything.
803
+ return checkpoint ? BlockNumber(checkpoint.startBlock + checkpoint.blockCount - 1) : BlockNumber(0);
804
+ }
805
+
712
806
  // === Query Methods ===
713
807
 
714
- async getTxByHash(txHash: TxHash): Promise<Tx | undefined> {
715
- const buffer = await this.#txsDB.getAsync(txHash.toString());
716
- return buffer ? Tx.fromBuffer(buffer) : undefined;
808
+ async getTxByHash(txHash: TxHash, opts: { includeProof?: boolean } = {}): Promise<Tx | undefined> {
809
+ const txHashStr = txHash.toString();
810
+ const buffer = await this.#txsDB.getAsync(txHashStr);
811
+ if (!buffer) {
812
+ return undefined;
813
+ }
814
+ return opts.includeProof === false ? Tx.fromBuffer(buffer) : this.#loadTxWithProof(txHashStr, buffer);
717
815
  }
718
816
 
719
- async getTxsByHash(txHashes: TxHash[]): Promise<(Tx | undefined)[]> {
817
+ async getTxsByHash(txHashes: TxHash[], opts: { includeProof?: boolean } = {}): Promise<(Tx | undefined)[]> {
720
818
  const results: (Tx | undefined)[] = [];
721
819
  for (const h of txHashes) {
722
- const buffer = await this.#txsDB.getAsync(h.toString());
723
- results.push(buffer ? Tx.fromBuffer(buffer) : undefined);
820
+ results.push(await this.getTxByHash(h, opts));
724
821
  }
725
822
  return results;
726
823
  }
727
824
 
825
+ /** Deserializes a tx blob loaded from #txsDB together with its separately-stored proof. */
826
+ async #loadTxWithProof(txHashStr: string, buffer: Buffer): Promise<Tx> {
827
+ const proofBuffer = await this.#proofsDB.getAsync(txHashStr);
828
+ if (!proofBuffer) {
829
+ // #addTx always writes a proof entry (an empty proof serializes to a 4-byte sentinel), so a missing entry
830
+ // means the blob/proof invariant is broken. Return the proofless tx but flag it, since a consumer that needs
831
+ // the proof (eg serving peers) would otherwise fail far away from the root cause.
832
+ this.#log.warn(`Missing stored proof for tx ${txHashStr}`, { txHash: txHashStr });
833
+ return Tx.fromBuffer(buffer);
834
+ }
835
+ return Tx.fromBuffers(buffer, proofBuffer);
836
+ }
837
+
728
838
  hasTxs(txHashes: TxHash[]): boolean[] {
729
839
  return txHashes.map(h => {
730
840
  const hashStr = h.toString();
@@ -760,6 +870,11 @@ export class TxPoolV2Impl {
760
870
  return this.#indices.getPendingTxCount();
761
871
  }
762
872
 
873
+ hasEligiblePendingTxs(minCount: number): boolean {
874
+ const maxReceivedAt = this.#dateProvider.now() - this.#config.minTxPoolAgeMs;
875
+ return this.#indices.hasEligiblePendingTxs(maxReceivedAt, minCount);
876
+ }
877
+
763
878
  getMinedTxHashes(): [TxHash, L2BlockId][] {
764
879
  return this.#indices.getMinedTxs().map(([hash, blockId]) => [TxHash.fromString(hash), blockId]);
765
880
  }
@@ -803,6 +918,9 @@ export class TxPoolV2Impl {
803
918
  if (config.minTxPoolAgeMs !== undefined) {
804
919
  this.#config.minTxPoolAgeMs = config.minTxPoolAgeMs;
805
920
  }
921
+ if (config.keepFinalizedTxsForSlots !== undefined) {
922
+ this.#config.keepFinalizedTxsForSlots = config.keepFinalizedTxsForSlots;
923
+ }
806
924
  // Update eviction rules with new config
807
925
  this.#evictionManager.updateConfig(config);
808
926
  }
@@ -851,7 +969,8 @@ export class TxPoolV2Impl {
851
969
  const meta = precomputedMeta ?? (await buildTxMetaData(tx));
852
970
  meta.receivedAt = this.#dateProvider.now();
853
971
 
854
- await this.#txsDB.set(txHashStr, tx.toBuffer());
972
+ await this.#txsDB.set(txHashStr, tx.withoutProof().toBuffer());
973
+ await this.#proofsDB.set(txHashStr, tx.chonkProof.toBuffer());
855
974
  await this.#deletedPool.clearSoftDeleted(txHashStr);
856
975
  this.#callbacks.onTxsAdded([tx], opts);
857
976
 
@@ -890,6 +1009,12 @@ export class TxPoolV2Impl {
890
1009
  await this.#deletedPool.deleteTx(txHashStr);
891
1010
  }
892
1011
 
1012
+ /** Hard-deletes a tx's stored data (tx blob and proof) from the DB. */
1013
+ async #deleteTxData(txHashStr: string): Promise<void> {
1014
+ await this.#txsDB.delete(txHashStr);
1015
+ await this.#proofsDB.delete(txHashStr);
1016
+ }
1017
+
893
1018
  /** Deletes a batch of transactions, emitting callbacks individually for each. */
894
1019
  async #deleteTxsBatch(txHashes: string[]): Promise<void> {
895
1020
  for (const txHashStr of txHashes) {
@@ -1078,7 +1203,7 @@ export class TxPoolV2Impl {
1078
1203
  const publicStateSource = new DatabasePublicStateSource(db);
1079
1204
  const balance = await publicStateSource.storageRead(
1080
1205
  ProtocolContractAddress.FeeJuice,
1081
- await computeFeePayerBalanceStorageSlot(AztecAddress.fromString(feePayer)),
1206
+ await computeFeePayerBalanceStorageSlot(AztecAddress.fromStringUnsafe(feePayer)),
1082
1207
  );
1083
1208
  return balance.toBigInt();
1084
1209
  },
@@ -9,29 +9,27 @@ import {
9
9
  type ValidationResult,
10
10
  hasValidSignatureContext,
11
11
  } from '@aztec/stdlib/p2p';
12
-
13
- import { PipeliningWindow, isWithinClockTolerance } from '../clock_tolerance.js';
12
+ import type { ConsensusTimetable } from '@aztec/stdlib/timetable';
14
13
 
15
14
  export class CheckpointAttestationValidator implements P2PValidator<CheckpointAttestation> {
16
15
  protected epochCache: EpochCacheInterface;
17
16
  protected logger: Logger;
18
- private readonly pipeliningWindow: PipeliningWindow;
17
+ private readonly timetable: ConsensusTimetable;
19
18
  protected readonly signatureContext: CoordinationSignatureContext;
19
+ private readonly clockDisparityMs: number;
20
20
 
21
21
  constructor(
22
22
  epochCache: EpochCacheInterface,
23
+ timetable: ConsensusTimetable,
23
24
  opts: {
24
- l1PublishingTime?: number;
25
- p2pPropagationTime?: number;
26
25
  signatureContext: CoordinationSignatureContext;
26
+ clockDisparityMs: number;
27
27
  },
28
28
  ) {
29
29
  this.epochCache = epochCache;
30
- this.pipeliningWindow = new PipeliningWindow(epochCache, {
31
- l1PublishingTime: opts.l1PublishingTime,
32
- p2pPropagationTime: opts.p2pPropagationTime,
33
- });
30
+ this.timetable = timetable;
34
31
  this.signatureContext = opts.signatureContext;
32
+ this.clockDisparityMs = opts.clockDisparityMs;
35
33
  this.logger = createLogger('p2p:checkpoint-attestation-validator');
36
34
  }
37
35
 
@@ -50,23 +48,25 @@ export class CheckpointAttestationValidator implements P2PValidator<CheckpointAt
50
48
  return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
51
49
  }
52
50
 
53
- // Use target slots since proposals target pipeline slots (slot + 1 when pipelining).
54
- const { targetSlot, nextSlot } = this.epochCache.getTargetAndNextSlot();
55
-
56
- if (slotNumber !== targetSlot && slotNumber !== nextSlot) {
57
- // When pipelining, accept attestations for the current slot (built in the previous slot)
58
- // until the target slot reaches its L1 publish cutoff.
59
- if (this.pipeliningWindow.acceptsAttestation(slotNumber)) {
60
- // Fall through to remaining validation (signature, committee, etc.)
61
- } else if (!isWithinClockTolerance(slotNumber, targetSlot, this.epochCache)) {
62
- this.logger.warn(
63
- `Checkpoint attestation slot ${slotNumber} is not current (${targetSlot}) or next (${nextSlot}) slot`,
64
- );
65
- return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError };
66
- } else {
67
- this.logger.debug(`Ignoring checkpoint attestation for previous slot ${slotNumber} within clock tolerance`);
68
- return { result: 'ignore' };
69
- }
51
+ // Accept attestations whose explicit per-slot receive window contains the current wall-clock time.
52
+ // The window spans the build-frame start (attestation_receive_start) to the attestation deadline
53
+ // (target_slot_start + S - 2E), widened by the configured clock-disparity tolerance on both ends, so
54
+ // it covers the build slot, the target slot, and clock-disparity grace. This is the sole arrival
55
+ // gate; attestations are otherwise attributed by content, not by current/next slot.
56
+ const startSeconds = this.timetable.getAttestationReceiveStart(slotNumber);
57
+ const deadlineSeconds = this.timetable.getAttestationDeadline(slotNumber);
58
+ const nowMs = Number(this.epochCache.getEpochAndSlotNow().nowMs);
59
+ if (
60
+ nowMs < startSeconds * 1000 - this.clockDisparityMs ||
61
+ nowMs > deadlineSeconds * 1000 + this.clockDisparityMs
62
+ ) {
63
+ this.logger.warn(`Checkpoint attestation slot ${slotNumber} is outside its receive window`, {
64
+ slotNumber,
65
+ nowMs,
66
+ windowStartSeconds: startSeconds,
67
+ windowDeadlineSeconds: deadlineSeconds,
68
+ });
69
+ return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError };
70
70
  }
71
71
 
72
72
  // Verify the signature is valid
@@ -5,6 +5,7 @@ import {
5
5
  PeerErrorSeverity,
6
6
  type ValidationResult,
7
7
  } from '@aztec/stdlib/p2p';
8
+ import type { ConsensusTimetable } from '@aztec/stdlib/timetable';
8
9
  import { Attributes, Metrics, type TelemetryClient, createUpDownCounterWithDefault } from '@aztec/telemetry-client';
9
10
 
10
11
  import type { AttestationPoolApi } from '../../mem_pools/attestation_pool/attestation_pool.js';
@@ -23,15 +24,15 @@ export class FishermanAttestationValidator extends CheckpointAttestationValidato
23
24
 
24
25
  constructor(
25
26
  epochCache: EpochCacheInterface,
27
+ timetable: ConsensusTimetable,
26
28
  private attestationPool: AttestationPoolApi,
27
29
  telemetryClient: TelemetryClient,
28
30
  opts: {
29
- l1PublishingTime?: number;
30
- p2pPropagationTime?: number;
31
31
  signatureContext: CoordinationSignatureContext;
32
+ clockDisparityMs: number;
32
33
  },
33
34
  ) {
34
- super(epochCache, opts);
35
+ super(epochCache, timetable, opts);
35
36
  this.logger = this.logger.createChild('[FISHERMAN]');
36
37
 
37
38
  const meter = telemetryClient.getMeter('FishermanAttestationValidator');
@@ -1,5 +1,6 @@
1
1
  import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
2
  import type { BlockProposal, CoordinationSignatureContext, P2PValidator, ValidationResult } from '@aztec/stdlib/p2p';
3
+ import type { ConsensusTimetable } from '@aztec/stdlib/timetable';
3
4
 
4
5
  import { ProposalValidator } from '../proposal_validator/proposal_validator.js';
5
6
 
@@ -8,16 +9,17 @@ export class BlockProposalValidator implements P2PValidator<BlockProposal> {
8
9
 
9
10
  constructor(
10
11
  epochCache: EpochCacheInterface,
12
+ timetable: ConsensusTimetable,
11
13
  opts: {
12
14
  txsPermitted: boolean;
13
15
  maxTxsPerBlock?: number;
14
16
  maxBlocksPerCheckpoint?: number;
15
- p2pPropagationTime?: number;
16
17
  skipSlotValidation?: boolean;
17
18
  signatureContext: CoordinationSignatureContext;
19
+ clockDisparityMs: number;
18
20
  },
19
21
  ) {
20
- this.proposalValidator = new ProposalValidator(epochCache, opts, 'p2p:block_proposal_validator');
22
+ this.proposalValidator = new ProposalValidator(epochCache, timetable, opts, 'p2p:block_proposal_validator');
21
23
  }
22
24
 
23
25
  async validate(proposal: BlockProposal): Promise<ValidationResult> {
@@ -5,6 +5,7 @@ import type {
5
5
  P2PValidator,
6
6
  ValidationResult,
7
7
  } from '@aztec/stdlib/p2p';
8
+ import type { ConsensusTimetable } from '@aztec/stdlib/timetable';
8
9
 
9
10
  import { ProposalValidator } from '../proposal_validator/proposal_validator.js';
10
11
 
@@ -13,16 +14,17 @@ export class CheckpointProposalValidator implements P2PValidator<CheckpointPropo
13
14
 
14
15
  constructor(
15
16
  epochCache: EpochCacheInterface,
17
+ timetable: ConsensusTimetable,
16
18
  opts: {
17
19
  txsPermitted: boolean;
18
20
  maxTxsPerBlock?: number;
19
21
  maxBlocksPerCheckpoint?: number;
20
- p2pPropagationTime?: number;
21
22
  skipSlotValidation?: boolean;
22
23
  signatureContext: CoordinationSignatureContext;
24
+ clockDisparityMs: number;
23
25
  },
24
26
  ) {
25
- this.proposalValidator = new ProposalValidator(epochCache, opts, 'p2p:checkpoint_proposal_validator');
27
+ this.proposalValidator = new ProposalValidator(epochCache, timetable, opts, 'p2p:checkpoint_proposal_validator');
26
28
  }
27
29
 
28
30
  async validate(proposal: CheckpointProposal): Promise<ValidationResult> {
@@ -33,6 +35,12 @@ export class CheckpointProposalValidator implements P2PValidator<CheckpointPropo
33
35
 
34
36
  const blockProposal = proposal.getBlockProposal();
35
37
  if (blockProposal) {
38
+ // The terminal block is carried inside the checkpoint proposal rather than gossiped as a
39
+ // standalone block proposal, so apply the per-checkpoint block ceiling to it here too.
40
+ const indexResult = this.proposalValidator.validateBlockIndexWithinCheckpoint(blockProposal);
41
+ if (indexResult.result !== 'accept') {
42
+ return indexResult;
43
+ }
36
44
  return this.proposalValidator.validateTxs(blockProposal);
37
45
  }
38
46