@aztec/p2p 0.0.1-commit.3e3d0c9cd → 0.0.1-commit.3fd054f6

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 (101) hide show
  1. package/dest/client/p2p_client.d.ts +1 -1
  2. package/dest/client/p2p_client.d.ts.map +1 -1
  3. package/dest/client/p2p_client.js +6 -4
  4. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.js +3 -3
  5. package/dest/config.d.ts +7 -1
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +10 -0
  8. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +3 -3
  9. package/dest/mem_pools/attestation_pool/attestation_pool.js +3 -3
  10. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +6 -6
  11. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +4 -4
  12. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts +1 -1
  13. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts.map +1 -1
  14. package/dest/msg_validators/attestation_validator/attestation_validator.js +5 -4
  15. package/dest/msg_validators/clock_tolerance.d.ts +1 -1
  16. package/dest/msg_validators/clock_tolerance.d.ts.map +1 -1
  17. package/dest/msg_validators/clock_tolerance.js +4 -3
  18. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +1 -1
  19. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  20. package/dest/msg_validators/proposal_validator/proposal_validator.js +5 -5
  21. package/dest/msg_validators/tx_validator/contract_instance_validator.d.ts +9 -0
  22. package/dest/msg_validators/tx_validator/contract_instance_validator.d.ts.map +1 -0
  23. package/dest/msg_validators/tx_validator/contract_instance_validator.js +48 -0
  24. package/dest/msg_validators/tx_validator/data_validator.d.ts +1 -1
  25. package/dest/msg_validators/tx_validator/data_validator.d.ts.map +1 -1
  26. package/dest/msg_validators/tx_validator/data_validator.js +35 -2
  27. package/dest/msg_validators/tx_validator/factory.d.ts +1 -1
  28. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  29. package/dest/msg_validators/tx_validator/factory.js +8 -2
  30. package/dest/msg_validators/tx_validator/phases_validator.js +1 -1
  31. package/dest/services/encoding.d.ts +5 -1
  32. package/dest/services/encoding.d.ts.map +1 -1
  33. package/dest/services/encoding.js +7 -1
  34. package/dest/services/libp2p/libp2p_service.d.ts +4 -9
  35. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  36. package/dest/services/libp2p/libp2p_service.js +85 -53
  37. package/dest/services/peer-manager/peer_manager.d.ts +1 -1
  38. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  39. package/dest/services/peer-manager/peer_manager.js +4 -2
  40. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts +4 -7
  41. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts.map +1 -1
  42. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.js +43 -56
  43. package/dest/services/reqresp/batch-tx-requester/interface.d.ts +1 -2
  44. package/dest/services/reqresp/batch-tx-requester/interface.d.ts.map +1 -1
  45. package/dest/services/reqresp/batch-tx-requester/missing_txs.d.ts +4 -4
  46. package/dest/services/reqresp/batch-tx-requester/missing_txs.d.ts.map +1 -1
  47. package/dest/services/reqresp/batch-tx-requester/missing_txs.js +7 -7
  48. package/dest/services/reqresp/reqresp.d.ts +1 -1
  49. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  50. package/dest/services/reqresp/reqresp.js +16 -8
  51. package/dest/services/tx_collection/fast_tx_collection.d.ts +1 -4
  52. package/dest/services/tx_collection/fast_tx_collection.d.ts.map +1 -1
  53. package/dest/services/tx_collection/fast_tx_collection.js +57 -73
  54. package/dest/services/tx_collection/proposal_tx_collector.d.ts +6 -7
  55. package/dest/services/tx_collection/proposal_tx_collector.d.ts.map +1 -1
  56. package/dest/services/tx_collection/proposal_tx_collector.js +4 -4
  57. package/dest/services/tx_collection/request_tracker.d.ts +53 -0
  58. package/dest/services/tx_collection/request_tracker.d.ts.map +1 -0
  59. package/dest/services/tx_collection/request_tracker.js +84 -0
  60. package/dest/services/tx_collection/slow_tx_collection.js +1 -1
  61. package/dest/services/tx_collection/tx_collection.d.ts +3 -6
  62. package/dest/services/tx_collection/tx_collection.d.ts.map +1 -1
  63. package/dest/test-helpers/testbench-utils.d.ts +1 -1
  64. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  65. package/dest/test-helpers/testbench-utils.js +20 -2
  66. package/dest/testbench/p2p_client_testbench_worker.js +3 -3
  67. package/package.json +14 -14
  68. package/src/client/p2p_client.ts +6 -4
  69. package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +3 -5
  70. package/src/config.ts +17 -0
  71. package/src/mem_pools/attestation_pool/attestation_pool.ts +3 -3
  72. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +6 -6
  73. package/src/mem_pools/tx_pool_v2/interfaces.ts +4 -4
  74. package/src/msg_validators/attestation_validator/README.md +1 -1
  75. package/src/msg_validators/attestation_validator/attestation_validator.ts +5 -4
  76. package/src/msg_validators/clock_tolerance.ts +4 -3
  77. package/src/msg_validators/proposal_validator/README.md +3 -3
  78. package/src/msg_validators/proposal_validator/proposal_validator.ts +6 -5
  79. package/src/msg_validators/tx_validator/contract_instance_validator.ts +56 -0
  80. package/src/msg_validators/tx_validator/data_validator.ts +42 -1
  81. package/src/msg_validators/tx_validator/factory.ts +7 -0
  82. package/src/msg_validators/tx_validator/phases_validator.ts +1 -1
  83. package/src/services/encoding.ts +9 -1
  84. package/src/services/libp2p/libp2p_service.ts +80 -64
  85. package/src/services/peer-manager/peer_manager.ts +5 -2
  86. package/src/services/reqresp/batch-tx-requester/README.md +46 -7
  87. package/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +35 -60
  88. package/src/services/reqresp/batch-tx-requester/interface.ts +0 -1
  89. package/src/services/reqresp/batch-tx-requester/missing_txs.ts +6 -6
  90. package/src/services/reqresp/reqresp.ts +18 -10
  91. package/src/services/tx_collection/fast_tx_collection.ts +57 -83
  92. package/src/services/tx_collection/proposal_tx_collector.ts +8 -13
  93. package/src/services/tx_collection/request_tracker.ts +127 -0
  94. package/src/services/tx_collection/slow_tx_collection.ts +1 -1
  95. package/src/services/tx_collection/tx_collection.ts +3 -5
  96. package/src/test-helpers/testbench-utils.ts +28 -3
  97. package/src/testbench/p2p_client_testbench_worker.ts +3 -5
  98. package/dest/services/tx_collection/missing_txs_tracker.d.ts +0 -32
  99. package/dest/services/tx_collection/missing_txs_tracker.d.ts.map +0 -1
  100. package/dest/services/tx_collection/missing_txs_tracker.js +0 -27
  101. package/src/services/tx_collection/missing_txs_tracker.ts +0 -52
@@ -9,6 +9,14 @@ import { webcrypto } from 'node:crypto';
9
9
  import { compressSync, uncompressSync } from 'snappy';
10
10
  import xxhashFactory from 'xxhash-wasm';
11
11
 
12
+ /** Thrown when a Snappy-compressed response exceeds the allowed decompressed size. */
13
+ export class OversizedSnappyResponseError extends Error {
14
+ constructor(decompressedSize: number, maxSizeKb: number) {
15
+ super(`Decompressed size ${decompressedSize} exceeds maximum allowed size of ${maxSizeKb}kb`);
16
+ this.name = 'OversizedSnappyResponseError';
17
+ }
18
+ }
19
+
12
20
  // Load WASM
13
21
  const xxhash = await xxhashFactory();
14
22
 
@@ -86,7 +94,7 @@ export class SnappyTransform implements DataTransform {
86
94
  const { decompressedSize } = readSnappyPreamble(data);
87
95
  if (decompressedSize > maxSizeKb * 1024) {
88
96
  this.logger.warn(`Decompressed size ${decompressedSize} exceeds maximum allowed size of ${maxSizeKb}kb`);
89
- throw new Error(`Decompressed size ${decompressedSize} exceeds maximum allowed size of ${maxSizeKb}kb`);
97
+ throw new OversizedSnappyResponseError(decompressedSize, maxSizeKb);
90
98
  }
91
99
 
92
100
  return Buffer.from(uncompressSync(data, { asBuffer: true }));
@@ -18,7 +18,6 @@ import {
18
18
  type CheckpointProposalCore,
19
19
  type Gossipable,
20
20
  P2PMessage,
21
- type ValidationResult as P2PValidationResult,
22
21
  PeerErrorSeverity,
23
22
  PeerErrorSeverityByHarshness,
24
23
  TopicType,
@@ -131,7 +130,7 @@ type ValidationOutcome = { allPassed: true } | { allPassed: false; failure: Vali
131
130
  // REFACTOR: Unify with the type above
132
131
  type ReceivedMessageValidationResult<T, M = undefined> =
133
132
  | { obj: T; result: Exclude<TopicValidatorResult, TopicValidatorResult.Reject>; metadata?: M }
134
- | { obj?: T; result: TopicValidatorResult.Reject; metadata?: M };
133
+ | { obj?: T; result: TopicValidatorResult.Reject; metadata?: M; severity: PeerErrorSeverity };
135
134
 
136
135
  /**
137
136
  * Lib P2P implementation of the P2PService interface.
@@ -226,7 +225,7 @@ export class LibP2PService extends WithTracer implements P2PService {
226
225
 
227
226
  const proposalValidatorOpts = {
228
227
  txsPermitted: !config.disableTransactions,
229
- maxTxsPerBlock: config.validateMaxTxsPerBlock,
228
+ maxTxsPerBlock: config.validateMaxTxsPerBlock ?? config.validateMaxTxsPerCheckpoint,
230
229
  };
231
230
  this.blockProposalValidator = new BlockProposalValidator(epochCache, proposalValidatorOpts);
232
231
  this.checkpointProposalValidator = new CheckpointProposalValidator(epochCache, proposalValidatorOpts);
@@ -883,30 +882,56 @@ export class LibP2PService extends WithTracer implements P2PService {
883
882
  source: PeerId,
884
883
  topicType: TopicType,
885
884
  ): Promise<ReceivedMessageValidationResult<T, M>> {
886
- let resultAndObj: ReceivedMessageValidationResult<T, M> = { result: TopicValidatorResult.Reject };
885
+ // Default to reject result with a penalty if validation function throws an error
886
+ let resultAndObj: ReceivedMessageValidationResult<T, M> = {
887
+ result: TopicValidatorResult.Reject,
888
+ severity: PeerErrorSeverity.MidToleranceError,
889
+ };
887
890
  const timer = new Timer();
888
891
  try {
889
892
  resultAndObj = await validationFunc();
890
893
  } catch (err) {
891
- this.peerManager.penalizePeer(source, PeerErrorSeverity.LowToleranceError);
892
- this.logger.error(`Error deserializing and validating gossipsub message`, err, {
893
- msgId,
894
- source: source.toString(),
895
- topicType,
896
- });
894
+ this.logger.error(`Error validating gossipsub message`, err, { msgId, source: source.toString(), topicType });
897
895
  }
898
896
 
899
897
  if (resultAndObj.result === TopicValidatorResult.Accept) {
898
+ this.logger.debug(`Message ${topicType} accepted by validator`, { msgId, source: source.toString(), topicType });
900
899
  this.instrumentation.recordMessageValidation(topicType, timer);
900
+ } else if (resultAndObj.result === TopicValidatorResult.Reject) {
901
+ this.logger.warn(`Message ${topicType} rejected by validator with severity ${resultAndObj.severity}`, {
902
+ msgId,
903
+ source: source.toString(),
904
+ topicType,
905
+ severity: resultAndObj.severity,
906
+ });
907
+ this.peerManager.penalizePeer(source, resultAndObj.severity);
908
+ } else {
909
+ this.logger.trace(`Message ${topicType} ignored by validator`, { msgId, source: source.toString(), topicType });
901
910
  }
902
911
 
903
912
  this.node.services.pubsub.reportMessageValidationResult(msgId, source.toString(), resultAndObj.result);
904
913
  return resultAndObj;
905
914
  }
906
915
 
916
+ private tryDeserialize<T>(deserializeFunc: () => T, msgId: string, source: PeerId): T | undefined {
917
+ try {
918
+ return deserializeFunc();
919
+ } catch (err) {
920
+ this.logger.warn(`Failed to deserialize gossipsub message from buffer`, {
921
+ err,
922
+ msgId,
923
+ source: source.toString(),
924
+ });
925
+ return undefined;
926
+ }
927
+ }
928
+
907
929
  protected async handleGossipedTx(payloadData: Buffer, msgId: string, source: PeerId) {
908
930
  const validationFunc: () => Promise<ReceivedMessageValidationResult<Tx>> = async () => {
909
- const tx = Tx.fromBuffer(payloadData);
931
+ const tx = this.tryDeserialize(() => Tx.fromBuffer(payloadData), msgId, source);
932
+ if (!tx) {
933
+ return { result: TopicValidatorResult.Reject, severity: PeerErrorSeverity.LowToleranceError };
934
+ }
910
935
 
911
936
  const currentBlockNumber = await this.archiver.getBlockNumber();
912
937
  const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot();
@@ -931,8 +956,7 @@ export class LibP2PService extends WithTracer implements P2PService {
931
956
  severity,
932
957
  source: source.toString(),
933
958
  });
934
- this.peerManager.penalizePeer(source, severity);
935
- return { result: TopicValidatorResult.Reject };
959
+ return { result: TopicValidatorResult.Reject, severity };
936
960
  }
937
961
 
938
962
  // Pool pre-check: see if the pool would accept this tx before doing expensive proof verification
@@ -954,8 +978,7 @@ export class LibP2PService extends WithTracer implements P2PService {
954
978
  severity,
955
979
  source: source.toString(),
956
980
  });
957
- this.peerManager.penalizePeer(source, severity);
958
- return { result: TopicValidatorResult.Reject };
981
+ return { result: TopicValidatorResult.Reject, severity };
959
982
  }
960
983
 
961
984
  // Pool add: persist the tx
@@ -976,7 +999,11 @@ export class LibP2PService extends WithTracer implements P2PService {
976
999
  } else if (wasIgnored) {
977
1000
  return { result: TopicValidatorResult.Ignore, obj: tx };
978
1001
  } else {
979
- return { result: TopicValidatorResult.Reject };
1002
+ this.logger.warn(`Gossiped tx ${txHash.toString()} unexpectedly rejected by pool`, {
1003
+ source: source.toString(),
1004
+ txHash: txHash.toString(),
1005
+ });
1006
+ return { result: TopicValidatorResult.Reject, severity: PeerErrorSeverity.HighToleranceError };
980
1007
  }
981
1008
  };
982
1009
 
@@ -1006,7 +1033,16 @@ export class LibP2PService extends WithTracer implements P2PService {
1006
1033
  source: PeerId,
1007
1034
  ): Promise<void> {
1008
1035
  const { result, obj: attestation } = await this.validateReceivedMessage<CheckpointAttestation>(
1009
- () => this.validateAndStoreCheckpointAttestation(source, CheckpointAttestation.fromBuffer(payloadData)),
1036
+ () => {
1037
+ const attestation = this.tryDeserialize(() => CheckpointAttestation.fromBuffer(payloadData), msgId, source);
1038
+ if (!attestation) {
1039
+ return Promise.resolve({
1040
+ result: TopicValidatorResult.Reject,
1041
+ severity: PeerErrorSeverity.LowToleranceError,
1042
+ });
1043
+ }
1044
+ return this.validateAndStoreCheckpointAttestation(source, attestation);
1045
+ },
1010
1046
  msgId,
1011
1047
  source,
1012
1048
  TopicType.checkpoint_attestation,
@@ -1039,8 +1075,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1039
1075
 
1040
1076
  if (validationResult.result === 'reject') {
1041
1077
  this.logger.warn(`Penalizing peer ${peerId} for checkpoint attestation validation failure`);
1042
- this.peerManager.penalizePeer(peerId, validationResult.severity);
1043
- return { result: TopicValidatorResult.Reject };
1078
+ return { result: TopicValidatorResult.Reject, severity: validationResult.severity };
1044
1079
  }
1045
1080
 
1046
1081
  if (validationResult.result === 'ignore') {
@@ -1066,16 +1101,16 @@ export class LibP2PService extends WithTracer implements P2PService {
1066
1101
  return { result: TopicValidatorResult.Ignore, obj: attestation };
1067
1102
  }
1068
1103
 
1069
- // Could not add (cap reached for signer), no need to re-broadcast
1104
+ // Could not add (cap reached for signer), penalize and do not re-broadcast
1070
1105
  if (!added) {
1071
- this.logger.warn(`Dropping checkpoint attestation due to cap`, {
1106
+ this.logger.warn(`Rejecting checkpoint attestation due to cap`, {
1072
1107
  slot: slot.toString(),
1073
1108
  archive: attestation.archive.toString(),
1074
1109
  source: peerId.toString(),
1075
1110
  attester: attestation.getSender()?.toString(),
1076
1111
  count,
1077
1112
  });
1078
- return { result: TopicValidatorResult.Ignore, obj: attestation };
1113
+ return { result: TopicValidatorResult.Reject, severity: PeerErrorSeverity.HighToleranceError };
1079
1114
  }
1080
1115
 
1081
1116
  // Check if this is a duplicate attestation (signer attested to a different proposal at the same slot)
@@ -1130,8 +1165,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1130
1165
 
1131
1166
  if (validationResult.result === 'reject') {
1132
1167
  this.logger.warn(`Penalizing peer ${peerId} for block proposal validation failure`);
1133
- this.peerManager.penalizePeer(peerId, validationResult.severity);
1134
- return { result: TopicValidatorResult.Reject };
1168
+ return { result: TopicValidatorResult.Reject, severity: validationResult.severity };
1135
1169
  }
1136
1170
 
1137
1171
  if (validationResult.result === 'ignore') {
@@ -1155,7 +1189,6 @@ export class LibP2PService extends WithTracer implements P2PService {
1155
1189
 
1156
1190
  // Too many blocks received for this slot and index, penalize peer and do not re-broadcast
1157
1191
  if (!added) {
1158
- this.peerManager.penalizePeer(peerId, PeerErrorSeverity.HighToleranceError);
1159
1192
  this.logger.warn(`Penalizing peer for block proposal exceeding per-position cap`, {
1160
1193
  ...block.toBlockInfo(),
1161
1194
  indexWithinCheckpoint: block.indexWithinCheckpoint,
@@ -1163,7 +1196,11 @@ export class LibP2PService extends WithTracer implements P2PService {
1163
1196
  proposer: block.getSender()?.toString(),
1164
1197
  source: peerId.toString(),
1165
1198
  });
1166
- return { result: TopicValidatorResult.Reject, metadata: { isEquivocated } };
1199
+ return {
1200
+ result: TopicValidatorResult.Reject,
1201
+ metadata: { isEquivocated },
1202
+ severity: PeerErrorSeverity.HighToleranceError,
1203
+ };
1167
1204
  }
1168
1205
 
1169
1206
  // If this was a duplicate proposal, do not process it, but do invoke the duplicate callback,
@@ -1256,8 +1293,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1256
1293
 
1257
1294
  if (validationResult.result === 'reject') {
1258
1295
  this.logger.warn(`Penalizing peer ${peerId} for checkpoint proposal validation failure`);
1259
- this.peerManager.penalizePeer(peerId, validationResult.severity);
1260
- return { result: TopicValidatorResult.Reject };
1296
+ return { result: TopicValidatorResult.Reject, severity: validationResult.severity };
1261
1297
  }
1262
1298
 
1263
1299
  if (validationResult.result === 'ignore') {
@@ -1272,20 +1308,21 @@ export class LibP2PService extends WithTracer implements P2PService {
1272
1308
  [Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
1273
1309
  [Attributes.P2P_ID]: peerId.toString(),
1274
1310
  });
1275
- const {
1276
- result,
1277
- obj,
1278
- metadata: { isEquivocated } = {},
1279
- } = await this.validateAndStoreBlockProposal(peerId, blockProposal);
1280
- if (result === TopicValidatorResult.Reject || !obj || isEquivocated) {
1311
+ const blockProposalResult = await this.validateAndStoreBlockProposal(peerId, blockProposal);
1312
+ const { obj, metadata: { isEquivocated } = {} } = blockProposalResult;
1313
+ if (blockProposalResult.result === TopicValidatorResult.Reject || !obj || isEquivocated) {
1281
1314
  this.logger.debug(`Rejecting checkpoint due to invalid last block proposal`, {
1282
1315
  [Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
1283
1316
  [Attributes.P2P_ID]: peerId.toString(),
1284
1317
  isEquivocated,
1285
- result,
1318
+ result: blockProposalResult.result,
1286
1319
  });
1287
- return { result: TopicValidatorResult.Reject };
1288
- } else if (result === TopicValidatorResult.Accept && obj && !isEquivocated) {
1320
+ return {
1321
+ result: TopicValidatorResult.Reject,
1322
+ severity:
1323
+ 'severity' in blockProposalResult ? blockProposalResult.severity : PeerErrorSeverity.MidToleranceError,
1324
+ };
1325
+ } else if (blockProposalResult.result === TopicValidatorResult.Accept && obj && !isEquivocated) {
1289
1326
  processBlock = true;
1290
1327
  }
1291
1328
  }
@@ -1312,13 +1349,17 @@ export class LibP2PService extends WithTracer implements P2PService {
1312
1349
  // Too many checkpoint proposals received for this slot, penalize peer and do not re-broadcast
1313
1350
  // Note: We still return the checkpoint obj so the lastBlock can be processed if valid
1314
1351
  if (!added) {
1315
- this.peerManager.penalizePeer(peerId, PeerErrorSeverity.HighToleranceError);
1316
1352
  this.logger.warn(`Penalizing peer for checkpoint proposal exceeding per-slot cap`, {
1317
1353
  ...checkpoint.toCheckpointInfo(),
1318
1354
  count,
1319
1355
  source: peerId.toString(),
1320
1356
  });
1321
- return { result: TopicValidatorResult.Reject, obj: checkpoint, metadata: { isEquivocated, processBlock } };
1357
+ return {
1358
+ result: TopicValidatorResult.Reject,
1359
+ obj: checkpoint,
1360
+ metadata: { isEquivocated, processBlock },
1361
+ severity: PeerErrorSeverity.HighToleranceError,
1362
+ };
1322
1363
  }
1323
1364
 
1324
1365
  // If this was a duplicate proposal, do not process it, but do invoke the duplicate callback,
@@ -1742,31 +1783,6 @@ export class LibP2PService extends WithTracer implements P2PService {
1742
1783
  return PeerErrorSeverity.HighToleranceError;
1743
1784
  }
1744
1785
 
1745
- /**
1746
- * Validate a checkpoint attestation.
1747
- *
1748
- * @param attestation - The checkpoint attestation to validate.
1749
- * @returns True if the checkpoint attestation is valid, false otherwise.
1750
- */
1751
- @trackSpan('Libp2pService.validateCheckpointAttestation', async (_, attestation) => ({
1752
- [Attributes.SLOT_NUMBER]: attestation.payload.header.slotNumber,
1753
- [Attributes.BLOCK_ARCHIVE]: attestation.archive.toString(),
1754
- [Attributes.P2P_ID]: await attestation.p2pMessageLoggingIdentifier().then(i => i.toString()),
1755
- }))
1756
- public async validateCheckpointAttestation(
1757
- peerId: PeerId,
1758
- attestation: CheckpointAttestation,
1759
- ): Promise<P2PValidationResult> {
1760
- const result = await this.checkpointAttestationValidator.validate(attestation);
1761
-
1762
- if (result.result === 'reject') {
1763
- this.logger.warn(`Penalizing peer ${peerId} for checkpoint attestation validation failure`);
1764
- this.peerManager.penalizePeer(peerId, result.severity);
1765
- }
1766
-
1767
- return result;
1768
- }
1769
-
1770
1786
  public getPeerScore(peerId: PeerId): number {
1771
1787
  return this.node.services.pubsub.score.score(peerId.toString());
1772
1788
  }
@@ -32,7 +32,7 @@ import { PeerScoreState, type PeerScoring } from './peer_scoring.js';
32
32
  const MAX_DIAL_ATTEMPTS = 3;
33
33
  const MAX_CACHED_PEERS = 100;
34
34
  const MAX_CACHED_PEER_AGE_MS = 5 * 60 * 1000; // 5 minutes
35
- const FAILED_PEER_BAN_TIME_MS = 5 * 60 * 1000; // 5 minutes timeout after failing MAX_DIAL_ATTEMPTS
35
+ const DEFAULT_FAILED_PEER_BAN_TIME_MS = 5 * 60 * 1000; // 5 minutes timeout after failing MAX_DIAL_ATTEMPTS
36
36
  const GOODBYE_DIAL_TIMEOUT_MS = 1000;
37
37
  const FAILED_AUTH_HANDSHAKE_EXPIRY_MS = 60 * 60 * 1000; // 1 hour
38
38
 
@@ -776,7 +776,8 @@ export class PeerManager implements PeerManagerInterface {
776
776
  // Add to timed out peers
777
777
  this.timedOutPeers.set(id, {
778
778
  peerId: id,
779
- timeoutUntilMs: this.dateProvider.now() + FAILED_PEER_BAN_TIME_MS,
779
+ timeoutUntilMs:
780
+ this.dateProvider.now() + (this.config.peerFailedBanTimeMs ?? DEFAULT_FAILED_PEER_BAN_TIME_MS),
780
781
  });
781
782
  }
782
783
  }
@@ -938,6 +939,8 @@ export class PeerManager implements PeerManagerInterface {
938
939
  `Received auth for validator ${sender.toString()} from peer ${peerIdString}, but this validator is already authenticated to peer ${peerForAddress.toString()}`,
939
940
  { ...logData, address: sender.toString() },
940
941
  );
942
+ this.markAuthHandshakeFailed(peerId);
943
+ this.markPeerForDisconnect(peerId);
941
944
  return;
942
945
  }
943
946
 
@@ -170,6 +170,37 @@ class BlockTxsResponse {
170
170
 
171
171
  The `BitVector` is a compact representation where each bit corresponds to a transaction index in the block proposal. This allows efficient capability advertisement without repeating full hashes.
172
172
 
173
+ ## Cancellation
174
+
175
+ All cancellation is managed by a single `RequestTracker` instance, shared across the entire collection
176
+ flow. The `RequestTracker` owns the deadline, tracks which txs are still missing, and exposes a
177
+ `cancellationToken` promise that resolves when the request should stop (deadline hit, all txs fetched,
178
+ or external `cancel()` call).
179
+
180
+ Cancellation propagates from the deepest stack level upward:
181
+
182
+ ```
183
+ RequestTracker.finish()
184
+ ├── resolves cancellationToken promise
185
+
186
+ ├── BatchTxRequester workers (deepest)
187
+ │ ├── shouldStop() checks requestTracker.cancelled → exit loop
188
+ │ ├── sleepClampedToDeadline races sleep vs cancellationToken → wakes
189
+ │ └── semaphore.acquire races vs cancellationToken → wakes
190
+ │ │
191
+ │ ▼ workers settle → txQueue.end() → generator returns
192
+
193
+ ├── Node collection loops
194
+ │ ├── notFinished() checks requestTracker.cancelled → exit loop
195
+ │ └── inter-retry sleep races vs cancellationToken → wakes
196
+ │ │
197
+ │ ▼ all node loops settle
198
+
199
+ └── collectFast (outermost)
200
+ awaits Promise.allSettled([reqresp, nodes]) → settles after inner tasks
201
+ finally: requestTracker.cancel() (idempotent), cleanup
202
+ ```
203
+
173
204
  ## Key Files
174
205
 
175
206
  | File | Description |
@@ -179,15 +210,16 @@ The `BitVector` is a compact representation where each bit corresponds to a tran
179
210
  | `peer_collection.ts` | Manages peer classification (dumb/smart/bad) and rate limiting |
180
211
  | `interface.ts` | Type definitions for dependencies |
181
212
  | `../protocols/block_txs/` | Wire protocol definitions (`BlockTxsRequest`, `BlockTxsResponse`, `BitVector`) |
213
+ | `../../tx_collection/request_tracker.ts` | Centralized deadline, missing tx tracking, and cancellation signal |
182
214
 
183
215
  ## Stopping Conditions
184
216
 
185
- The `BatchTxRequester` stops when any of these conditions are met:
217
+ The `BatchTxRequester` stops when any of these conditions are met, all managed by the `RequestTracker`:
186
218
 
187
- 1. **All transactions fetched** - Success!
188
- 2. **Deadline exceeded** - Timeout configured by caller
189
- 3. **Abort signal** - External cancellation
190
- 4. **No transactions to fetch** - Nothing was missing
219
+ 1. **All transactions fetched** - `markFetched()` removes the last missing tx, triggering `finish()`
220
+ 2. **Deadline exceeded** - `setTimeout` in `RequestTracker` fires, triggering `finish()`
221
+ 3. **External cancellation** - `RequestTracker.cancel()` called (e.g., from `stop()`, `stopCollectingForBlocksUpTo`)
222
+ 4. **No transactions to fetch** - Empty hash set at construction, `RequestTracker` finishes immediately
191
223
 
192
224
  ## Configuration
193
225
 
@@ -228,11 +260,15 @@ Request to peer fails
228
260
  ## Usage Example
229
261
 
230
262
  ```typescript
263
+ const requestTracker = RequestTracker.create(
264
+ missingTxHashes, // TxHash[] - what we need
265
+ new Date(Date.now() + 5_000), // deadline
266
+ );
267
+
231
268
  const requester = new BatchTxRequester(
232
- missingTxHashes, // TxHash[] - what we need
269
+ requestTracker, // IRequestTracker - tracks missing txs, deadline, and cancellation
233
270
  blockTxsSource, // BlockTxsSource - the proposal or block we need txs for
234
271
  pinnedPeer, // PeerId | undefined - peer expected to have the txs
235
- timeoutMs, // number - how long to try
236
272
  p2pService, // BatchTxRequesterLibP2PService
237
273
  );
238
274
 
@@ -273,6 +309,8 @@ const txs = await BatchTxRequester.collectAllTxs(requester.run());
273
309
  │ 1. Try RPC nodes first (fast) │ │ Periodic polling of RPC nodes │
274
310
  │ 2. Fall back to BatchTxRequester │ │ and peers for missing txs │
275
311
  │ │ │ │
312
+ │ Creates RequestTracker per │ │ │
313
+ │ request with deadline │ │ │
276
314
  └───────────────────┬───────────────┘ └─────────────────────────────────────┘
277
315
 
278
316
  │ For 'proposal' and 'block' requests
@@ -281,6 +319,7 @@ const txs = await BatchTxRequester.collectAllTxs(requester.run());
281
319
  │ BatchTxRequester │
282
320
  │ │
283
321
  │ Aggressive parallel fetching from multiple peers │
322
+ │ Shares RequestTracker with FastTxCollection for unified cancellation │
284
323
  │ Uses BLOCK_TXS sub-protocol for efficient batching │
285
324
  └───────────────────┬─────────────────────────────────────────────────────────┘
286
325
 
@@ -1,15 +1,14 @@
1
1
  import { chunkWrapAround } from '@aztec/foundation/collection';
2
- import { TimeoutError } from '@aztec/foundation/error';
3
2
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
3
  import { FifoMemoryQueue, type ISemaphore, Semaphore } from '@aztec/foundation/queue';
5
4
  import { sleep } from '@aztec/foundation/sleep';
6
- import { DateProvider, executeTimeout } from '@aztec/foundation/timer';
5
+ import { DateProvider } from '@aztec/foundation/timer';
7
6
  import { PeerErrorSeverity } from '@aztec/stdlib/p2p';
8
7
  import { Tx, TxArray, TxHash } from '@aztec/stdlib/tx';
9
8
 
10
9
  import type { PeerId } from '@libp2p/interface';
11
10
 
12
- import type { IMissingTxsTracker } from '../../tx_collection/missing_txs_tracker.js';
11
+ import type { IRequestTracker } from '../../tx_collection/request_tracker.js';
13
12
  import { ReqRespSubProtocol } from '.././interface.js';
14
13
  import { BlockTxsRequest, BlockTxsResponse, type BlockTxsSource } from '.././protocols/index.js';
15
14
  import { ReqRespStatus } from '.././status.js';
@@ -42,16 +41,14 @@ import { BatchRequestTxValidator, type IBatchRequestTxValidator } from './tx_val
42
41
  * - Is the peer which was unable to send us successful response N times in a row
43
42
  * */
44
43
  export class BatchTxRequester {
44
+ private readonly requestTracker: IRequestTracker;
45
45
  private readonly blockTxsSource: BlockTxsSource;
46
46
  private readonly pinnedPeer: PeerId | undefined;
47
- private readonly timeoutMs: number;
48
47
  private readonly p2pService: BatchTxRequesterLibP2PService;
49
48
  private readonly logger: Logger;
50
- private readonly dateProvider: DateProvider;
51
49
  private readonly opts: BatchTxRequesterOptions;
52
50
  private readonly peers: IPeerCollection;
53
51
  private readonly txsMetadata: ITxMetadataCollection;
54
- private readonly deadline: number;
55
52
  private readonly smartRequesterSemaphore: ISemaphore;
56
53
  private readonly txQueue: FifoMemoryQueue<Tx>;
57
54
  private readonly txValidator: IBatchRequestTxValidator;
@@ -60,21 +57,19 @@ export class BatchTxRequester {
60
57
  private readonly txBatchSize: number;
61
58
 
62
59
  constructor(
63
- missingTxsTracker: IMissingTxsTracker,
60
+ requestTracker: IRequestTracker,
64
61
  blockTxsSource: BlockTxsSource,
65
62
  pinnedPeer: PeerId | undefined,
66
- timeoutMs: number,
67
63
  p2pService: BatchTxRequesterLibP2PService,
68
64
  logger?: Logger,
69
65
  dateProvider?: DateProvider,
70
66
  opts?: BatchTxRequesterOptions,
71
67
  ) {
68
+ this.requestTracker = requestTracker;
72
69
  this.blockTxsSource = blockTxsSource;
73
70
  this.pinnedPeer = pinnedPeer;
74
- this.timeoutMs = timeoutMs;
75
71
  this.p2pService = p2pService;
76
72
  this.logger = logger ?? createLogger('p2p:reqresp_batch');
77
- this.dateProvider = dateProvider ?? new DateProvider();
78
73
  this.opts = opts ?? {};
79
74
 
80
75
  this.smartParallelWorkerCount =
@@ -82,7 +77,6 @@ export class BatchTxRequester {
82
77
  this.dumbParallelWorkerCount =
83
78
  this.opts.dumbParallelWorkerCount ?? DEFAULT_BATCH_TX_REQUESTER_DUMB_PARALLEL_WORKER_COUNT;
84
79
  this.txBatchSize = this.opts.txBatchSize ?? DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE;
85
- this.deadline = this.dateProvider.now() + this.timeoutMs;
86
80
  this.txQueue = new FifoMemoryQueue(this.logger);
87
81
  this.txValidator = this.opts.txValidator ?? new BatchRequestTxValidator(this.p2pService.txValidatorConfig);
88
82
 
@@ -93,12 +87,12 @@ export class BatchTxRequester {
93
87
  this.peers = new PeerCollection(
94
88
  this.p2pService.connectionSampler,
95
89
  this.pinnedPeer,
96
- this.dateProvider,
90
+ dateProvider ?? new DateProvider(),
97
91
  badPeerThreshold,
98
92
  this.p2pService.peerScoring,
99
93
  );
100
94
  }
101
- this.txsMetadata = new MissingTxMetadataCollection(missingTxsTracker, this.txBatchSize);
95
+ this.txsMetadata = new MissingTxMetadataCollection(requestTracker, this.txBatchSize);
102
96
  this.smartRequesterSemaphore = this.opts.semaphore ?? new Semaphore(0);
103
97
  }
104
98
 
@@ -106,40 +100,30 @@ export class BatchTxRequester {
106
100
  * Fetches all missing transactions and yields them one by one
107
101
  * */
108
102
  public async *run(): AsyncGenerator<Tx, Tx | undefined, unknown> {
109
- // Our timeout is represented in milliseconds but queue expects seconds
110
- // We also want to make sure we wait at least 1 second in case of very low timeouts
111
- const timeoutQueueAfter = Math.max(Math.ceil(this.timeoutMs / 1_000), 1);
112
103
  try {
113
104
  if (this.txsMetadata.getMissingTxHashes().size === 0) {
114
105
  return undefined;
115
106
  }
116
107
 
117
- // Start workers in background
118
- const workersPromise = executeTimeout(
119
- () => Promise.allSettled([this.smartRequester(), this.dumbRequester(), this.pinnedPeerRequester()]),
120
- this.timeoutMs,
121
- ).finally(() => {
108
+ // Start workers in background. Workers stop themselves via requestTracker.checkCancelled().
109
+ const workersPromise = Promise.allSettled([
110
+ this.smartRequester(),
111
+ this.dumbRequester(),
112
+ this.pinnedPeerRequester(),
113
+ ]).finally(() => {
122
114
  this.txQueue.end();
123
115
  });
124
116
 
117
+ // Yield txs as workers put them on the queue. The queue's end() drains remaining items
118
+ // before returning null, so we don't lose any txs.
125
119
  while (true) {
126
- const tx = await this.txQueue.get(timeoutQueueAfter);
120
+ const tx = await this.txQueue.get();
127
121
 
128
- // null indicates that the queue has ended
129
122
  if (tx === null) {
130
123
  break;
131
124
  }
132
125
 
133
126
  yield tx;
134
-
135
- if (this.shouldStop()) {
136
- // Drain queue before ending
137
- let remaining;
138
- while ((remaining = this.txQueue.getImmediate()) !== undefined) {
139
- yield remaining;
140
- }
141
- break;
142
- }
143
127
  }
144
128
 
145
129
  this.unlockSmartRequesterSemaphores();
@@ -360,7 +344,10 @@ export class BatchTxRequester {
360
344
  ) {
361
345
  try {
362
346
  this.logger.trace(`Smart worker ${workerIndex} started`);
363
- await executeTimeout((_: AbortSignal) => this.smartRequesterSemaphore.acquire(), this.timeoutMs);
347
+ await Promise.race([this.smartRequesterSemaphore.acquire(), this.requestTracker.cancellationToken]);
348
+ if (this.requestTracker.checkCancelled()) {
349
+ return;
350
+ }
364
351
  this.logger.trace(`Smart worker ${workerIndex} acquired semaphore`);
365
352
 
366
353
  while (!this.shouldStop()) {
@@ -384,7 +371,10 @@ export class BatchTxRequester {
384
371
  //
385
372
  // When a dumb peer responds with valid txIndices, it gets
386
373
  // promoted to smart and releases the semaphore, waking this worker.
387
- await executeTimeout((_: AbortSignal) => this.smartRequesterSemaphore.acquire(), this.timeoutMs);
374
+ await Promise.race([this.smartRequesterSemaphore.acquire(), this.requestTracker.cancellationToken]);
375
+ if (this.requestTracker.checkCancelled()) {
376
+ break;
377
+ }
388
378
  this.logger.debug(`Worker loop smart: acquired next smart peer`);
389
379
  continue;
390
380
  }
@@ -411,11 +401,7 @@ export class BatchTxRequester {
411
401
  });
412
402
  }
413
403
  } catch (err: any) {
414
- if (err instanceof TimeoutError) {
415
- this.logger.debug(`Smart worker ${workerIndex} timed out waiting for semaphore`);
416
- } else {
417
- this.logger.error(`Smart worker ${workerIndex} encountered an error: ${err}`);
418
- }
404
+ this.logger.error(`Smart worker ${workerIndex} encountered an error: ${err}`);
419
405
  } finally {
420
406
  this.logger.debug(`Smart worker ${workerIndex} finished`);
421
407
  }
@@ -528,6 +514,9 @@ export class BatchTxRequester {
528
514
  });
529
515
 
530
516
  if (hasInvalidTx) {
517
+ this.logger.warn(`Penalizing peer ${peerId.toString()} for sending invalid transactions in batch response`, {
518
+ peerId,
519
+ });
531
520
  this.peers.penalisePeer(peerId, PeerErrorSeverity.LowToleranceError);
532
521
  } else {
533
522
  // If we have received successful response from the peer, they have "redeemed" themselves and not considered bad anymore
@@ -651,27 +640,14 @@ export class BatchTxRequester {
651
640
  }
652
641
 
653
642
  /*
654
- * @returns true if all missing txs have been fetched */
655
- private fetchedAllTxs() {
656
- return this.txsMetadata.getMissingTxHashes().size == 0;
657
- }
658
-
659
- /*
660
- * Checks if the BatchTxRequester should stop fetching missing txs
661
- * Conditions for stopping are:
662
- * - There have been no missing transactions to start with
663
- * - All transactions have been fetched
664
- * - The deadline has been hit (no more time to fetch)
665
- * - This process has been cancelled via abortSignal
666
- *
667
- * @returns true if BatchTxRequester should stop, otherwise false*/
643
+ * Checks if the BatchTxRequester should stop fetching missing txs.
644
+ * Delegates to requestTracker which covers: deadline hit, all txs fetched, or external cancellation. */
668
645
  private shouldStop() {
669
- const aborted = this.opts.abortSignal?.aborted ?? false;
670
- if (aborted) {
646
+ if (this.requestTracker.checkCancelled()) {
671
647
  this.unlockSmartRequesterSemaphores();
672
648
  }
673
649
 
674
- return aborted || this.fetchedAllTxs() || this.dateProvider.now() > this.deadline;
650
+ return this.requestTracker.checkCancelled();
675
651
  }
676
652
 
677
653
  /*
@@ -689,10 +665,9 @@ export class BatchTxRequester {
689
665
  * This ensures we don't sleep past the deadline.
690
666
  * */
691
667
  private async sleepClampedToDeadline(durationMs: number) {
692
- const remaining = this.deadline - this.dateProvider.now();
693
- const thereIsTimeRemaining = remaining > 0;
694
- if (thereIsTimeRemaining) {
695
- await sleep(Math.min(durationMs, remaining));
668
+ if (this.requestTracker.checkCancelled()) {
669
+ return;
696
670
  }
671
+ await Promise.race([sleep(durationMs), this.requestTracker.cancellationToken]);
697
672
  }
698
673
  }
@@ -49,7 +49,6 @@ export interface BatchTxRequesterOptions {
49
49
  //Injectable for testing purposes
50
50
  semaphore?: ISemaphore;
51
51
  peerCollection?: IPeerCollection;
52
- abortSignal?: AbortSignal;
53
52
  /** Optional tx validator for testing - if not provided, one is created from p2pService.txValidatorConfig */
54
53
  txValidator?: IBatchRequestTxValidator;
55
54
  }