@aztec/p2p 0.67.1 → 0.68.1

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 (211) hide show
  1. package/dest/bootstrap/bootstrap.d.ts +5 -3
  2. package/dest/bootstrap/bootstrap.d.ts.map +1 -1
  3. package/dest/bootstrap/bootstrap.js +17 -15
  4. package/dest/client/factory.d.ts +19 -0
  5. package/dest/client/factory.d.ts.map +1 -0
  6. package/dest/client/factory.js +40 -0
  7. package/dest/client/index.d.ts +1 -15
  8. package/dest/client/index.d.ts.map +1 -1
  9. package/dest/client/index.js +2 -37
  10. package/dest/client/p2p_client.d.ts +6 -6
  11. package/dest/client/p2p_client.d.ts.map +1 -1
  12. package/dest/client/p2p_client.js +12 -11
  13. package/dest/config.d.ts +1 -1
  14. package/dest/config.d.ts.map +1 -1
  15. package/dest/config.js +3 -3
  16. package/dest/errors/reqresp.error.d.ts +12 -1
  17. package/dest/errors/reqresp.error.d.ts.map +1 -1
  18. package/dest/errors/reqresp.error.js +15 -2
  19. package/dest/index.d.ts +2 -2
  20. package/dest/index.d.ts.map +1 -1
  21. package/dest/index.js +3 -3
  22. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +9 -0
  23. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  24. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +3 -0
  25. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -0
  26. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +171 -0
  27. package/dest/mem_pools/attestation_pool/kv_attestation_pool.d.ts +29 -0
  28. package/dest/mem_pools/attestation_pool/kv_attestation_pool.d.ts.map +1 -0
  29. package/dest/mem_pools/attestation_pool/kv_attestation_pool.js +114 -0
  30. package/dest/mem_pools/interface.d.ts +4 -3
  31. package/dest/mem_pools/interface.d.ts.map +1 -1
  32. package/dest/mocks/index.d.ts +7 -6
  33. package/dest/mocks/index.d.ts.map +1 -1
  34. package/dest/mocks/index.js +8 -8
  35. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts +8 -0
  36. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts.map +1 -0
  37. package/dest/msg_validators/attestation_validator/attestation_validator.js +19 -0
  38. package/dest/msg_validators/attestation_validator/index.d.ts +2 -0
  39. package/dest/msg_validators/attestation_validator/index.d.ts.map +1 -0
  40. package/dest/msg_validators/attestation_validator/index.js +2 -0
  41. package/dest/msg_validators/block_proposal_validator/block_proposal_validator.d.ts +8 -0
  42. package/dest/msg_validators/block_proposal_validator/block_proposal_validator.d.ts.map +1 -0
  43. package/dest/msg_validators/block_proposal_validator/block_proposal_validator.js +21 -0
  44. package/dest/msg_validators/block_proposal_validator/index.d.ts +2 -0
  45. package/dest/msg_validators/block_proposal_validator/index.d.ts.map +1 -0
  46. package/dest/msg_validators/block_proposal_validator/index.js +2 -0
  47. package/dest/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.d.ts +8 -0
  48. package/dest/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.d.ts.map +1 -0
  49. package/dest/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.js +16 -0
  50. package/dest/msg_validators/epoch_proof_quote_validator/index.d.ts +2 -0
  51. package/dest/msg_validators/epoch_proof_quote_validator/index.d.ts.map +1 -0
  52. package/dest/msg_validators/epoch_proof_quote_validator/index.js +2 -0
  53. package/dest/msg_validators/index.d.ts +4 -0
  54. package/dest/msg_validators/index.d.ts.map +1 -0
  55. package/dest/msg_validators/index.js +4 -0
  56. package/dest/{tx_validator → msg_validators/tx_validator}/aggregate_tx_validator.d.ts +1 -1
  57. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts.map +1 -0
  58. package/dest/msg_validators/tx_validator/aggregate_tx_validator.js +34 -0
  59. package/dest/msg_validators/tx_validator/data_validator.d.ts.map +1 -0
  60. package/dest/{tx_validator → msg_validators/tx_validator}/data_validator.js +1 -1
  61. package/dest/{tx_validator → msg_validators/tx_validator}/double_spend_validator.d.ts +3 -2
  62. package/dest/msg_validators/tx_validator/double_spend_validator.d.ts.map +1 -0
  63. package/dest/msg_validators/tx_validator/double_spend_validator.js +56 -0
  64. package/dest/msg_validators/tx_validator/index.d.ts.map +1 -0
  65. package/dest/{tx_validator → msg_validators/tx_validator}/index.js +1 -1
  66. package/dest/msg_validators/tx_validator/metadata_validator.d.ts.map +1 -0
  67. package/dest/{tx_validator → msg_validators/tx_validator}/metadata_validator.js +1 -1
  68. package/dest/msg_validators/tx_validator/tx_proof_validator.d.ts.map +1 -0
  69. package/dest/msg_validators/tx_validator/tx_proof_validator.js +29 -0
  70. package/dest/services/data_store.d.ts.map +1 -0
  71. package/dest/services/data_store.js +188 -0
  72. package/dest/{service → services/discv5}/discV5_service.d.ts +3 -9
  73. package/dest/services/discv5/discV5_service.d.ts.map +1 -0
  74. package/dest/services/discv5/discV5_service.js +139 -0
  75. package/dest/services/dummy_service.d.ts.map +1 -0
  76. package/dest/{service → services}/dummy_service.js +1 -1
  77. package/dest/{service → services}/encoding.d.ts +5 -0
  78. package/dest/services/encoding.d.ts.map +1 -0
  79. package/dest/services/encoding.js +65 -0
  80. package/dest/services/index.d.ts +3 -0
  81. package/dest/services/index.d.ts.map +1 -0
  82. package/dest/services/index.js +3 -0
  83. package/dest/services/libp2p/libp2p_service.d.ts +222 -0
  84. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -0
  85. package/dest/services/libp2p/libp2p_service.js +697 -0
  86. package/dest/services/peer-scoring/peer_scoring.d.ts +25 -0
  87. package/dest/services/peer-scoring/peer_scoring.d.ts.map +1 -0
  88. package/dest/services/peer-scoring/peer_scoring.js +75 -0
  89. package/dest/services/peer_manager.d.ts +60 -0
  90. package/dest/services/peer_manager.d.ts.map +1 -0
  91. package/dest/services/peer_manager.js +358 -0
  92. package/dest/services/reqresp/config.d.ts.map +1 -0
  93. package/dest/{service → services}/reqresp/config.js +1 -1
  94. package/dest/services/reqresp/handlers.d.ts.map +1 -0
  95. package/dest/{service → services}/reqresp/handlers.js +1 -1
  96. package/dest/services/reqresp/index.d.ts.map +1 -0
  97. package/dest/{service → services}/reqresp/index.js +1 -1
  98. package/dest/services/reqresp/interface.d.ts.map +1 -0
  99. package/dest/{service → services}/reqresp/interface.js +1 -1
  100. package/dest/services/reqresp/rate_limiter/index.d.ts.map +1 -0
  101. package/dest/{service → services}/reqresp/rate_limiter/index.js +1 -1
  102. package/dest/{service → services}/reqresp/rate_limiter/rate_limiter.d.ts +0 -5
  103. package/dest/services/reqresp/rate_limiter/rate_limiter.d.ts.map +1 -0
  104. package/dest/{service → services}/reqresp/rate_limiter/rate_limiter.js +7 -2
  105. package/dest/services/reqresp/rate_limiter/rate_limits.d.ts.map +1 -0
  106. package/dest/{service → services}/reqresp/rate_limiter/rate_limits.js +1 -1
  107. package/dest/{service → services}/reqresp/reqresp.d.ts +16 -0
  108. package/dest/services/reqresp/reqresp.d.ts.map +1 -0
  109. package/dest/services/reqresp/reqresp.js +279 -0
  110. package/dest/services/service.d.ts.map +1 -0
  111. package/dest/{service → services}/service.js +1 -1
  112. package/dest/services/types.d.ts +38 -0
  113. package/dest/services/types.d.ts.map +1 -0
  114. package/dest/services/types.js +43 -0
  115. package/package.json +14 -11
  116. package/src/bootstrap/bootstrap.ts +25 -20
  117. package/src/client/factory.ts +97 -0
  118. package/src/client/index.ts +1 -73
  119. package/src/client/p2p_client.ts +28 -15
  120. package/src/config.ts +2 -2
  121. package/src/errors/reqresp.error.ts +15 -1
  122. package/src/index.ts +2 -2
  123. package/src/mem_pools/attestation_pool/attestation_pool.ts +10 -0
  124. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +237 -0
  125. package/src/mem_pools/attestation_pool/kv_attestation_pool.ts +153 -0
  126. package/src/mem_pools/interface.ts +5 -3
  127. package/src/mocks/index.ts +16 -10
  128. package/src/msg_validators/attestation_validator/attestation_validator.ts +26 -0
  129. package/src/msg_validators/attestation_validator/index.ts +1 -0
  130. package/src/msg_validators/block_proposal_validator/block_proposal_validator.ts +29 -0
  131. package/src/msg_validators/block_proposal_validator/index.ts +1 -0
  132. package/src/msg_validators/epoch_proof_quote_validator/epoch_proof_quote_validator.ts +22 -0
  133. package/src/msg_validators/epoch_proof_quote_validator/index.ts +1 -0
  134. package/src/msg_validators/index.ts +3 -0
  135. package/src/{tx_validator → msg_validators/tx_validator}/aggregate_tx_validator.ts +5 -3
  136. package/src/{tx_validator → msg_validators/tx_validator}/double_spend_validator.ts +6 -8
  137. package/src/{service → services/discv5}/discV5_service.ts +19 -23
  138. package/src/{service → services}/encoding.ts +21 -3
  139. package/src/services/index.ts +2 -0
  140. package/src/{service → services/libp2p}/libp2p_service.ts +350 -90
  141. package/src/{service → services/peer-scoring}/peer_scoring.ts +27 -23
  142. package/src/services/peer_manager.ts +422 -0
  143. package/src/{service → services}/reqresp/rate_limiter/rate_limiter.ts +2 -1
  144. package/src/{service → services}/reqresp/reqresp.ts +86 -20
  145. package/src/services/types.ts +44 -0
  146. package/dest/service/data_store.d.ts.map +0 -1
  147. package/dest/service/data_store.js +0 -188
  148. package/dest/service/discV5_service.d.ts.map +0 -1
  149. package/dest/service/discV5_service.js +0 -144
  150. package/dest/service/dummy_service.d.ts.map +0 -1
  151. package/dest/service/encoding.d.ts.map +0 -1
  152. package/dest/service/encoding.js +0 -49
  153. package/dest/service/index.d.ts +0 -3
  154. package/dest/service/index.d.ts.map +0 -1
  155. package/dest/service/index.js +0 -3
  156. package/dest/service/libp2p_service.d.ts +0 -136
  157. package/dest/service/libp2p_service.d.ts.map +0 -1
  158. package/dest/service/libp2p_service.js +0 -500
  159. package/dest/service/peer_manager.d.ts +0 -33
  160. package/dest/service/peer_manager.d.ts.map +0 -1
  161. package/dest/service/peer_manager.js +0 -214
  162. package/dest/service/peer_scoring.d.ts +0 -35
  163. package/dest/service/peer_scoring.d.ts.map +0 -1
  164. package/dest/service/peer_scoring.js +0 -72
  165. package/dest/service/reqresp/config.d.ts.map +0 -1
  166. package/dest/service/reqresp/handlers.d.ts.map +0 -1
  167. package/dest/service/reqresp/index.d.ts.map +0 -1
  168. package/dest/service/reqresp/interface.d.ts.map +0 -1
  169. package/dest/service/reqresp/rate_limiter/index.d.ts.map +0 -1
  170. package/dest/service/reqresp/rate_limiter/rate_limiter.d.ts.map +0 -1
  171. package/dest/service/reqresp/rate_limiter/rate_limits.d.ts.map +0 -1
  172. package/dest/service/reqresp/reqresp.d.ts.map +0 -1
  173. package/dest/service/reqresp/reqresp.js +0 -230
  174. package/dest/service/service.d.ts.map +0 -1
  175. package/dest/tx_validator/aggregate_tx_validator.d.ts.map +0 -1
  176. package/dest/tx_validator/aggregate_tx_validator.js +0 -32
  177. package/dest/tx_validator/data_validator.d.ts.map +0 -1
  178. package/dest/tx_validator/double_spend_validator.d.ts.map +0 -1
  179. package/dest/tx_validator/double_spend_validator.js +0 -56
  180. package/dest/tx_validator/index.d.ts.map +0 -1
  181. package/dest/tx_validator/metadata_validator.d.ts.map +0 -1
  182. package/dest/tx_validator/tx_proof_validator.d.ts.map +0 -1
  183. package/dest/tx_validator/tx_proof_validator.js +0 -29
  184. package/src/service/index.ts +0 -2
  185. package/src/service/peer_manager.ts +0 -266
  186. /package/dest/{tx_validator → msg_validators/tx_validator}/data_validator.d.ts +0 -0
  187. /package/dest/{tx_validator → msg_validators/tx_validator}/index.d.ts +0 -0
  188. /package/dest/{tx_validator → msg_validators/tx_validator}/metadata_validator.d.ts +0 -0
  189. /package/dest/{tx_validator → msg_validators/tx_validator}/tx_proof_validator.d.ts +0 -0
  190. /package/dest/{service → services}/data_store.d.ts +0 -0
  191. /package/dest/{service → services}/dummy_service.d.ts +0 -0
  192. /package/dest/{service → services}/reqresp/config.d.ts +0 -0
  193. /package/dest/{service → services}/reqresp/handlers.d.ts +0 -0
  194. /package/dest/{service → services}/reqresp/index.d.ts +0 -0
  195. /package/dest/{service → services}/reqresp/interface.d.ts +0 -0
  196. /package/dest/{service → services}/reqresp/rate_limiter/index.d.ts +0 -0
  197. /package/dest/{service → services}/reqresp/rate_limiter/rate_limits.d.ts +0 -0
  198. /package/dest/{service → services}/service.d.ts +0 -0
  199. /package/src/{tx_validator → msg_validators/tx_validator}/data_validator.ts +0 -0
  200. /package/src/{tx_validator → msg_validators/tx_validator}/index.ts +0 -0
  201. /package/src/{tx_validator → msg_validators/tx_validator}/metadata_validator.ts +0 -0
  202. /package/src/{tx_validator → msg_validators/tx_validator}/tx_proof_validator.ts +0 -0
  203. /package/src/{service → services}/data_store.ts +0 -0
  204. /package/src/{service → services}/dummy_service.ts +0 -0
  205. /package/src/{service → services}/reqresp/config.ts +0 -0
  206. /package/src/{service → services}/reqresp/handlers.ts +0 -0
  207. /package/src/{service → services}/reqresp/index.ts +0 -0
  208. /package/src/{service → services}/reqresp/interface.ts +0 -0
  209. /package/src/{service → services}/reqresp/rate_limiter/index.ts +0 -0
  210. /package/src/{service → services}/reqresp/rate_limiter/rate_limits.ts +0 -0
  211. /package/src/{service → services}/service.ts +0 -0
@@ -1,31 +1,24 @@
1
+ import { PeerErrorSeverity } from '@aztec/circuit-types';
1
2
  import { median } from '@aztec/foundation/collection';
2
3
 
3
- import { type P2PConfig } from '../config.js';
4
-
5
- export enum PeerErrorSeverity {
6
- /**
7
- * Not malicious action, but it must not be tolerated
8
- * ~2 occurrences will get the peer banned
9
- */
10
- LowToleranceError = 'LowToleranceError',
11
- /**
12
- * Negative action that can be tolerated only sometimes
13
- * ~10 occurrences will get the peer banned
14
- */
15
- MidToleranceError = 'MidToleranceError',
16
- /**
17
- * Some error that can be tolerated multiple times
18
- * ~50 occurrences will get the peer banned
19
- */
20
- HighToleranceError = 'HighToleranceError',
21
- }
4
+ import { type P2PConfig } from '../../config.js';
22
5
 
23
6
  const DefaultPeerPenalties = {
24
- [PeerErrorSeverity.LowToleranceError]: 2,
7
+ [PeerErrorSeverity.LowToleranceError]: 50,
25
8
  [PeerErrorSeverity.MidToleranceError]: 10,
26
- [PeerErrorSeverity.HighToleranceError]: 50,
9
+ [PeerErrorSeverity.HighToleranceError]: 2,
27
10
  };
28
11
 
12
+ export enum PeerScoreState {
13
+ Banned,
14
+ Disconnect,
15
+ Healthy,
16
+ }
17
+
18
+ // TODO: move into config / constants
19
+ const MIN_SCORE_BEFORE_BAN = -100;
20
+ const MIN_SCORE_BEFORE_DISCONNECT = -50;
21
+
29
22
  export class PeerScoring {
30
23
  private scores: Map<string, number> = new Map();
31
24
  private lastUpdateTime: Map<string, number> = new Map();
@@ -37,11 +30,11 @@ export class PeerScoring {
37
30
  const orderedValues = config.peerPenaltyValues?.sort((a, b) => a - b);
38
31
  this.peerPenalties = {
39
32
  [PeerErrorSeverity.HighToleranceError]:
40
- orderedValues?.[0] ?? DefaultPeerPenalties[PeerErrorSeverity.LowToleranceError],
33
+ orderedValues?.[0] ?? DefaultPeerPenalties[PeerErrorSeverity.HighToleranceError],
41
34
  [PeerErrorSeverity.MidToleranceError]:
42
35
  orderedValues?.[1] ?? DefaultPeerPenalties[PeerErrorSeverity.MidToleranceError],
43
36
  [PeerErrorSeverity.LowToleranceError]:
44
- orderedValues?.[2] ?? DefaultPeerPenalties[PeerErrorSeverity.HighToleranceError],
37
+ orderedValues?.[2] ?? DefaultPeerPenalties[PeerErrorSeverity.LowToleranceError],
45
38
  };
46
39
  }
47
40
 
@@ -82,6 +75,17 @@ export class PeerScoring {
82
75
  return this.scores.get(peerId) || 0;
83
76
  }
84
77
 
78
+ getScoreState(peerId: string) {
79
+ // TODO: permanently store banned peers???
80
+ const score = this.getScore(peerId);
81
+ if (score < MIN_SCORE_BEFORE_BAN) {
82
+ return PeerScoreState.Banned;
83
+ } else if (score < MIN_SCORE_BEFORE_DISCONNECT) {
84
+ return PeerScoreState.Disconnect;
85
+ }
86
+ return PeerScoreState.Healthy;
87
+ }
88
+
85
89
  getStats(): { medianScore: number } {
86
90
  return { medianScore: median(Array.from(this.scores.values())) ?? 0 };
87
91
  }
@@ -0,0 +1,422 @@
1
+ import { type PeerErrorSeverity, type PeerInfo } from '@aztec/circuit-types';
2
+ import { createLogger } from '@aztec/foundation/log';
3
+ import { type TelemetryClient, WithTracer, trackSpan } from '@aztec/telemetry-client';
4
+
5
+ import { type ENR } from '@chainsafe/enr';
6
+ import { type Connection, type PeerId } from '@libp2p/interface';
7
+ import { type Multiaddr } from '@multiformats/multiaddr';
8
+ import { inspect } from 'util';
9
+
10
+ import { type P2PConfig } from '../config.js';
11
+ import { type PubSubLibp2p } from '../util.js';
12
+ import { PeerScoreState, PeerScoring } from './peer-scoring/peer_scoring.js';
13
+ import { type PeerDiscoveryService } from './service.js';
14
+ import { PeerEvent } from './types.js';
15
+
16
+ const MAX_DIAL_ATTEMPTS = 3;
17
+ const MAX_CACHED_PEERS = 100;
18
+ const MAX_CACHED_PEER_AGE_MS = 5 * 60 * 1000; // 5 minutes
19
+ const FAILED_PEER_BAN_TIME_MS = 5 * 60 * 1000; // 5 minutes timeout after failing MAX_DIAL_ATTEMPTS
20
+
21
+ type CachedPeer = {
22
+ peerId: PeerId;
23
+ enr: ENR;
24
+ multiaddrTcp: Multiaddr;
25
+ dialAttempts: number;
26
+ addedUnixMs: number;
27
+ };
28
+
29
+ type TimedOutPeer = {
30
+ peerId: string;
31
+ timeoutUntilMs: number;
32
+ };
33
+
34
+ export class PeerManager extends WithTracer {
35
+ private cachedPeers: Map<string, CachedPeer> = new Map();
36
+ private peerScoring: PeerScoring;
37
+ private heartbeatCounter: number = 0;
38
+ private displayPeerCountsPeerHeartbeat: number = 0;
39
+ private timedOutPeers: Map<string, TimedOutPeer> = new Map();
40
+
41
+ constructor(
42
+ private libP2PNode: PubSubLibp2p,
43
+ private peerDiscoveryService: PeerDiscoveryService,
44
+ private config: P2PConfig,
45
+ telemetryClient: TelemetryClient,
46
+ private logger = createLogger('p2p:peer-manager'),
47
+ ) {
48
+ super(telemetryClient, 'PeerManager');
49
+
50
+ this.peerScoring = new PeerScoring(config);
51
+ // Handle new established connections
52
+ this.libP2PNode.addEventListener(PeerEvent.CONNECTED, this.handleConnectedPeerEvent.bind(this));
53
+ // Handle lost connections
54
+ this.libP2PNode.addEventListener(PeerEvent.DISCONNECTED, this.handleDisconnectedPeerEvent.bind(this));
55
+
56
+ // Handle Discovered peers
57
+ this.peerDiscoveryService.on(PeerEvent.DISCOVERED, this.handleDiscoveredPeer.bind(this));
58
+
59
+ // Display peer counts every 60 seconds
60
+ this.displayPeerCountsPeerHeartbeat = Math.floor(60_000 / this.config.peerCheckIntervalMS);
61
+ }
62
+
63
+ @trackSpan('PeerManager.heartbeat')
64
+ public heartbeat() {
65
+ this.heartbeatCounter++;
66
+ this.peerScoring.decayAllScores();
67
+
68
+ this.cleanupExpiredTimeouts();
69
+
70
+ this.discover();
71
+ }
72
+
73
+ /**
74
+ * Cleans up expired timeouts.
75
+ *
76
+ * When peers fail to dial after a number of retries, they are temporarily timed out.
77
+ * This function removes any peers that have been in the timed out state for too long.
78
+ * To give them a chance to reconnect.
79
+ */
80
+ private cleanupExpiredTimeouts() {
81
+ // Clean up expired timeouts
82
+ const now = Date.now();
83
+ for (const [peerId, timedOutPeer] of this.timedOutPeers.entries()) {
84
+ if (now >= timedOutPeer.timeoutUntilMs) {
85
+ this.timedOutPeers.delete(peerId);
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Simply logs the type of connected peer.
92
+ * @param e - The connected peer event.
93
+ */
94
+ private handleConnectedPeerEvent(e: CustomEvent<PeerId>) {
95
+ const peerId = e.detail;
96
+ if (this.peerDiscoveryService.isBootstrapPeer(peerId)) {
97
+ this.logger.verbose(`Connected to bootstrap peer ${peerId.toString()}`);
98
+ } else {
99
+ this.logger.verbose(`Connected to transaction peer ${peerId.toString()}`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Simply logs the type of disconnected peer.
105
+ * @param e - The disconnected peer event.
106
+ */
107
+ private handleDisconnectedPeerEvent(e: CustomEvent<PeerId>) {
108
+ const peerId = e.detail;
109
+ if (this.peerDiscoveryService.isBootstrapPeer(peerId)) {
110
+ this.logger.verbose(`Disconnected from bootstrap peer ${peerId.toString()}`);
111
+ } else {
112
+ this.logger.verbose(`Disconnected from transaction peer ${peerId.toString()}`);
113
+ }
114
+ }
115
+
116
+ public penalizePeer(peerId: PeerId, penalty: PeerErrorSeverity) {
117
+ const id = peerId.toString();
118
+ const penaltyValue = this.peerScoring.peerPenalties[penalty];
119
+ const newScore = this.peerScoring.updateScore(id, -penaltyValue);
120
+ this.logger.verbose(`Penalizing peer ${id} with ${penalty} (new score is ${newScore})`);
121
+ }
122
+
123
+ public getPeerScore(peerId: string): number {
124
+ return this.peerScoring.getScore(peerId);
125
+ }
126
+
127
+ public getPeers(includePending = false): PeerInfo[] {
128
+ const connected = this.libP2PNode
129
+ .getPeers()
130
+ .map(peer => ({ id: peer.toString(), score: this.getPeerScore(peer.toString()), status: 'connected' as const }));
131
+
132
+ if (!includePending) {
133
+ return connected;
134
+ }
135
+
136
+ const dialQueue = this.libP2PNode
137
+ .getDialQueue()
138
+ .filter(peer => !!peer.peerId)
139
+ .map(peer => ({
140
+ id: peer.peerId!.toString(),
141
+ status: 'dialing' as const,
142
+ dialStatus: peer.status,
143
+ addresses: peer.multiaddrs.map(m => m.toString()),
144
+ }));
145
+
146
+ const cachedPeers = Array.from(this.cachedPeers.values())
147
+ .filter(peer => !dialQueue.some(dialPeer => dialPeer.id && peer.peerId.toString() === dialPeer.id.toString()))
148
+ .filter(peer => !connected.some(connPeer => connPeer.id.toString() === peer.peerId.toString()))
149
+ .map(peer => ({
150
+ status: 'cached' as const,
151
+ id: peer.peerId.toString(),
152
+ addresses: [peer.multiaddrTcp.toString()],
153
+ dialAttempts: peer.dialAttempts,
154
+ enr: peer.enr.encodeTxt(),
155
+ }));
156
+
157
+ return [...connected, ...dialQueue, ...cachedPeers];
158
+ }
159
+
160
+ /**
161
+ * Discovers peers.
162
+ */
163
+ private discover() {
164
+ const connections = this.libP2PNode.getConnections();
165
+
166
+ const healthyConnections = this.pruneUnhealthyPeers(connections);
167
+
168
+ // Calculate how many connections we're looking to make
169
+ const peersToConnect = this.config.maxPeerCount - healthyConnections.length;
170
+
171
+ const logLevel = this.heartbeatCounter % this.displayPeerCountsPeerHeartbeat === 0 ? 'info' : 'debug';
172
+ this.logger[logLevel](`Connected to ${connections.length} peers`, {
173
+ connections: connections.length,
174
+ maxPeerCount: this.config.maxPeerCount,
175
+ cachedPeers: this.cachedPeers.size,
176
+ ...this.peerScoring.getStats(),
177
+ });
178
+
179
+ // Exit if no peers to connect
180
+ if (peersToConnect <= 0) {
181
+ return;
182
+ }
183
+
184
+ const cachedPeersToDial: CachedPeer[] = [];
185
+
186
+ const pendingDials = new Set(
187
+ this.libP2PNode
188
+ .getDialQueue()
189
+ .map(pendingDial => pendingDial.peerId?.toString())
190
+ .filter(Boolean) as string[],
191
+ );
192
+
193
+ for (const [id, peerData] of this.cachedPeers.entries()) {
194
+ // if already dialling or connected to, remove from cache
195
+ if (
196
+ pendingDials.has(id) ||
197
+ healthyConnections.some(conn => conn.remotePeer.equals(peerData.peerId)) ||
198
+ // if peer has been in cache for the max cache age, remove from cache
199
+ Date.now() - peerData.addedUnixMs > MAX_CACHED_PEER_AGE_MS
200
+ ) {
201
+ this.cachedPeers.delete(id);
202
+ } else {
203
+ // cachedPeersToDial.set(id, enr);
204
+ cachedPeersToDial.push(peerData);
205
+ }
206
+ }
207
+
208
+ // reverse to dial older entries first
209
+ cachedPeersToDial.reverse();
210
+
211
+ for (const peer of cachedPeersToDial) {
212
+ // We remove from the cache before, as dialling will add it back if it fails
213
+ this.cachedPeers.delete(peer.peerId.toString());
214
+ void this.dialPeer(peer);
215
+ }
216
+
217
+ // if we need more peers, start randomNodesQuery
218
+ if (peersToConnect > 0) {
219
+ this.logger.trace(`Running random nodes query to connect to ${peersToConnect} peers`);
220
+ void this.peerDiscoveryService.runRandomNodesQuery();
221
+ }
222
+ }
223
+
224
+ private pruneUnhealthyPeers(connections: Connection[]): Connection[] {
225
+ const connectedHealthyPeers: Connection[] = [];
226
+
227
+ for (const peer of connections) {
228
+ const score = this.peerScoring.getScoreState(peer.remotePeer.toString());
229
+ switch (score) {
230
+ // TODO: add goodbye and give reasons
231
+ case PeerScoreState.Banned:
232
+ case PeerScoreState.Disconnect:
233
+ void this.disconnectPeer(peer.remotePeer);
234
+ break;
235
+ case PeerScoreState.Healthy:
236
+ connectedHealthyPeers.push(peer);
237
+ }
238
+ }
239
+
240
+ return connectedHealthyPeers;
241
+ }
242
+
243
+ // TODO: send a goodbye with a reason to the peer
244
+ private async disconnectPeer(peer: PeerId) {
245
+ this.logger.debug(`Disconnecting peer ${peer.toString()}`);
246
+ await this.libP2PNode.hangUp(peer);
247
+ }
248
+
249
+ /**
250
+ * Handles a discovered peer.
251
+ * @param enr - The discovered peer's ENR.
252
+ */
253
+ private async handleDiscoveredPeer(enr: ENR) {
254
+ // Check that the peer has not already been banned
255
+ const peerId = await enr.peerId();
256
+ const peerIdString = peerId.toString();
257
+
258
+ // Check if peer is temporarily timed out
259
+ const timedOutPeer = this.timedOutPeers.get(peerIdString);
260
+ if (timedOutPeer) {
261
+ if (Date.now() < timedOutPeer.timeoutUntilMs) {
262
+ this.logger.trace(`Skipping timed out peer ${peerId}`);
263
+ return;
264
+ }
265
+ // Timeout period expired, remove from timed out peers
266
+ this.timedOutPeers.delete(peerIdString);
267
+ }
268
+
269
+ if (this.peerScoring.getScoreState(peerIdString) != PeerScoreState.Healthy) {
270
+ return;
271
+ }
272
+
273
+ const [multiaddrTcp] = await Promise.all([enr.getFullMultiaddr('tcp')]);
274
+
275
+ this.logger.trace(`Handling discovered peer ${peerId} at ${multiaddrTcp?.toString() ?? 'undefined address'}`);
276
+
277
+ // stop if no tcp addr in multiaddr
278
+ if (!multiaddrTcp) {
279
+ this.logger.debug(`No TCP address in discovered node's multiaddr ${enr.encodeTxt()}`);
280
+ return;
281
+ }
282
+ // check if peer is already connected
283
+ const connections = this.libP2PNode.getConnections();
284
+ if (connections.some(conn => conn.remotePeer.equals(peerId))) {
285
+ this.logger.trace(`Already connected to peer ${peerId}`);
286
+ return;
287
+ }
288
+
289
+ // check if peer is already in cache
290
+ if (this.cachedPeers.has(peerIdString)) {
291
+ this.logger.trace(`Peer already in cache ${peerIdString}`);
292
+ return;
293
+ }
294
+
295
+ // create cached peer object
296
+ const cachedPeer: CachedPeer = {
297
+ peerId,
298
+ enr,
299
+ multiaddrTcp,
300
+ dialAttempts: 0,
301
+ addedUnixMs: Date.now(),
302
+ };
303
+
304
+ // Determine if we should dial immediately or not
305
+ if (this.shouldDialPeer()) {
306
+ void this.dialPeer(cachedPeer);
307
+ } else {
308
+ this.logger.trace(`Caching peer ${peerIdString}`);
309
+ this.cachedPeers.set(peerIdString, cachedPeer);
310
+ // Prune set of cached peers
311
+ this.pruneCachedPeers();
312
+ }
313
+ }
314
+
315
+ private async dialPeer(peer: CachedPeer) {
316
+ const id = peer.peerId.toString();
317
+
318
+ // Add to the address book before dialing
319
+ await this.libP2PNode.peerStore.merge(peer.peerId, { multiaddrs: [peer.multiaddrTcp] });
320
+
321
+ this.logger.trace(`Dialing peer ${id}`);
322
+ try {
323
+ await this.libP2PNode.dial(peer.multiaddrTcp);
324
+ } catch (error) {
325
+ peer.dialAttempts++;
326
+ if (peer.dialAttempts < MAX_DIAL_ATTEMPTS) {
327
+ this.logger.trace(`Failed to dial peer ${id} (attempt ${peer.dialAttempts})`, { error: inspect(error) });
328
+ this.cachedPeers.set(id, peer);
329
+ } else {
330
+ formatLibp2pDialError(error as Error);
331
+ this.logger.debug(`Failed to dial peer ${id} (dropping)`, { error: inspect(error) });
332
+ this.cachedPeers.delete(id);
333
+ // Add to timed out peers
334
+ this.timedOutPeers.set(id, {
335
+ peerId: id,
336
+ timeoutUntilMs: Date.now() + FAILED_PEER_BAN_TIME_MS,
337
+ });
338
+ }
339
+ }
340
+ }
341
+
342
+ private shouldDialPeer(): boolean {
343
+ const connections = this.libP2PNode.getConnections().length;
344
+ if (connections >= this.config.maxPeerCount) {
345
+ this.logger.trace(
346
+ `Not dialing peer due to max peer count of ${this.config.maxPeerCount} reached (${connections} current connections)`,
347
+ );
348
+ return false;
349
+ }
350
+ return true;
351
+ }
352
+
353
+ private pruneCachedPeers() {
354
+ let peersToDelete = this.cachedPeers.size - MAX_CACHED_PEERS;
355
+ if (peersToDelete <= 0) {
356
+ return;
357
+ }
358
+
359
+ // Remove the oldest peers
360
+ for (const key of this.cachedPeers.keys()) {
361
+ this.cachedPeers.delete(key);
362
+ this.logger.trace(`Pruning peer ${key} from cache`);
363
+ peersToDelete--;
364
+ if (peersToDelete <= 0) {
365
+ break;
366
+ }
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Stops the peer manager.
372
+ * Removing all event listeners.
373
+ */
374
+ public stop() {
375
+ this.libP2PNode.removeEventListener(PeerEvent.CONNECTED, this.handleConnectedPeerEvent);
376
+ this.libP2PNode.removeEventListener(PeerEvent.DISCONNECTED, this.handleDisconnectedPeerEvent);
377
+ this.peerDiscoveryService.off(PeerEvent.DISCOVERED, this.handleDiscoveredPeer);
378
+ }
379
+ }
380
+
381
+ /**
382
+ * copied from github.com/ChainSafe/lodestar
383
+ * libp2p errors with extremely noisy errors here, which are deeply nested taking 30-50 lines.
384
+ * Some known errors:
385
+ * ```
386
+ * Error: The operation was aborted
387
+ * Error: stream ended before 1 bytes became available
388
+ * Error: Error occurred during XX handshake: Error occurred while verifying signed payload: Peer ID doesn't match libp2p public key
389
+ * ```
390
+ *
391
+ * Also the error's message is not properly formatted, where the error message is indented and includes the full stack
392
+ * ```
393
+ * {
394
+ * emessage: '\n' +
395
+ * ' Error: stream ended before 1 bytes became available\n' +
396
+ * ' at /home/lion/Code/eth2.0/lodestar/node_modules/it-reader/index.js:37:9\n' +
397
+ * ' at runMicrotasks (<anonymous>)\n' +
398
+ * ' at decoder (/home/lion/Code/eth2.0/lodestar/node_modules/it-length-prefixed/src/decode.js:113:22)\n' +
399
+ * ' at first (/home/lion/Code/eth2.0/lodestar/node_modules/it-first/index.js:11:20)\n' +
400
+ * ' at Object.exports.read (/home/lion/Code/eth2.0/lodestar/node_modules/multistream-select/src/multistream.js:31:15)\n' +
401
+ * ' at module.exports (/home/lion/Code/eth2.0/lodestar/node_modules/multistream-select/src/select.js:21:19)\n' +
402
+ * ' at Upgrader._encryptOutbound (/home/lion/Code/eth2.0/lodestar/node_modules/libp2p/src/upgrader.js:397:36)\n' +
403
+ * ' at Upgrader.upgradeOutbound (/home/lion/Code/eth2.0/lodestar/node_modules/libp2p/src/upgrader.js:176:11)\n' +
404
+ * ' at ClassIsWrapper.dial (/home/lion/Code/eth2.0/lodestar/node_modules/libp2p-tcp/src/index.js:49:18)'
405
+ * }
406
+ * ```
407
+ *
408
+ * Tracking issue https://github.com/libp2p/js-libp2p/issues/996
409
+ */
410
+ function formatLibp2pDialError(e: Error): void {
411
+ const errorMessage = e.message.trim();
412
+ const newlineIndex = errorMessage.indexOf('\n');
413
+ e.message = newlineIndex !== -1 ? errorMessage.slice(0, newlineIndex) : errorMessage;
414
+
415
+ if (
416
+ e.message.includes('The operation was aborted') ||
417
+ e.message.includes('stream ended before 1 bytes became available') ||
418
+ e.message.includes('The operation was aborted')
419
+ ) {
420
+ e.stack = undefined;
421
+ }
422
+ }
@@ -3,10 +3,11 @@
3
3
  * Rationale is that if it was good enough for them, then it should be good enough for us.
4
4
  * https://github.com/ChainSafe/lodestar
5
5
  */
6
+ import { PeerErrorSeverity } from '@aztec/circuit-types';
7
+
6
8
  import { type PeerId } from '@libp2p/interface';
7
9
 
8
10
  import { type PeerManager } from '../../peer_manager.js';
9
- import { PeerErrorSeverity } from '../../peer_scoring.js';
10
11
  import { type ReqRespSubProtocol, type ReqRespSubProtocolRateLimits } from '../interface.js';
11
12
  import { DEFAULT_RATE_LIMITS } from './rate_limits.js';
12
13
 
@@ -1,16 +1,20 @@
1
1
  // @attribution: lodestar impl for inspiration
2
+ import { PeerErrorSeverity } from '@aztec/circuit-types';
2
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
3
- import { executeTimeoutWithCustomError } from '@aztec/foundation/timer';
4
+ import { executeTimeout } from '@aztec/foundation/timer';
4
5
 
5
6
  import { type IncomingStreamData, type PeerId, type Stream } from '@libp2p/interface';
6
7
  import { pipe } from 'it-pipe';
7
8
  import { type Libp2p } from 'libp2p';
8
- import { compressSync, uncompressSync } from 'snappy';
9
9
  import { type Uint8ArrayList } from 'uint8arraylist';
10
10
 
11
- import { CollectiveReqRespTimeoutError, IndiviualReqRespTimeoutError } from '../../errors/reqresp.error.js';
11
+ import {
12
+ CollectiveReqRespTimeoutError,
13
+ IndividualReqRespTimeoutError,
14
+ InvalidResponseError,
15
+ } from '../../errors/reqresp.error.js';
16
+ import { SnappyTransform } from '../encoding.js';
12
17
  import { type PeerManager } from '../peer_manager.js';
13
- import { PeerErrorSeverity } from '../peer_scoring.js';
14
18
  import { type P2PReqRespConfig } from './config.js';
15
19
  import {
16
20
  DEFAULT_SUB_PROTOCOL_HANDLERS,
@@ -49,6 +53,8 @@ export class ReqResp {
49
53
 
50
54
  private rateLimiter: RequestResponseRateLimiter;
51
55
 
56
+ private snappyTransform: SnappyTransform;
57
+
52
58
  constructor(config: P2PReqRespConfig, protected readonly libp2p: Libp2p, private peerManager: PeerManager) {
53
59
  this.logger = createLogger('p2p:reqresp');
54
60
 
@@ -56,6 +62,7 @@ export class ReqResp {
56
62
  this.individualRequestTimeoutMs = config.individualRequestTimeoutMs;
57
63
 
58
64
  this.rateLimiter = new RequestResponseRateLimiter(peerManager);
65
+ this.snappyTransform = new SnappyTransform();
59
66
  }
60
67
 
61
68
  /**
@@ -143,8 +150,7 @@ export class ReqResp {
143
150
  // The response validator handles peer punishment within
144
151
  const isValid = await responseValidator(request, object, peer);
145
152
  if (!isValid) {
146
- this.logger.error(`Invalid response for ${subProtocol} from ${peer.toString()}`);
147
- return undefined;
153
+ throw new InvalidResponseError();
148
154
  }
149
155
  return object;
150
156
  }
@@ -153,13 +159,13 @@ export class ReqResp {
153
159
  };
154
160
 
155
161
  try {
156
- return await executeTimeoutWithCustomError<InstanceType<SubProtocolMap[SubProtocol]['response']> | undefined>(
162
+ return await executeTimeout<InstanceType<SubProtocolMap[SubProtocol]['response']> | undefined>(
157
163
  requestFunction,
158
164
  this.overallRequestTimeoutMs,
159
165
  () => new CollectiveReqRespTimeoutError(),
160
166
  );
161
167
  } catch (e: any) {
162
- this.logger.error(`${e.message} | subProtocol: ${subProtocol}`);
168
+ this.logger.debug(`${e.message} | subProtocol: ${subProtocol}`);
163
169
  return undefined;
164
170
  }
165
171
  }
@@ -199,19 +205,15 @@ export class ReqResp {
199
205
  this.logger.trace(`Stream opened with ${peerId.toString()} for ${subProtocol}`);
200
206
 
201
207
  // Open the stream with a timeout
202
- const result = await executeTimeoutWithCustomError<Buffer>(
203
- (): Promise<Buffer> => pipe([payload], stream!, this.readMessage),
208
+ const result = await executeTimeout<Buffer>(
209
+ (): Promise<Buffer> => pipe([payload], stream!, this.readMessage.bind(this)),
204
210
  this.individualRequestTimeoutMs,
205
- () => new IndiviualReqRespTimeoutError(),
211
+ () => new IndividualReqRespTimeoutError(),
206
212
  );
207
213
 
208
- await stream.close();
209
- this.logger.trace(`Stream closed with ${peerId.toString()} for ${subProtocol}`);
210
-
211
214
  return result;
212
215
  } catch (e: any) {
213
- this.logger.error(`Error sending request to peer`, e, { peerId: peerId.toString(), subProtocol });
214
- this.peerManager.penalizePeer(peerId, PeerErrorSeverity.HighToleranceError);
216
+ this.handleResponseError(e, peerId, subProtocol);
215
217
  } finally {
216
218
  if (stream) {
217
219
  try {
@@ -224,7 +226,70 @@ export class ReqResp {
224
226
  }
225
227
  }
226
228
  }
227
- return undefined;
229
+ }
230
+
231
+ /**
232
+ * Handle a response error
233
+ *
234
+ * ReqResp errors are punished differently depending on the severity of the offense
235
+ *
236
+ * @param e - The error
237
+ * @param peerId - The peer id
238
+ * @param subProtocol - The sub protocol
239
+ * @returns If the error is non pubishable, then undefined is returned, otherwise the peer is penalized
240
+ */
241
+ private handleResponseError(e: any, peerId: PeerId, subProtocol: ReqRespSubProtocol): void {
242
+ const severity = this.categorizeError(e, peerId, subProtocol);
243
+ if (severity) {
244
+ this.peerManager.penalizePeer(peerId, severity);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Categorize the error and log it.
250
+ */
251
+ private categorizeError(e: any, peerId: PeerId, subProtocol: ReqRespSubProtocol): PeerErrorSeverity | undefined {
252
+ // Non pubishable errors
253
+ // We do not punish a collective timeout, as the node triggers this interupt, independent of the peer's behaviour
254
+ const logTags = {
255
+ peerId: peerId.toString(),
256
+ subProtocol,
257
+ };
258
+ if (e instanceof CollectiveReqRespTimeoutError || e instanceof InvalidResponseError) {
259
+ this.logger.debug(
260
+ `Non-punishable error: ${e.message} | peerId: ${peerId.toString()} | subProtocol: ${subProtocol}`,
261
+ logTags,
262
+ );
263
+ return undefined;
264
+ }
265
+
266
+ // Pubishable errors
267
+ // Connection reset errors in the networking stack are punished with high severity
268
+ // it just signals an unreliable peer
269
+ // We assume that the requesting node has a functioning networking stack.
270
+ if (e?.code === 'ECONNRESET' || e?.code === 'EPIPE') {
271
+ this.logger.debug(`Connection reset: ${peerId.toString()}`, logTags);
272
+ return PeerErrorSeverity.HighToleranceError;
273
+ }
274
+
275
+ if (e?.code === 'ECONNREFUSED') {
276
+ this.logger.debug(`Connection refused: ${peerId.toString()}`, logTags);
277
+ return PeerErrorSeverity.HighToleranceError;
278
+ }
279
+
280
+ // Timeout errors are punished with high tolerance, they can be due to a geogrpahically far away peer or an
281
+ // overloaded peer
282
+ if (e instanceof IndividualReqRespTimeoutError) {
283
+ this.logger.debug(
284
+ `Timeout error: ${e.message} | peerId: ${peerId.toString()} | subProtocol: ${subProtocol}`,
285
+ logTags,
286
+ );
287
+ return PeerErrorSeverity.HighToleranceError;
288
+ }
289
+
290
+ // Catch all error
291
+ this.logger.error(`Unexpected error sending request to peer`, e, logTags);
292
+ return PeerErrorSeverity.HighToleranceError;
228
293
  }
229
294
 
230
295
  /**
@@ -235,8 +300,8 @@ export class ReqResp {
235
300
  for await (const chunk of source) {
236
301
  chunks.push(chunk.subarray());
237
302
  }
238
- const messageData = chunks.concat();
239
- return uncompressSync(Buffer.concat(messageData), { asBuffer: true }) as Buffer;
303
+ const messageData = Buffer.concat(chunks);
304
+ return this.snappyTransform.inboundTransformNoTopic(messageData);
240
305
  }
241
306
 
242
307
  /**
@@ -266,6 +331,7 @@ export class ReqResp {
266
331
  }
267
332
 
268
333
  const handler = this.subProtocolHandlers[protocol];
334
+ const transform = this.snappyTransform;
269
335
 
270
336
  try {
271
337
  await pipe(
@@ -274,7 +340,7 @@ export class ReqResp {
274
340
  for await (const chunkList of source) {
275
341
  const msg = Buffer.from(chunkList.subarray());
276
342
  const response = await handler(msg);
277
- yield new Uint8Array(compressSync(response));
343
+ yield new Uint8Array(transform.outboundTransformNoTopic(response));
278
344
  }
279
345
  },
280
346
  stream,