@aztec/p2p 0.0.1-commit.87a0206 → 0.0.1-commit.8f9871590

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 (213) hide show
  1. package/dest/client/factory.d.ts +3 -3
  2. package/dest/client/factory.d.ts.map +1 -1
  3. package/dest/client/factory.js +42 -8
  4. package/dest/client/interface.d.ts +30 -15
  5. package/dest/client/interface.d.ts.map +1 -1
  6. package/dest/client/p2p_client.d.ts +33 -35
  7. package/dest/client/p2p_client.d.ts.map +1 -1
  8. package/dest/client/p2p_client.js +107 -137
  9. package/dest/config.d.ts +22 -4
  10. package/dest/config.d.ts.map +1 -1
  11. package/dest/config.js +16 -1
  12. package/dest/index.d.ts +2 -1
  13. package/dest/index.d.ts.map +1 -1
  14. package/dest/index.js +1 -0
  15. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +21 -12
  16. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  17. package/dest/mem_pools/attestation_pool/attestation_pool.js +67 -37
  18. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +1 -1
  19. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
  20. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +53 -53
  21. package/dest/mem_pools/attestation_pool/index.d.ts +2 -2
  22. package/dest/mem_pools/attestation_pool/index.d.ts.map +1 -1
  23. package/dest/mem_pools/attestation_pool/index.js +1 -1
  24. package/dest/mem_pools/attestation_pool/mocks.d.ts +2 -2
  25. package/dest/mem_pools/attestation_pool/mocks.d.ts.map +1 -1
  26. package/dest/mem_pools/attestation_pool/mocks.js +2 -2
  27. package/dest/mem_pools/index.d.ts +2 -1
  28. package/dest/mem_pools/index.d.ts.map +1 -1
  29. package/dest/mem_pools/interface.d.ts +3 -3
  30. package/dest/mem_pools/interface.d.ts.map +1 -1
  31. package/dest/mem_pools/tx_pool/eviction/invalid_txs_after_mining_rule.js +3 -3
  32. package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts +102 -0
  33. package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts.map +1 -0
  34. package/dest/mem_pools/tx_pool_v2/deleted_pool.js +242 -0
  35. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.d.ts +1 -1
  36. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.d.ts.map +1 -1
  37. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +3 -0
  38. package/dest/mem_pools/tx_pool_v2/eviction/invalid_txs_after_mining_rule.js +5 -3
  39. package/dest/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.js +3 -1
  40. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.d.ts +1 -1
  41. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.d.ts.map +1 -1
  42. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.js +3 -1
  43. package/dest/mem_pools/tx_pool_v2/index.d.ts +2 -1
  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 -0
  46. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +7 -3
  47. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  48. package/dest/mem_pools/tx_pool_v2/interfaces.js +2 -1
  49. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +28 -4
  50. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  51. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +37 -4
  52. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +8 -2
  53. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
  54. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +15 -2
  55. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +4 -2
  56. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  57. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +6 -2
  58. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +4 -2
  59. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  60. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +110 -78
  61. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts +3 -3
  62. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts.map +1 -1
  63. package/dest/msg_validators/tx_validator/block_header_validator.d.ts +16 -3
  64. package/dest/msg_validators/tx_validator/block_header_validator.d.ts.map +1 -1
  65. package/dest/msg_validators/tx_validator/block_header_validator.js +1 -1
  66. package/dest/msg_validators/tx_validator/double_spend_validator.d.ts +13 -3
  67. package/dest/msg_validators/tx_validator/double_spend_validator.d.ts.map +1 -1
  68. package/dest/msg_validators/tx_validator/double_spend_validator.js +4 -4
  69. package/dest/msg_validators/tx_validator/timestamp_validator.d.ts +20 -4
  70. package/dest/msg_validators/tx_validator/timestamp_validator.d.ts.map +1 -1
  71. package/dest/msg_validators/tx_validator/timestamp_validator.js +6 -6
  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 +2 -2
  76. package/dest/services/encoding.d.ts.map +1 -1
  77. package/dest/services/encoding.js +2 -2
  78. package/dest/services/gossipsub/index.d.ts +3 -0
  79. package/dest/services/gossipsub/index.d.ts.map +1 -0
  80. package/dest/services/gossipsub/index.js +2 -0
  81. package/dest/services/gossipsub/scoring.d.ts +21 -3
  82. package/dest/services/gossipsub/scoring.d.ts.map +1 -1
  83. package/dest/services/gossipsub/scoring.js +24 -7
  84. package/dest/services/gossipsub/topic_score_params.d.ts +161 -0
  85. package/dest/services/gossipsub/topic_score_params.d.ts.map +1 -0
  86. package/dest/services/gossipsub/topic_score_params.js +324 -0
  87. package/dest/services/libp2p/libp2p_service.d.ts +11 -3
  88. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  89. package/dest/services/libp2p/libp2p_service.js +91 -67
  90. package/dest/services/peer-manager/peer_scoring.d.ts +1 -1
  91. package/dest/services/peer-manager/peer_scoring.d.ts.map +1 -1
  92. package/dest/services/peer-manager/peer_scoring.js +25 -2
  93. package/dest/services/reqresp/interface.d.ts +10 -1
  94. package/dest/services/reqresp/interface.d.ts.map +1 -1
  95. package/dest/services/reqresp/interface.js +15 -1
  96. package/dest/services/reqresp/protocols/block_txs/block_txs_handler.d.ts +3 -3
  97. package/dest/services/reqresp/protocols/block_txs/block_txs_handler.d.ts.map +1 -1
  98. package/dest/services/reqresp/protocols/block_txs/block_txs_reqresp.d.ts +7 -1
  99. package/dest/services/reqresp/protocols/block_txs/block_txs_reqresp.d.ts.map +1 -1
  100. package/dest/services/reqresp/protocols/block_txs/block_txs_reqresp.js +15 -0
  101. package/dest/services/reqresp/protocols/tx.d.ts +7 -1
  102. package/dest/services/reqresp/protocols/tx.d.ts.map +1 -1
  103. package/dest/services/reqresp/protocols/tx.js +20 -0
  104. package/dest/services/reqresp/reqresp.d.ts +1 -1
  105. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  106. package/dest/services/reqresp/reqresp.js +11 -4
  107. package/dest/services/service.d.ts +18 -1
  108. package/dest/services/service.d.ts.map +1 -1
  109. package/dest/services/tx_collection/config.d.ts +19 -1
  110. package/dest/services/tx_collection/config.d.ts.map +1 -1
  111. package/dest/services/tx_collection/config.js +46 -0
  112. package/dest/services/tx_collection/fast_tx_collection.d.ts +3 -1
  113. package/dest/services/tx_collection/fast_tx_collection.d.ts.map +1 -1
  114. package/dest/services/tx_collection/fast_tx_collection.js +17 -3
  115. package/dest/services/tx_collection/file_store_tx_collection.d.ts +53 -0
  116. package/dest/services/tx_collection/file_store_tx_collection.d.ts.map +1 -0
  117. package/dest/services/tx_collection/file_store_tx_collection.js +165 -0
  118. package/dest/services/tx_collection/file_store_tx_source.d.ts +28 -0
  119. package/dest/services/tx_collection/file_store_tx_source.d.ts.map +1 -0
  120. package/dest/services/tx_collection/file_store_tx_source.js +59 -0
  121. package/dest/services/tx_collection/index.d.ts +2 -1
  122. package/dest/services/tx_collection/index.d.ts.map +1 -1
  123. package/dest/services/tx_collection/index.js +1 -0
  124. package/dest/services/tx_collection/slow_tx_collection.d.ts +6 -2
  125. package/dest/services/tx_collection/slow_tx_collection.d.ts.map +1 -1
  126. package/dest/services/tx_collection/slow_tx_collection.js +55 -23
  127. package/dest/services/tx_collection/tx_collection.d.ts +19 -7
  128. package/dest/services/tx_collection/tx_collection.d.ts.map +1 -1
  129. package/dest/services/tx_collection/tx_collection.js +75 -3
  130. package/dest/services/tx_collection/tx_collection_sink.d.ts +15 -6
  131. package/dest/services/tx_collection/tx_collection_sink.d.ts.map +1 -1
  132. package/dest/services/tx_collection/tx_collection_sink.js +13 -7
  133. package/dest/services/tx_file_store/config.d.ts +1 -3
  134. package/dest/services/tx_file_store/config.d.ts.map +1 -1
  135. package/dest/services/tx_file_store/config.js +0 -4
  136. package/dest/services/tx_file_store/tx_file_store.d.ts +4 -3
  137. package/dest/services/tx_file_store/tx_file_store.d.ts.map +1 -1
  138. package/dest/services/tx_file_store/tx_file_store.js +8 -5
  139. package/dest/services/tx_provider.d.ts +3 -3
  140. package/dest/services/tx_provider.d.ts.map +1 -1
  141. package/dest/services/tx_provider.js +5 -4
  142. package/dest/test-helpers/make-test-p2p-clients.d.ts +3 -3
  143. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  144. package/dest/test-helpers/mock-pubsub.d.ts +27 -1
  145. package/dest/test-helpers/mock-pubsub.d.ts.map +1 -1
  146. package/dest/test-helpers/mock-pubsub.js +97 -2
  147. package/dest/test-helpers/reqresp-nodes.d.ts +1 -1
  148. package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
  149. package/dest/test-helpers/reqresp-nodes.js +2 -1
  150. package/dest/test-helpers/testbench-utils.d.ts +32 -24
  151. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  152. package/dest/test-helpers/testbench-utils.js +103 -36
  153. package/dest/testbench/p2p_client_testbench_worker.js +3 -3
  154. package/package.json +14 -14
  155. package/src/client/factory.ts +78 -9
  156. package/src/client/interface.ts +32 -13
  157. package/src/client/p2p_client.ts +135 -157
  158. package/src/config.ts +34 -2
  159. package/src/index.ts +1 -0
  160. package/src/mem_pools/attestation_pool/attestation_pool.ts +95 -44
  161. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +57 -53
  162. package/src/mem_pools/attestation_pool/index.ts +3 -3
  163. package/src/mem_pools/attestation_pool/mocks.ts +2 -1
  164. package/src/mem_pools/index.ts +3 -0
  165. package/src/mem_pools/interface.ts +2 -2
  166. package/src/mem_pools/tx_pool/README.md +1 -1
  167. package/src/mem_pools/tx_pool/eviction/invalid_txs_after_mining_rule.ts +3 -3
  168. package/src/mem_pools/tx_pool_v2/README.md +76 -10
  169. package/src/mem_pools/tx_pool_v2/deleted_pool.ts +310 -0
  170. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +3 -0
  171. package/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_mining_rule.ts +3 -3
  172. package/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.ts +1 -1
  173. package/src/mem_pools/tx_pool_v2/eviction/low_priority_eviction_rule.ts +3 -1
  174. package/src/mem_pools/tx_pool_v2/index.ts +1 -0
  175. package/src/mem_pools/tx_pool_v2/interfaces.ts +8 -2
  176. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +57 -7
  177. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +18 -2
  178. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +7 -1
  179. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +122 -80
  180. package/src/msg_validators/tx_validator/aggregate_tx_validator.ts +2 -2
  181. package/src/msg_validators/tx_validator/block_header_validator.ts +15 -3
  182. package/src/msg_validators/tx_validator/double_spend_validator.ts +11 -6
  183. package/src/msg_validators/tx_validator/timestamp_validator.ts +23 -18
  184. package/src/services/dummy_service.ts +6 -0
  185. package/src/services/encoding.ts +2 -2
  186. package/src/services/gossipsub/README.md +626 -0
  187. package/src/services/gossipsub/index.ts +2 -0
  188. package/src/services/gossipsub/scoring.ts +29 -5
  189. package/src/services/gossipsub/topic_score_params.ts +451 -0
  190. package/src/services/libp2p/libp2p_service.ts +91 -70
  191. package/src/services/peer-manager/peer_scoring.ts +25 -0
  192. package/src/services/reqresp/interface.ts +26 -1
  193. package/src/services/reqresp/protocols/block_txs/block_txs_handler.ts +2 -2
  194. package/src/services/reqresp/protocols/block_txs/block_txs_reqresp.ts +17 -0
  195. package/src/services/reqresp/protocols/tx.ts +22 -0
  196. package/src/services/reqresp/reqresp.ts +13 -3
  197. package/src/services/service.ts +20 -0
  198. package/src/services/tx_collection/config.ts +68 -0
  199. package/src/services/tx_collection/fast_tx_collection.ts +14 -2
  200. package/src/services/tx_collection/file_store_tx_collection.ts +198 -0
  201. package/src/services/tx_collection/file_store_tx_source.ts +73 -0
  202. package/src/services/tx_collection/index.ts +1 -0
  203. package/src/services/tx_collection/slow_tx_collection.ts +64 -30
  204. package/src/services/tx_collection/tx_collection.ts +109 -13
  205. package/src/services/tx_collection/tx_collection_sink.ts +17 -7
  206. package/src/services/tx_file_store/config.ts +0 -6
  207. package/src/services/tx_file_store/tx_file_store.ts +9 -7
  208. package/src/services/tx_provider.ts +8 -7
  209. package/src/test-helpers/make-test-p2p-clients.ts +3 -3
  210. package/src/test-helpers/mock-pubsub.ts +133 -3
  211. package/src/test-helpers/reqresp-nodes.ts +2 -1
  212. package/src/test-helpers/testbench-utils.ts +117 -40
  213. package/src/testbench/p2p_client_testbench_worker.ts +3 -3
@@ -1,6 +1,5 @@
1
1
  import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
2
  import { BlockNumber, type SlotNumber } from '@aztec/foundation/branded-types';
3
- import { randomInt } from '@aztec/foundation/crypto/random';
4
3
  import { Fr } from '@aztec/foundation/curves/bn254';
5
4
  import { type Logger, createLibp2pComponentLogger, createLogger } from '@aztec/foundation/log';
6
5
  import { RunningPromise } from '@aztec/foundation/running-promise';
@@ -45,7 +44,7 @@ import {
45
44
  type GossipsubMessage,
46
45
  gossipsub,
47
46
  } from '@chainsafe/libp2p-gossipsub';
48
- import { createPeerScoreParams, createTopicScoreParams } from '@chainsafe/libp2p-gossipsub/score';
47
+ import { createPeerScoreParams } from '@chainsafe/libp2p-gossipsub/score';
49
48
  import { SignaturePolicy } from '@chainsafe/libp2p-gossipsub/types';
50
49
  import { noise } from '@chainsafe/libp2p-noise';
51
50
  import { yamux } from '@chainsafe/libp2p-yamux';
@@ -80,7 +79,8 @@ import { getVersions } from '../../versioning.js';
80
79
  import { AztecDatastore } from '../data_store.js';
81
80
  import { DiscV5Service } from '../discv5/discV5_service.js';
82
81
  import { SnappyTransform, fastMsgIdFn, getMsgIdFn, msgIdToStrFn } from '../encoding.js';
83
- import { gossipScoreThresholds } from '../gossipsub/scoring.js';
82
+ import { APP_SPECIFIC_WEIGHT, gossipScoreThresholds } from '../gossipsub/scoring.js';
83
+ import { createAllTopicScoreParams } from '../gossipsub/topic_score_params.js';
84
84
  import type { PeerManagerInterface } from '../peer-manager/interface.js';
85
85
  import { PeerManager } from '../peer-manager/peer_manager.js';
86
86
  import { PeerScoring } from '../peer-manager/peer_scoring.js';
@@ -113,6 +113,7 @@ import { ReqResp } from '../reqresp/reqresp.js';
113
113
  import type {
114
114
  P2PBlockReceivedCallback,
115
115
  P2PCheckpointReceivedCallback,
116
+ P2PDuplicateAttestationCallback,
116
117
  P2PService,
117
118
  PeerDiscoveryService,
118
119
  } from '../service.js';
@@ -155,6 +156,9 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
155
156
  type: 'checkpoint' | 'block';
156
157
  }) => void;
157
158
 
159
+ /** Callback invoked when a duplicate attestation is detected (triggers slashing). */
160
+ private duplicateAttestationCallback?: P2PDuplicateAttestationCallback;
161
+
158
162
  /**
159
163
  * Callback for when a block is received from a peer.
160
164
  * @param block - The block received from the peer.
@@ -311,11 +315,6 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
311
315
  const versions = getVersions(config);
312
316
  const protocolVersion = compressComponentVersions(versions);
313
317
 
314
- const txTopic = createTopicString(TopicType.tx, protocolVersion);
315
- const blockProposalTopic = createTopicString(TopicType.block_proposal, protocolVersion);
316
- const checkpointProposalTopic = createTopicString(TopicType.checkpoint_proposal, protocolVersion);
317
- const checkpointAttestationTopic = createTopicString(TopicType.checkpoint_attestation, protocolVersion);
318
-
319
318
  const preferredPeersEnrs: ENR[] = config.preferredPeers.map(enr => ENR.decodeTxt(enr));
320
319
  const directPeers = (
321
320
  await Promise.all(
@@ -335,6 +334,15 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
335
334
 
336
335
  const announceTcpMultiaddr = config.p2pIp ? [convertToMultiaddr(config.p2pIp, p2pPort, 'tcp')] : [];
337
336
 
337
+ // Create dynamic topic score params based on network configuration
338
+ const l1Constants = epochCache.getL1Constants();
339
+ const topicScoreParams = createAllTopicScoreParams(protocolVersion, {
340
+ slotDurationMs: l1Constants.slotDuration * 1000,
341
+ heartbeatIntervalMs: config.gossipsubInterval,
342
+ targetCommitteeSize: l1Constants.targetCommitteeSize,
343
+ blockDurationMs: config.blockDurationMs,
344
+ });
345
+
338
346
  const node = await createLibp2p({
339
347
  start: false,
340
348
  peerId,
@@ -430,28 +438,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
430
438
  scoreParams: createPeerScoreParams({
431
439
  // IPColocation factor can be disabled for local testing - default to -5
432
440
  IPColocationFactorWeight: config.debugDisableColocationPenalty ? 0 : -5.0,
433
- topics: {
434
- [txTopic]: createTopicScoreParams({
435
- topicWeight: 1,
436
- invalidMessageDeliveriesWeight: -20,
437
- invalidMessageDeliveriesDecay: 0.5,
438
- }),
439
- [blockProposalTopic]: createTopicScoreParams({
440
- topicWeight: 1,
441
- invalidMessageDeliveriesWeight: -20,
442
- invalidMessageDeliveriesDecay: 0.5,
443
- }),
444
- [checkpointProposalTopic]: createTopicScoreParams({
445
- topicWeight: 1,
446
- invalidMessageDeliveriesWeight: -20,
447
- invalidMessageDeliveriesDecay: 0.5,
448
- }),
449
- [checkpointAttestationTopic]: createTopicScoreParams({
450
- topicWeight: 1,
451
- invalidMessageDeliveriesWeight: -20,
452
- invalidMessageDeliveriesDecay: 0.5,
453
- }),
454
- },
441
+ topics: topicScoreParams,
455
442
  }),
456
443
  }) as (components: GossipSubComponents) => GossipSub,
457
444
  components: (components: { connectionManager: ConnectionManager }) => ({
@@ -477,8 +464,12 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
477
464
  epochCache,
478
465
  );
479
466
 
480
- // Update gossipsub score params
481
- node.services.pubsub.score.params.appSpecificWeight = 10;
467
+ // Configure application-specific scoring for gossipsub.
468
+ // The weight scales app score to align with gossipsub thresholds:
469
+ // - Disconnect (-50) × 10 = -500 = gossipThreshold (stops receiving gossip)
470
+ // - Ban (-100) × 10 = -1000 = publishThreshold (cannot publish)
471
+ // Note: positive topic scores can offset penalties, so alignment is best-effort.
472
+ node.services.pubsub.score.params.appSpecificWeight = APP_SPECIFIC_WEIGHT;
482
473
  node.services.pubsub.score.params.appSpecificScore = (peerId: string) =>
483
474
  peerManager.shouldDisableP2PGossip(peerId) ? -Infinity : peerManager.getPeerScore(peerId);
484
475
 
@@ -685,6 +676,15 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
685
676
  this.duplicateProposalCallback = callback;
686
677
  }
687
678
 
679
+ /**
680
+ * Registers a callback to be invoked when a duplicate attestation is detected.
681
+ * A validator signing attestations for different proposals at the same slot.
682
+ * This callback is triggered on the first duplicate (when count goes from 1 to 2).
683
+ */
684
+ public registerDuplicateAttestationCallback(callback: P2PDuplicateAttestationCallback): void {
685
+ this.duplicateAttestationCallback = callback;
686
+ }
687
+
688
688
  /**
689
689
  * Subscribes to a topic.
690
690
  * @param topic - The topic to subscribe to.
@@ -902,20 +902,33 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
902
902
  const validationFunc: () => Promise<ReceivedMessageValidationResult<Tx>> = async () => {
903
903
  const tx = Tx.fromBuffer(payloadData);
904
904
  const isValid = await this.validatePropagatedTx(tx, source);
905
- const exists = isValid && (await this.mempools.txPool.hasTx(tx.getTxHash()));
905
+ if (!isValid) {
906
+ this.logger.trace(`Rejecting invalid propagated tx`, {
907
+ [Attributes.P2P_ID]: source.toString(),
908
+ });
909
+ return { result: TopicValidatorResult.Reject };
910
+ }
911
+
912
+ // Propagate only on pool acceptance
913
+ const txHash = tx.getTxHash();
914
+ const addResult = await this.mempools.txPool.addPendingTxs([tx], { source: 'gossip' });
915
+
916
+ const wasAccepted = addResult.accepted.some(h => h.equals(txHash));
917
+ const wasIgnored = addResult.ignored.some(h => h.equals(txHash));
906
918
 
907
919
  this.logger.trace(`Validate propagated tx`, {
908
920
  isValid,
909
- exists,
921
+ wasAccepted,
922
+ wasIgnored,
910
923
  [Attributes.P2P_ID]: source.toString(),
911
924
  });
912
925
 
913
- if (!isValid) {
914
- return { result: TopicValidatorResult.Reject };
915
- } else if (exists) {
926
+ if (wasAccepted) {
927
+ return { result: TopicValidatorResult.Accept, obj: tx };
928
+ } else if (wasIgnored) {
916
929
  return { result: TopicValidatorResult.Ignore, obj: tx };
917
930
  } else {
918
- return { result: TopicValidatorResult.Accept, obj: tx };
931
+ return { result: TopicValidatorResult.Reject };
919
932
  }
920
933
  };
921
934
 
@@ -924,6 +937,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
924
937
  return;
925
938
  }
926
939
 
940
+ // Tx was accepted into pool and will be propagated - just log and record metrics
927
941
  const txHash = tx.getTxHash();
928
942
  const txHashString = txHash.toString();
929
943
  this.logger.verbose(`Received tx ${txHashString} from external peer ${source.toString()} via gossip`, {
@@ -931,13 +945,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
931
945
  txHash: txHashString,
932
946
  });
933
947
 
934
- if (this.config.dropTransactions && randomInt(1000) < this.config.dropTransactionsProbability * 1000) {
935
- this.logger.warn(`Intentionally dropping tx ${txHashString} (probability rule)`);
936
- return;
937
- }
938
-
939
948
  this.instrumentation.incrementTxReceived(1);
940
- await this.mempools.txPool.addTxs([tx]);
941
949
  }
942
950
 
943
951
  /**
@@ -991,40 +999,53 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
991
999
  return { result: TopicValidatorResult.Ignore, obj: attestation };
992
1000
  }
993
1001
 
994
- // Get committee size for the attestation's slot
995
- const slot = attestation.payload.header.slotNumber;
996
- const { committee } = await this.epochCache.getCommittee(slot);
997
- const committeeSize = committee?.length ?? 0;
998
-
999
1002
  // Try to add the attestation: this handles existence check, cap check, and adding in one call
1000
- const { added, alreadyExists } = await this.mempools.attestationPool.tryAddCheckpointAttestation(
1001
- attestation,
1002
- committeeSize,
1003
- );
1003
+ // count is the number of attestations by this signer for this slot (for duplicate detection)
1004
+ const slot = attestation.payload.header.slotNumber;
1005
+ const { added, alreadyExists, count } =
1006
+ await this.mempools.attestationPool.tryAddCheckpointAttestation(attestation);
1004
1007
 
1005
1008
  this.logger.trace(`Validate propagated checkpoint attestation`, {
1006
1009
  added,
1007
1010
  alreadyExists,
1011
+ count,
1008
1012
  [Attributes.SLOT_NUMBER]: slot.toString(),
1009
1013
  [Attributes.P2P_ID]: peerId.toString(),
1010
1014
  });
1011
1015
 
1012
- // Duplicate attestation received, no need to re-broadcast
1016
+ // Exact same attestation received, no need to re-broadcast
1013
1017
  if (alreadyExists) {
1014
1018
  return { result: TopicValidatorResult.Ignore, obj: attestation };
1015
1019
  }
1016
1020
 
1017
- // Could not add (cap reached), no need to re-broadcast
1021
+ // Could not add (cap reached for signer), no need to re-broadcast
1018
1022
  if (!added) {
1019
- this.logger.warn(`Dropping checkpoint attestation due to per-(slot, proposalId) attestation cap`, {
1023
+ this.logger.warn(`Dropping checkpoint attestation due to cap`, {
1020
1024
  slot: slot.toString(),
1021
1025
  archive: attestation.archive.toString(),
1022
1026
  source: peerId.toString(),
1027
+ attester: attestation.getSender()?.toString(),
1028
+ count,
1023
1029
  });
1024
1030
  return { result: TopicValidatorResult.Ignore, obj: attestation };
1025
1031
  }
1026
1032
 
1027
- // Attestation was added successfully
1033
+ // Check if this is a duplicate attestation (signer attested to a different proposal at the same slot)
1034
+ // count is the number of attestations by this signer for this slot
1035
+ if (count === 2) {
1036
+ const attester = attestation.getSender();
1037
+ if (attester) {
1038
+ this.logger.warn(`Detected duplicate attestation (equivocation) at slot ${slot}`, {
1039
+ slot: slot.toString(),
1040
+ archive: attestation.archive.toString(),
1041
+ source: peerId.toString(),
1042
+ attester: attester.toString(),
1043
+ });
1044
+ this.duplicateAttestationCallback?.({ slot, attester });
1045
+ }
1046
+ }
1047
+
1048
+ // Attestation was added successfully - accept it so other nodes can also detect the equivocation
1028
1049
  return { result: TopicValidatorResult.Accept, obj: attestation };
1029
1050
  }
1030
1051
 
@@ -1070,8 +1091,8 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
1070
1091
  }
1071
1092
 
1072
1093
  // Try to add the proposal: this handles existence check, cap check, and adding in one call
1073
- const { added, alreadyExists, totalForPosition } = await this.mempools.attestationPool.tryAddBlockProposal(block);
1074
- const isEquivocated = totalForPosition !== undefined && totalForPosition > 1;
1094
+ const { added, alreadyExists, count } = await this.mempools.attestationPool.tryAddBlockProposal(block);
1095
+ const isEquivocated = count !== undefined && count > 1;
1075
1096
 
1076
1097
  // Duplicate proposal received, no need to re-broadcast
1077
1098
  if (alreadyExists) {
@@ -1090,7 +1111,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
1090
1111
  this.logger.warn(`Penalizing peer for block proposal exceeding per-position cap`, {
1091
1112
  ...block.toBlockInfo(),
1092
1113
  indexWithinCheckpoint: block.indexWithinCheckpoint,
1093
- totalForPosition,
1114
+ count,
1094
1115
  proposer: block.getSender()?.toString(),
1095
1116
  source: peerId.toString(),
1096
1117
  });
@@ -1107,7 +1128,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
1107
1128
  proposer: proposer?.toString(),
1108
1129
  });
1109
1130
  // Invoke the duplicate callback on the first duplicate spotted only
1110
- if (proposer && totalForPosition === 2) {
1131
+ if (proposer && count === 2) {
1111
1132
  this.duplicateProposalCallback?.({ slot: block.slotNumber, proposer, type: 'block' });
1112
1133
  }
1113
1134
  return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated } };
@@ -1132,8 +1153,8 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
1132
1153
  ...block.toBlockInfo(),
1133
1154
  });
1134
1155
 
1135
- // Mark the txs in this proposal as non-evictable
1136
- await this.mempools.txPool.markTxsAsNonEvictable(block.txHashes);
1156
+ // Mark the txs in this proposal as protected
1157
+ await this.mempools.txPool.protectTxs(block.txHashes, block.blockHeader);
1137
1158
 
1138
1159
  // Call the block received callback to validate the proposal.
1139
1160
  // Note: Validators do NOT attest to individual blocks, only to checkpoint proposals.
@@ -1224,8 +1245,8 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
1224
1245
  // Try to add the checkpoint proposal core: this handles existence check, cap check, and adding in one call
1225
1246
  const checkpointCore = checkpoint.toCore();
1226
1247
  const tryAddResult = await this.mempools.attestationPool.tryAddCheckpointProposal(checkpointCore);
1227
- const { added, alreadyExists, totalForPosition } = tryAddResult;
1228
- const isEquivocated = totalForPosition !== undefined && totalForPosition > 1;
1248
+ const { added, alreadyExists, count } = tryAddResult;
1249
+ const isEquivocated = count !== undefined && count > 1;
1229
1250
 
1230
1251
  // Duplicate proposal received, do not re-broadcast
1231
1252
  if (alreadyExists) {
@@ -1246,7 +1267,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
1246
1267
  this.peerManager.penalizePeer(peerId, PeerErrorSeverity.HighToleranceError);
1247
1268
  this.logger.warn(`Penalizing peer for checkpoint proposal exceeding per-slot cap`, {
1248
1269
  ...checkpoint.toCheckpointInfo(),
1249
- totalForPosition,
1270
+ count,
1250
1271
  source: peerId.toString(),
1251
1272
  });
1252
1273
  return { result: TopicValidatorResult.Reject, obj: checkpoint, metadata: { isEquivocated, processBlock } };
@@ -1262,7 +1283,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
1262
1283
  proposer: proposer?.toString(),
1263
1284
  });
1264
1285
  // Invoke the duplicate callback on the first duplicate spotted only
1265
- if (proposer && totalForPosition === 2) {
1286
+ if (proposer && count === 2) {
1266
1287
  this.duplicateProposalCallback?.({ slot: checkpoint.slotNumber, proposer, type: 'checkpoint' });
1267
1288
  }
1268
1289
  return {
@@ -1520,10 +1541,10 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
1520
1541
  @trackSpan('Libp2pService.validatePropagatedTx', tx => ({
1521
1542
  [Attributes.TX_HASH]: tx.getTxHash().toString(),
1522
1543
  }))
1523
- private async validatePropagatedTx(tx: Tx, peerId: PeerId): Promise<boolean> {
1544
+ protected async validatePropagatedTx(tx: Tx, peerId: PeerId): Promise<boolean> {
1524
1545
  const currentBlockNumber = await this.archiver.getBlockNumber();
1525
1546
 
1526
- // We accept transactions if they are not expired by the next slot (checked based on the IncludeByTimestamp field)
1547
+ // We accept transactions if they are not expired by the next slot (checked based on the ExpirationTimestamp field)
1527
1548
  const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot();
1528
1549
  const messageValidators = await this.createMessageValidators(currentBlockNumber, nextSlotTimestamp);
1529
1550
 
@@ -1578,7 +1599,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
1578
1599
  public async validate(txs: Tx[]): Promise<void> {
1579
1600
  const currentBlockNumber = await this.archiver.getBlockNumber();
1580
1601
 
1581
- // We accept transactions if they are not expired by the next slot (checked based on the IncludeByTimestamp field)
1602
+ // We accept transactions if they are not expired by the next slot (checked based on the ExpirationTimestamp field)
1582
1603
  const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot();
1583
1604
  const messageValidators = await this.createMessageValidators(currentBlockNumber, nextSlotTimestamp);
1584
1605
 
@@ -14,6 +14,21 @@ import type { PeerId } from '@libp2p/interface';
14
14
 
15
15
  import type { P2PConfig } from '../../config.js';
16
16
 
17
+ /**
18
+ * Application-level peer penalties.
19
+ *
20
+ * These scores are multiplied by appSpecificWeight (10) when contributing to gossipsub score.
21
+ * The values are designed to align with gossipsub thresholds:
22
+ *
23
+ * - LowToleranceError (50): 1 error → app score -50 → gossipsub -500 → gossipThreshold
24
+ * - MidToleranceError (10): 5 errors → app score -50 → gossipsub -500 → gossipThreshold
25
+ * - HighToleranceError (2): 25 errors → app score -50 → gossipsub -500 → gossipThreshold
26
+ *
27
+ * Examples of each severity:
28
+ * - LowToleranceError: Invalid messages, deserialization errors, manipulation attempts
29
+ * - MidToleranceError: Hash mismatches, protocol violations
30
+ * - HighToleranceError: Rate limit exceeded, failed responses, transient errors
31
+ */
17
32
  const DefaultPeerPenalties = {
18
33
  [PeerErrorSeverity.LowToleranceError]: 50,
19
34
  [PeerErrorSeverity.MidToleranceError]: 10,
@@ -26,6 +41,16 @@ export enum PeerScoreState {
26
41
  Healthy,
27
42
  }
28
43
 
44
+ /**
45
+ * Score thresholds for peer states.
46
+ *
47
+ * These values align with gossipsub thresholds when multiplied by appSpecificWeight (10):
48
+ * - MIN_SCORE_BEFORE_DISCONNECT (-50) × 10 = -500 = gossipThreshold
49
+ * - MIN_SCORE_BEFORE_BAN (-100) × 10 = -1000 = publishThreshold
50
+ *
51
+ * This ensures that when a peer is disconnected at the application level,
52
+ * they also stop receiving gossip, and when banned, they cannot publish.
53
+ */
29
54
  // TODO: move into config / constants
30
55
  const MIN_SCORE_BEFORE_BAN = -100;
31
56
  const MIN_SCORE_BEFORE_DISCONNECT = -50;
@@ -1,5 +1,6 @@
1
1
  import { Fr } from '@aztec/foundation/curves/bn254';
2
2
  import { L2Block } from '@aztec/stdlib/block';
3
+ import { MAX_L2_BLOCK_SIZE_KB } from '@aztec/stdlib/p2p';
3
4
  import { TxArray, TxHashArray } from '@aztec/stdlib/tx';
4
5
 
5
6
  import type { PeerId } from '@libp2p/interface';
@@ -7,8 +8,13 @@ import type { PeerId } from '@libp2p/interface';
7
8
  import type { P2PReqRespConfig } from './config.js';
8
9
  import type { ConnectionSampler } from './connection-sampler/connection_sampler.js';
9
10
  import { AuthRequest, AuthResponse } from './protocols/auth.js';
10
- import { BlockTxsRequest, BlockTxsResponse } from './protocols/block_txs/block_txs_reqresp.js';
11
+ import {
12
+ BlockTxsRequest,
13
+ BlockTxsResponse,
14
+ calculateBlockTxsResponseSize,
15
+ } from './protocols/block_txs/block_txs_reqresp.js';
11
16
  import { StatusMessage } from './protocols/status.js';
17
+ import { calculateTxResponseSize } from './protocols/tx.js';
12
18
  import type { ReqRespStatus } from './status.js';
13
19
 
14
20
  /*
@@ -211,6 +217,25 @@ export const subProtocolMap = {
211
217
  },
212
218
  };
213
219
 
220
+ /**
221
+ * Type for a function that calculates the expected response size in KB for a given request.
222
+ */
223
+ export type ExpectedResponseSizeCalculator = (requestBuffer: Buffer) => number;
224
+
225
+ /**
226
+ * Map of sub-protocols to their expected response size calculators.
227
+ * These are used to validate that responses don't exceed expected sizes based on request parameters.
228
+ */
229
+ export const subProtocolSizeCalculators: Record<ReqRespSubProtocol, ExpectedResponseSizeCalculator> = {
230
+ [ReqRespSubProtocol.TX]: calculateTxResponseSize,
231
+ [ReqRespSubProtocol.BLOCK_TXS]: calculateBlockTxsResponseSize,
232
+ [ReqRespSubProtocol.BLOCK]: () => MAX_L2_BLOCK_SIZE_KB,
233
+ [ReqRespSubProtocol.STATUS]: () => 1,
234
+ [ReqRespSubProtocol.PING]: () => 1,
235
+ [ReqRespSubProtocol.AUTH]: () => 1,
236
+ [ReqRespSubProtocol.GOODBYE]: () => 1, // No response expected, but provide minimal limit
237
+ };
238
+
214
239
  export interface ReqRespInterface {
215
240
  start(
216
241
  subProtocolHandlers: Partial<ReqRespSubProtocolHandlers>,
@@ -5,7 +5,7 @@ import { TxArray } from '@aztec/stdlib/tx';
5
5
  import type { PeerId } from '@libp2p/interface';
6
6
 
7
7
  import type { AttestationPoolApi } from '../../../../mem_pools/attestation_pool/attestation_pool.js';
8
- import type { TxPool } from '../../../../mem_pools/index.js';
8
+ import type { TxPoolV2 } from '../../../../mem_pools/tx_pool_v2/interfaces.js';
9
9
  import type { ReqRespSubProtocolHandler } from '../../interface.js';
10
10
  import { ReqRespStatus, ReqRespStatusError } from '../../status.js';
11
11
  import { BitVector } from './bitvector.js';
@@ -21,7 +21,7 @@ import { BlockTxsRequest, BlockTxsResponse } from './block_txs_reqresp.js';
21
21
  export function reqRespBlockTxsHandler(
22
22
  attestationPool: AttestationPoolApi,
23
23
  archiver: L2BlockSource,
24
- txPool: TxPool,
24
+ txPool: TxPoolV2,
25
25
  ): ReqRespSubProtocolHandler {
26
26
  /**
27
27
  * Handler for block txs requests
@@ -1,5 +1,6 @@
1
1
  import { Fr } from '@aztec/foundation/curves/bn254';
2
2
  import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize';
3
+ import { MAX_TX_SIZE_KB } from '@aztec/stdlib/p2p';
3
4
  import { TxArray, type TxHash, TxHashArray } from '@aztec/stdlib/tx';
4
5
 
5
6
  import { BitVector } from './bitvector.js';
@@ -125,3 +126,19 @@ export class BlockTxsResponse {
125
126
  return new BlockTxsResponse(Fr.ZERO, new TxArray(), BitVector.init(0, []));
126
127
  }
127
128
  }
129
+
130
+ /**
131
+ * Calculate the expected response size for a BLOCK_TXS request.
132
+ * @param requestBuffer - The serialized request buffer containing BlockTxsRequest
133
+ * @returns Expected response size in KB
134
+ */
135
+ export function calculateBlockTxsResponseSize(requestBuffer: Buffer): number {
136
+ try {
137
+ const request = BlockTxsRequest.fromBuffer(requestBuffer);
138
+ const requestedTxCount = request.txIndices.getTrueIndices().length;
139
+ return requestedTxCount * MAX_TX_SIZE_KB + 1; // +1 KB overhead for serialization
140
+ } catch {
141
+ // If we can't parse the request, fall back to allowing a single transaction response
142
+ return MAX_TX_SIZE_KB + 1;
143
+ }
144
+ }
@@ -1,4 +1,5 @@
1
1
  import { chunk } from '@aztec/foundation/collection';
2
+ import { MAX_TX_SIZE_KB } from '@aztec/stdlib/p2p';
2
3
  import { TxArray, TxHash, TxHashArray } from '@aztec/stdlib/tx';
3
4
 
4
5
  import type { PeerId } from '@libp2p/interface';
@@ -55,3 +56,24 @@ export function reqRespTxHandler(mempools: MemPools): ReqRespSubProtocolHandler
55
56
  export function chunkTxHashesRequest(hashes: TxHash[], chunkSize = 1): Array<TxHashArray> {
56
57
  return chunk(hashes, chunkSize).map(chunk => new TxHashArray(...chunk));
57
58
  }
59
+
60
+ /**
61
+ * Calculate the expected response size for a TX request.
62
+ * @param requestBuffer - The serialized request buffer containing TxHashArray
63
+ * @returns Expected response size in KB
64
+ */
65
+ export function calculateTxResponseSize(requestBuffer: Buffer): number {
66
+ try {
67
+ const txHashes = TxHashArray.fromBuffer(requestBuffer);
68
+ // TxHashArray.fromBuffer returns empty array on parse failure, so check for that
69
+ if (txHashes.length === 0 && requestBuffer.length > 0) {
70
+ // If we got an empty array but had a non-empty buffer, parsing likely failed
71
+ // Fall back to allowing a single transaction response
72
+ return MAX_TX_SIZE_KB + 1;
73
+ }
74
+ return Math.max(txHashes.length, 1) * MAX_TX_SIZE_KB + 1; // +1 KB overhead, at least 1 tx
75
+ } catch {
76
+ // If we can't parse the request, fall back to allowing a single transaction response
77
+ return MAX_TX_SIZE_KB + 1;
78
+ }
79
+ }
@@ -36,6 +36,7 @@ import {
36
36
  type ReqRespSubProtocolValidators,
37
37
  type SubProtocolMap,
38
38
  responseFromBuffer,
39
+ subProtocolSizeCalculators,
39
40
  } from './interface.js';
40
41
  import { ReqRespMetrics } from './metrics.js';
41
42
  import {
@@ -437,6 +438,9 @@ export class ReqResp implements ReqRespInterface {
437
438
  try {
438
439
  this.metrics.recordRequestSent(subProtocol);
439
440
 
441
+ // Calculate expected response size based on the request payload
442
+ const expectedSizeKb = subProtocolSizeCalculators[subProtocol](payload);
443
+
440
444
  this.logger.trace(`Sending request to peer ${peerId.toString()} on sub protocol ${subProtocol}`);
441
445
  stream = await this.connectionSampler.dialProtocol(peerId, subProtocol, dialTimeout);
442
446
  this.logger.trace(
@@ -444,11 +448,14 @@ export class ReqResp implements ReqRespInterface {
444
448
  );
445
449
 
446
450
  const timeoutErr = new IndividualReqRespTimeoutError();
451
+ // Create a wrapper to pass the expected size to readMessage
452
+ const readMessageWithSizeLimit = (source: AsyncIterable<Uint8ArrayList>) =>
453
+ this.readMessage(source, expectedSizeKb);
447
454
  const [_, resp] = await executeTimeout(
448
455
  signal =>
449
456
  Promise.all([
450
457
  pipeline([payload], stream!.sink, { signal }),
451
- pipeline(stream!.source, this.readMessage.bind(this), { signal }),
458
+ pipeline(stream!.source, readMessageWithSizeLimit, { signal }),
452
459
  ]),
453
460
  this.individualRequestTimeoutMs,
454
461
  () => timeoutErr,
@@ -510,8 +517,11 @@ export class ReqResp implements ReqRespInterface {
510
517
  * The message is split into two components
511
518
  * - The first chunk should contain a control byte, indicating the status of the response see `ReqRespStatus`
512
519
  * - The second chunk should contain the response data
520
+ *
521
+ * @param source - The async iterable source of data chunks
522
+ * @param maxSizeKb - Optional maximum expected size in KB for the decompressed response
513
523
  */
514
- private async readMessage(source: AsyncIterable<Uint8ArrayList>): Promise<ReqRespResponse> {
524
+ private async readMessage(source: AsyncIterable<Uint8ArrayList>, maxSizeKb?: number): Promise<ReqRespResponse> {
515
525
  let status: ReqRespStatus | undefined;
516
526
  const chunks: Uint8Array[] = [];
517
527
 
@@ -536,7 +546,7 @@ export class ReqResp implements ReqRespInterface {
536
546
  }
537
547
 
538
548
  const messageData = Buffer.concat(chunks);
539
- const message: Buffer = this.snappyTransform.inboundTransformData(messageData);
549
+ const message: Buffer = this.snappyTransform.inboundTransformData(messageData, undefined, maxSizeKb);
540
550
 
541
551
  return {
542
552
  status: status ?? ReqRespStatus.UNKNOWN,
@@ -57,6 +57,19 @@ export type DuplicateProposalInfo = {
57
57
  */
58
58
  export type P2PDuplicateProposalCallback = (info: DuplicateProposalInfo) => void;
59
59
 
60
+ /** Minimal info passed to the duplicate attestation callback. */
61
+ export type DuplicateAttestationInfo = {
62
+ slot: SlotNumber;
63
+ attester: EthAddress;
64
+ };
65
+
66
+ /**
67
+ * Callback for when a duplicate attestation is detected (equivocation).
68
+ * A validator signing attestations for different proposals at the same slot.
69
+ * Invoked on the first duplicate (when count goes from 1 to 2).
70
+ */
71
+ export type P2PDuplicateAttestationCallback = (info: DuplicateAttestationInfo) => void;
72
+
60
73
  /**
61
74
  * The interface for a P2P service implementation.
62
75
  */
@@ -106,6 +119,13 @@ export interface P2PService {
106
119
  */
107
120
  registerDuplicateProposalCallback(callback: P2PDuplicateProposalCallback): void;
108
121
 
122
+ /**
123
+ * Registers a callback invoked when a duplicate attestation is detected (equivocation).
124
+ * A validator signing attestations for different proposals at the same slot.
125
+ * The callback is triggered on the first duplicate (when count goes from 1 to 2).
126
+ */
127
+ registerDuplicateAttestationCallback(callback: P2PDuplicateAttestationCallback): void;
128
+
109
129
  getEnr(): ENR | undefined;
110
130
 
111
131
  getPeers(includePending?: boolean): PeerInfo[];