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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +7 -6
  2. package/dest/bootstrap/bootstrap.d.ts +1 -1
  3. package/dest/bootstrap/bootstrap.d.ts.map +1 -1
  4. package/dest/bootstrap/bootstrap.js +20 -12
  5. package/dest/client/factory.d.ts +2 -2
  6. package/dest/client/factory.d.ts.map +1 -1
  7. package/dest/client/factory.js +10 -5
  8. package/dest/client/interface.d.ts +36 -8
  9. package/dest/client/interface.d.ts.map +1 -1
  10. package/dest/client/p2p_client.d.ts +23 -12
  11. package/dest/client/p2p_client.d.ts.map +1 -1
  12. package/dest/client/p2p_client.js +38 -35
  13. package/dest/config.d.ts +19 -13
  14. package/dest/config.d.ts.map +1 -1
  15. package/dest/config.js +27 -26
  16. package/dest/errors/reqresp.error.d.ts +13 -1
  17. package/dest/errors/reqresp.error.d.ts.map +1 -1
  18. package/dest/errors/reqresp.error.js +14 -0
  19. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +4 -2
  20. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  21. package/dest/mem_pools/attestation_pool/attestation_pool.js +9 -2
  22. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +1 -1
  23. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
  24. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +43 -0
  25. package/dest/mem_pools/instrumentation.d.ts +9 -4
  26. package/dest/mem_pools/instrumentation.d.ts.map +1 -1
  27. package/dest/mem_pools/instrumentation.js +11 -8
  28. package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts +3 -3
  29. package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts.map +1 -1
  30. package/dest/mem_pools/tx_pool_v2/deleted_pool.js +6 -6
  31. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +1 -1
  32. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +30 -5
  33. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  34. package/dest/mem_pools/tx_pool_v2/interfaces.js +2 -1
  35. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +15 -1
  36. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
  37. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +31 -0
  38. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +9 -3
  39. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  40. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +19 -8
  41. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +17 -4
  42. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  43. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +133 -34
  44. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts +6 -5
  45. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts.map +1 -1
  46. package/dest/msg_validators/attestation_validator/attestation_validator.js +24 -26
  47. package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.d.ts +4 -4
  48. package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.d.ts.map +1 -1
  49. package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.js +2 -2
  50. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts +4 -3
  51. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts.map +1 -1
  52. package/dest/msg_validators/proposal_validator/block_proposal_validator.js +2 -2
  53. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts +4 -3
  54. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts.map +1 -1
  55. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.js +8 -2
  56. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +19 -5
  57. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  58. package/dest/msg_validators/proposal_validator/proposal_validator.js +66 -30
  59. package/dest/msg_validators/tx_validator/factory.d.ts +9 -11
  60. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  61. package/dest/msg_validators/tx_validator/factory.js +15 -6
  62. package/dest/msg_validators/tx_validator/gas_validator.d.ts +12 -7
  63. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  64. package/dest/msg_validators/tx_validator/gas_validator.js +15 -15
  65. package/dest/msg_validators/tx_validator/phases_validator.js +2 -2
  66. package/dest/services/discv5/discV5_service.d.ts +12 -2
  67. package/dest/services/discv5/discV5_service.d.ts.map +1 -1
  68. package/dest/services/discv5/discV5_service.js +84 -15
  69. package/dest/services/discv5/persisted_enr_store.d.ts +38 -0
  70. package/dest/services/discv5/persisted_enr_store.d.ts.map +1 -0
  71. package/dest/services/discv5/persisted_enr_store.js +145 -0
  72. package/dest/services/dummy_service.d.ts +6 -2
  73. package/dest/services/dummy_service.d.ts.map +1 -1
  74. package/dest/services/dummy_service.js +3 -0
  75. package/dest/services/encoding.d.ts +19 -6
  76. package/dest/services/encoding.d.ts.map +1 -1
  77. package/dest/services/encoding.js +25 -15
  78. package/dest/services/gossipsub/topic_score_params.d.ts +6 -23
  79. package/dest/services/gossipsub/topic_score_params.d.ts.map +1 -1
  80. package/dest/services/gossipsub/topic_score_params.js +4 -31
  81. package/dest/services/libp2p/libp2p_service.d.ts +11 -3
  82. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  83. package/dest/services/libp2p/libp2p_service.js +135 -53
  84. package/dest/services/peer-manager/peer_manager.d.ts +15 -2
  85. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  86. package/dest/services/peer-manager/peer_manager.js +46 -18
  87. package/dest/services/peer-manager/peer_scoring.d.ts +28 -1
  88. package/dest/services/peer-manager/peer_scoring.d.ts.map +1 -1
  89. package/dest/services/peer-manager/peer_scoring.js +70 -3
  90. package/dest/services/reqresp/config.d.ts +13 -1
  91. package/dest/services/reqresp/config.d.ts.map +1 -1
  92. package/dest/services/reqresp/config.js +11 -0
  93. package/dest/services/reqresp/rate-limiter/rate_limits.js +3 -3
  94. package/dest/services/reqresp/reqresp.d.ts +1 -1
  95. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  96. package/dest/services/reqresp/reqresp.js +34 -3
  97. package/dest/services/service.d.ts +17 -1
  98. package/dest/services/service.d.ts.map +1 -1
  99. package/dest/services/tx_collection/tx_source.d.ts +1 -1
  100. package/dest/services/tx_collection/tx_source.d.ts.map +1 -1
  101. package/dest/services/tx_collection/tx_source.js +4 -1
  102. package/dest/test-helpers/testbench-utils.d.ts +4 -1
  103. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  104. package/dest/test-helpers/testbench-utils.js +9 -0
  105. package/dest/testbench/p2p_client_testbench_worker.d.ts +1 -1
  106. package/dest/testbench/p2p_client_testbench_worker.d.ts.map +1 -1
  107. package/dest/testbench/p2p_client_testbench_worker.js +19 -2
  108. package/package.json +16 -17
  109. package/src/bootstrap/bootstrap.ts +15 -9
  110. package/src/client/factory.ts +9 -5
  111. package/src/client/interface.ts +29 -6
  112. package/src/client/p2p_client.ts +46 -48
  113. package/src/config.ts +50 -38
  114. package/src/errors/reqresp.error.ts +19 -0
  115. package/src/mem_pools/attestation_pool/attestation_pool.ts +12 -2
  116. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +40 -1
  117. package/src/mem_pools/instrumentation.ts +13 -11
  118. package/src/mem_pools/tx_pool_v2/deleted_pool.ts +7 -7
  119. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +1 -1
  120. package/src/mem_pools/tx_pool_v2/interfaces.ts +28 -4
  121. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +35 -0
  122. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +22 -8
  123. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +158 -33
  124. package/src/msg_validators/attestation_validator/attestation_validator.ts +26 -26
  125. package/src/msg_validators/attestation_validator/fisherman_attestation_validator.ts +4 -3
  126. package/src/msg_validators/proposal_validator/block_proposal_validator.ts +4 -2
  127. package/src/msg_validators/proposal_validator/checkpoint_proposal_validator.ts +10 -2
  128. package/src/msg_validators/proposal_validator/proposal_validator.ts +69 -30
  129. package/src/msg_validators/tx_validator/README.md +2 -0
  130. package/src/msg_validators/tx_validator/factory.ts +19 -13
  131. package/src/msg_validators/tx_validator/gas_validator.ts +16 -20
  132. package/src/msg_validators/tx_validator/phases_validator.ts +2 -2
  133. package/src/services/discv5/discV5_service.ts +88 -9
  134. package/src/services/discv5/persisted_enr_store.ts +158 -0
  135. package/src/services/dummy_service.ts +6 -0
  136. package/src/services/encoding.ts +28 -20
  137. package/src/services/gossipsub/README.md +33 -11
  138. package/src/services/gossipsub/topic_score_params.ts +9 -50
  139. package/src/services/libp2p/libp2p_service.ts +159 -56
  140. package/src/services/peer-manager/peer_manager.ts +48 -17
  141. package/src/services/peer-manager/peer_scoring.ts +75 -3
  142. package/src/services/reqresp/README.md +2 -23
  143. package/src/services/reqresp/config.ts +17 -0
  144. package/src/services/reqresp/rate-limiter/rate_limits.ts +3 -3
  145. package/src/services/reqresp/reqresp.ts +42 -2
  146. package/src/services/service.ts +19 -0
  147. package/src/services/tx_collection/tx_source.ts +2 -1
  148. package/src/test-helpers/testbench-utils.ts +12 -0
  149. package/src/testbench/p2p_client_testbench_worker.ts +22 -2
  150. package/dest/msg_validators/clock_tolerance.d.ts +0 -32
  151. package/dest/msg_validators/clock_tolerance.d.ts.map +0 -1
  152. package/dest/msg_validators/clock_tolerance.js +0 -90
  153. package/src/msg_validators/clock_tolerance.ts +0 -121
@@ -57,6 +57,9 @@ const MIN_SCORE_BEFORE_BAN = -100;
57
57
  const MIN_SCORE_BEFORE_DISCONNECT = -50;
58
58
  const SCORE_CLEANUP_THRESHOLD = 0.1;
59
59
 
60
+ /** An active ban: the score the peer held when banned, and the timestamp at which the ban lifts. */
61
+ type BanRecord = { score: number; expiry: number };
62
+
60
63
  export class PeerScoring {
61
64
  private logger = createLogger('p2p:peer-scoring');
62
65
  private scores: Map<string, number> = new Map();
@@ -67,11 +70,21 @@ export class PeerScoring {
67
70
 
68
71
  private peerStateCounter: UpDownCounter;
69
72
 
73
+ /** Active bans, keyed by peer id. Held in memory only, so they are cleared on restart. */
74
+ private bannedPeers: Map<string, BanRecord> = new Map();
75
+ /**
76
+ * How long a peer remains banned once its score crosses MIN_SCORE_BEFORE_BAN. While banned, the
77
+ * peer's ban score is returned by getScore regardless of decay, so it cannot recover its way out
78
+ * of the ban early. After the ban expires the live (decayed) score takes over again.
79
+ */
80
+ private readonly banDurationMs: number;
81
+
70
82
  constructor(
71
83
  config: P2PConfig,
72
84
  telemetry: TelemetryClient = getTelemetryClient(),
73
85
  private readonly dateProvider: DateProvider = new DateProvider(),
74
86
  ) {
87
+ this.banDurationMs = config.peerBanDurationSeconds * 1000;
75
88
  const orderedValues = config.peerPenaltyValues?.sort((a, b) => a - b);
76
89
  this.peerPenalties = {
77
90
  [PeerErrorSeverity.HighToleranceError]:
@@ -113,9 +126,31 @@ export class PeerScoring {
113
126
 
114
127
  this.scores.set(peerId, currentScore);
115
128
  this.lastUpdateTime.set(peerId, currentTime);
129
+
130
+ this.maybeBanPeer(peerId, currentScore);
131
+
116
132
  return currentScore;
117
133
  }
118
134
 
135
+ /**
136
+ * Records a ban for a peer whose score has crossed the ban threshold, with an expiry banDurationMs
137
+ * in the future. No-op if the score is above the threshold or the peer is already serving an active
138
+ * ban (an existing ban is not extended; the original window stands). A previously expired ban does
139
+ * not block a fresh one — getActiveBanScore prunes it first.
140
+ */
141
+ private maybeBanPeer(peerId: string, score: number): void {
142
+ if (score >= MIN_SCORE_BEFORE_BAN || this.getActiveBanScore(peerId) !== undefined) {
143
+ return;
144
+ }
145
+ const record: BanRecord = { score, expiry: this.dateProvider.now() + this.banDurationMs };
146
+ this.bannedPeers.set(peerId, record);
147
+ this.logger.verbose(`Banning peer ${peerId} until ${new Date(record.expiry).toISOString()}`, {
148
+ peerId,
149
+ score,
150
+ expiry: record.expiry,
151
+ });
152
+ }
153
+
119
154
  decayAllScores(): void {
120
155
  const currentTime = this.dateProvider.now();
121
156
  for (const [peerId, lastUpdate] of this.lastUpdateTime.entries()) {
@@ -135,10 +170,24 @@ export class PeerScoring {
135
170
  }
136
171
  }
137
172
 
173
+ /**
174
+ * Removes bans whose window has elapsed. Expired bans are otherwise only pruned lazily when their
175
+ * peer's score is next queried, so a banned peer that disconnects and is never queried again would
176
+ * linger in the map. Called periodically (per heartbeat) to bound the ban map's size.
177
+ */
178
+ pruneExpiredBans(): void {
179
+ const now = this.dateProvider.now();
180
+ const expired = [...this.bannedPeers.entries()].filter(([, ban]) => ban.expiry <= now).map(([peerId]) => peerId);
181
+ for (const peerId of expired) {
182
+ this.bannedPeers.delete(peerId);
183
+ }
184
+ }
185
+
138
186
  /** Resets all peer scores. Useful for benchmarks to prevent cross-case contamination. */
139
187
  resetAllScores(): void {
140
188
  this.scores.clear();
141
189
  this.lastUpdateTime.clear();
190
+ this.bannedPeers.clear();
142
191
  }
143
192
 
144
193
  removePeer(peerId: string): void {
@@ -146,12 +195,33 @@ export class PeerScoring {
146
195
  this.lastUpdateTime.delete(peerId);
147
196
  }
148
197
 
198
+ /**
199
+ * The single source of truth for whether a peer is banned. Returns the ban score while the ban is
200
+ * active, or undefined if the peer is not banned. A ban that has expired is lazily lifted before
201
+ * returning undefined, so callers never see a stale ban.
202
+ */
203
+ private getActiveBanScore(peerId: string): number | undefined {
204
+ const ban = this.bannedPeers.get(peerId);
205
+ if (ban === undefined) {
206
+ return undefined;
207
+ }
208
+ if (ban.expiry > this.dateProvider.now()) {
209
+ return ban.score;
210
+ }
211
+ // Ban expired: lift it so the peer can recover.
212
+ this.bannedPeers.delete(peerId);
213
+ return undefined;
214
+ }
215
+
149
216
  getScore(peerId: string): number {
150
- return this.scores.get(peerId) || 0;
217
+ // While a ban is active its ban score is returned regardless of how the live score has decayed,
218
+ // so the peer stays banned for the full duration.
219
+ return this.getActiveBanScore(peerId) ?? this.scores.get(peerId) ?? 0;
151
220
  }
152
221
 
153
222
  public getScoreState(peerId: string): PeerScoreState {
154
- // TODO(#11329): permanently store banned peers?
223
+ // A banned peer stays banned for the full ban duration regardless of score decay (see getScore /
224
+ // maybeBanPeer), rather than silently recovering once its decayed score is cleaned up.
155
225
  const score = this.getScore(peerId);
156
226
  if (score < MIN_SCORE_BEFORE_BAN) {
157
227
  return PeerScoreState.Banned;
@@ -165,7 +235,9 @@ export class PeerScoring {
165
235
  getStats(): { medianScore: number; healthyCount: number; disconnectCount: number; bannedCount: number } {
166
236
  const stateCounts = { healthy: 0, disconnect: 0, banned: 0 };
167
237
 
168
- for (const peerId of this.scores.keys()) {
238
+ // Include banned peers whose live score may have been decayed away but whose ban is still active.
239
+ const peerIds = new Set([...this.scores.keys(), ...this.bannedPeers.keys()]);
240
+ for (const peerId of peerIds) {
169
241
  const state = this.getScoreState(peerId);
170
242
  switch (state) {
171
243
  case PeerScoreState.Healthy:
@@ -14,9 +14,8 @@ Applied before the protocol handler runs.
14
14
  | STATUS | 5/s | 10/s | same |
15
15
  | AUTH | 5/s | 10/s | same |
16
16
  | GOODBYE | 5/s | 10/s | same |
17
- | BLOCK | 2/s | 5/s | same |
18
17
  | BLOCK_TXS | 10/s | 200/s | same |
19
- | TX | (see rate limits file) | (see rate limits file) | same |
18
+ | TX | 10/s | 200/s | same |
20
19
 
21
20
  - Per-peer limit exceeded: `HighToleranceError` penalty + `RATE_LIMIT_EXCEEDED` status. Penalty fires inside `RequestResponseRateLimiter.allow()`, not the stream handler.
22
21
  - Global limit exceeded: `RATE_LIMIT_EXCEEDED` status only (no peer penalty).
@@ -149,26 +148,6 @@ Protected peers (private/trusted/preferred) are always considered "authenticated
149
148
 
150
149
  ## Block Data Protocols
151
150
 
152
- ### BLOCK Protocol (`/aztec/req/block/1.0.0`)
153
-
154
- **Server side**:
155
-
156
- | Rule | Consequence | File |
157
- |------|-------------|------|
158
- | Request must parse as `Fr` | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/block.ts` |
159
- | Block lookup throws | `INTERNAL_ERROR` status | same |
160
- | Block not found | SUCCESS + empty buffer (design choice; no `NOT_FOUND` status used) | same |
161
-
162
- **Requester side** (Snappy limit: 3 MB):
163
-
164
- | Rule | Consequence | File |
165
- |------|-------------|------|
166
- | Response block number must match requested | LowToleranceError; rejected | `libp2p_service.ts` (`validateRequestedBlock`) |
167
- | Local block must exist for hash verification | Rejected (no penalty) | same |
168
- | Response block hash must equal local block hash | MidToleranceError; rejected | same |
169
-
170
- **Limitation**: the local-block requirement means BLOCK req/resp is unusable for initial P2P-only sync (before L1 sync provides local copies for verification). A TODO in the code acknowledges this.
171
-
172
151
  ### BLOCK_TXS Protocol (`/aztec/req/block_txs/1.0.0`)
173
152
 
174
153
  **Server side**:
@@ -178,7 +157,7 @@ Protected peers (private/trusted/preferred) are always considered "authenticated
178
157
  | Request must parse as `BlockTxsRequest` (Fr + TxHashArray + BitVector) | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/block_txs/block_txs_handler.ts` |
179
158
  | BitVector length: non-negative and <= `MAX_TXS_PER_BLOCK` (65536) | Deserialization throws -> `BADLY_FORMED_REQUEST` | `protocols/block_txs/bitvector.ts` |
180
159
  | Archive root not found and no explicit txHashes | `NOT_FOUND` status | handler |
181
- | Internal error during lookup | Unhandled exception -> stream abort (no `INTERNAL_ERROR` status, unlike BLOCK) | handler |
160
+ | Internal error during lookup | Unhandled exception -> stream abort (no `INTERNAL_ERROR` status) | handler |
182
161
 
183
162
  Conditional registration: BLOCK_TXS handler only registered when `config.disableTransactions` is false. Otherwise peers get `ERR_UNSUPPORTED_PROTOCOL`.
184
163
 
@@ -5,6 +5,23 @@ export const DEFAULT_OVERALL_REQUEST_TIMEOUT_MS = 10_000; // Not currently used
5
5
  export const DEFAULT_REQRESP_DIAL_TIMEOUT_MS = 5_000;
6
6
  export const DEFAULT_OPTIMISTIC_NEGOTIATION = false;
7
7
 
8
+ /**
9
+ * Max size of a yamux frame, header included. This is the library default, pinned explicitly when configuring the
10
+ * muxer in the libp2p service so a dependency upgrade cannot silently change it.
11
+ */
12
+ export const YAMUX_MAX_MESSAGE_SIZE_BYTES = 64 * 1024;
13
+
14
+ /** Size of a yamux data frame header. */
15
+ const YAMUX_HEADER_LENGTH_BYTES = 12;
16
+
17
+ /**
18
+ * Max size of a reqresp request payload. A request must fit in a single muxer frame: the responder never reassembles
19
+ * a request from multiple chunks (see `ReqResp.processStream`), and yamux splits writes larger than one frame into
20
+ * multiple frames, each of which arrives as a separate chunk. mplex only splits at 1 MiB, so yamux is the binding
21
+ * constraint.
22
+ */
23
+ export const MAX_REQRESP_REQUEST_SIZE_BYTES = YAMUX_MAX_MESSAGE_SIZE_BYTES - YAMUX_HEADER_LENGTH_BYTES;
24
+
8
25
  // For use in tests.
9
26
  export const DEFAULT_P2P_REQRESP_CONFIG: P2PReqRespConfig = {
10
27
  overallRequestTimeoutMs: DEFAULT_OVERALL_REQUEST_TIMEOUT_MS,
@@ -9,7 +9,7 @@ export const DEFAULT_RATE_LIMITS: ReqRespSubProtocolRateLimits = {
9
9
  },
10
10
  globalLimit: {
11
11
  quotaTimeMs: 1000,
12
- quotaCount: 10,
12
+ quotaCount: 50,
13
13
  },
14
14
  },
15
15
  [ReqRespSubProtocol.STATUS]: {
@@ -19,7 +19,7 @@ export const DEFAULT_RATE_LIMITS: ReqRespSubProtocolRateLimits = {
19
19
  },
20
20
  globalLimit: {
21
21
  quotaTimeMs: 1000,
22
- quotaCount: 10,
22
+ quotaCount: 50,
23
23
  },
24
24
  },
25
25
  [ReqRespSubProtocol.AUTH]: {
@@ -49,7 +49,7 @@ export const DEFAULT_RATE_LIMITS: ReqRespSubProtocolRateLimits = {
49
49
  },
50
50
  globalLimit: {
51
51
  quotaTimeMs: 1000,
52
- quotaCount: 10,
52
+ quotaCount: 50,
53
53
  },
54
54
  },
55
55
  [ReqRespSubProtocol.BLOCK_TXS]: {
@@ -10,12 +10,18 @@ import type { Libp2p } from 'libp2p';
10
10
  import { pipeline } from 'node:stream/promises';
11
11
  import type { Uint8ArrayList } from 'uint8arraylist';
12
12
 
13
- import { IndividualReqRespTimeoutError } from '../../errors/reqresp.error.js';
14
- import { OversizedSnappyResponseError, SnappyTransform } from '../encoding.js';
13
+ import { IndividualReqRespTimeoutError, OversizedReqRespRequestError } from '../../errors/reqresp.error.js';
14
+ import {
15
+ DEFAULT_MAX_RESPONSE_SIZE_KB,
16
+ OversizedSnappyResponseError,
17
+ ResponseSizeLimitExceededError,
18
+ SnappyTransform,
19
+ } from '../encoding.js';
15
20
  import type { PeerScoring } from '../peer-manager/peer_scoring.js';
16
21
  import {
17
22
  DEFAULT_INDIVIDUAL_REQUEST_TIMEOUT_MS,
18
23
  DEFAULT_REQRESP_DIAL_TIMEOUT_MS,
24
+ MAX_REQRESP_REQUEST_SIZE_BYTES,
19
25
  type P2PReqRespConfig,
20
26
  } from './config.js';
21
27
  import { ConnectionSampler, RandomSampler } from './connection-sampler/connection_sampler.js';
@@ -207,6 +213,11 @@ export class ReqResp implements ReqRespInterface {
207
213
  payload: Buffer,
208
214
  dialTimeout: number = this.dialTimeoutMs,
209
215
  ): Promise<ReqRespResponse> {
216
+ // Thrown before the try block so the generic error handling below does not penalize the peer for our own bug.
217
+ if (payload.length > MAX_REQRESP_REQUEST_SIZE_BYTES) {
218
+ throw new OversizedReqRespRequestError(subProtocol, payload.length, MAX_REQRESP_REQUEST_SIZE_BYTES);
219
+ }
220
+
210
221
  let stream: Stream | undefined;
211
222
  try {
212
223
  this.metrics.recordRequestSent(subProtocol);
@@ -291,6 +302,14 @@ export class ReqResp implements ReqRespInterface {
291
302
  * - The first chunk should contain a control byte, indicating the status of the response see `ReqRespStatus`
292
303
  * - The second chunk should contain the response data
293
304
  *
305
+ * To bound memory usage we abort reception as soon as the accumulated compressed bytes exceed a
306
+ * bound derived from `maxSizeKb`, before the full stream has been buffered. Without this a peer we
307
+ * dialed could stream data up to the request timeout, forcing us to buffer arbitrarily much (the
308
+ * `maxSizeKb` guard in `inboundTransformData` only runs after the whole compressed payload is
309
+ * concatenated, so it does nothing to cap reception memory). Snappy can expand incompressible input
310
+ * to ~1.17x its size, so we allow 2x `maxSizeKb`: comfortably above the worst-case expansion of a
311
+ * legitimate max-size response, while still capping buffered memory at twice the permitted size.
312
+ *
294
313
  * @param source - The async iterable source of data chunks
295
314
  * @param maxSizeKb - Optional maximum expected size in KB for the decompressed response
296
315
  */
@@ -298,10 +317,20 @@ export class ReqResp implements ReqRespInterface {
298
317
  let status: ReqRespStatus | undefined;
299
318
  const chunks: Uint8Array[] = [];
300
319
 
320
+ const maxAccumulatedBytes = (maxSizeKb ?? DEFAULT_MAX_RESPONSE_SIZE_KB) * 1024 * 2;
321
+ let accumulatedBytes = 0;
322
+
301
323
  try {
302
324
  for await (const chunk of source) {
303
325
  const statusParsed = status !== undefined;
304
326
  if (statusParsed) {
327
+ // Use `byteLength` (the logical length) rather than `chunk.subarray().length`: calling
328
+ // `subarray()` on a `Uint8ArrayList` consolidates its backing buffers into a single copy,
329
+ // which would materialize the very chunk we are trying to reject.
330
+ accumulatedBytes += chunk.byteLength;
331
+ if (accumulatedBytes > maxAccumulatedBytes) {
332
+ throw new ResponseSizeLimitExceededError(accumulatedBytes, maxAccumulatedBytes);
333
+ }
305
334
  chunks.push(chunk.subarray());
306
335
  continue;
307
336
  }
@@ -440,6 +469,9 @@ export class ReqResp implements ReqRespInterface {
440
469
  await pipeline(
441
470
  stream.source,
442
471
  async function* (source: any) {
472
+ // Req/resp is one-request-one-response: an honest sender writes a single payload and half-closes.
473
+ // Process only the first chunk and return, so extra frames a peer queues on the same stream cannot
474
+ // drive further handler invocations that bypass the per-stream rate-limit check in streamHandler.
443
475
  for await (const chunk of source) {
444
476
  const response = await handler(connection.remotePeer, chunk.subarray());
445
477
 
@@ -454,6 +486,7 @@ export class ReqResp implements ReqRespInterface {
454
486
 
455
487
  yield SUCCESS;
456
488
  yield snappy.outboundTransformData(response);
489
+ return;
457
490
  }
458
491
  },
459
492
  stream.sink,
@@ -564,6 +597,13 @@ export class ReqResp implements ReqRespInterface {
564
597
  return PeerErrorSeverity.LowToleranceError;
565
598
  }
566
599
 
600
+ // Response exceeded the reception buffer bound: the peer streamed more compressed data than the
601
+ // per-request size limit allows. Same protocol violation as above, penalize harshly.
602
+ if (e instanceof ResponseSizeLimitExceededError) {
603
+ this.logger.warn(`Oversized response from peer ${peerId.toString()} in ${subProtocol}: ${e.message}`, logTags);
604
+ return PeerErrorSeverity.LowToleranceError;
605
+ }
606
+
567
607
  return this.categorizeConnectionErrors(e, peerId, subProtocol);
568
608
  }
569
609
 
@@ -58,6 +58,19 @@ export type DuplicateProposalInfo = {
58
58
  */
59
59
  export type P2PDuplicateProposalCallback = (info: DuplicateProposalInfo) => void;
60
60
 
61
+ /** Minimal info passed to the oversized proposal callback. */
62
+ export type OversizedProposalInfo = {
63
+ slot: SlotNumber;
64
+ proposer: EthAddress;
65
+ };
66
+
67
+ /**
68
+ * Callback for when a block proposal whose index lands at or beyond the consensus per-checkpoint block
69
+ * limit is stored and re-broadcast as slashing evidence. May fire multiple times per (slot, proposer)
70
+ * if the proposer signed several oversized proposals; consumers are expected to dedup.
71
+ */
72
+ export type P2POversizedProposalCallback = (info: OversizedProposalInfo) => void;
73
+
61
74
  /** Minimal info passed to the duplicate attestation callback. */
62
75
  export type DuplicateAttestationInfo = {
63
76
  slot: SlotNumber;
@@ -108,6 +121,12 @@ export interface P2PService {
108
121
  */
109
122
  registerDuplicateProposalCallback(callback: P2PDuplicateProposalCallback): void;
110
123
 
124
+ /**
125
+ * Registers a callback invoked when an oversized block proposal (index at or beyond the consensus
126
+ * per-checkpoint block limit) is stored and re-broadcast as slashing evidence.
127
+ */
128
+ registerOversizedProposalCallback(callback: P2POversizedProposalCallback): void;
129
+
111
130
  /**
112
131
  * Registers a callback invoked when a duplicate attestation is detected (equivocation).
113
132
  * A validator signing attestations for different proposals at the same slot.
@@ -30,7 +30,8 @@ export class NodeRpcTxSource implements TxSource {
30
30
  }
31
31
 
32
32
  public async getTxsByHash(txHashes: TxHash[]): Promise<TxSourceCollectionResult> {
33
- return this.verifyTxs(await this.client.getTxsByHash(txHashes));
33
+ // Collected txs feed block validation and proving, so we need their proofs.
34
+ return this.verifyTxs(await this.client.getTxsByHash(txHashes, { includeProof: true }));
34
35
  }
35
36
 
36
37
  private async verifyTxs(txs: Tx[]): Promise<TxSourceCollectionResult> {
@@ -119,6 +119,10 @@ export class InMemoryTxPool extends EventEmitter implements TxPoolV2 {
119
119
  return Promise.resolve();
120
120
  }
121
121
 
122
+ unprotectTxs(_txHashes: TxHash[], _slotNumber: SlotNumber): Promise<void> {
123
+ return Promise.resolve();
124
+ }
125
+
122
126
  handlePrunedBlocks(_latestBlock: L2BlockId, _options?: { deleteAllTxs?: boolean }): Promise<void> {
123
127
  return Promise.resolve();
124
128
  }
@@ -167,6 +171,10 @@ export class InMemoryTxPool extends EventEmitter implements TxPoolV2 {
167
171
  return Promise.resolve(this.txsByHash.size);
168
172
  }
169
173
 
174
+ async hasEligiblePendingTxs(minCount: number): Promise<boolean> {
175
+ return (await this.getPendingTxCount()) >= minCount;
176
+ }
177
+
170
178
  getMinedTxHashes(): Promise<[TxHash, L2BlockId][]> {
171
179
  return Promise.resolve([]);
172
180
  }
@@ -237,6 +245,10 @@ export class InMemoryAttestationPool {
237
245
  return Promise.resolve(this.checkpoints.get(slot)?.[0]);
238
246
  }
239
247
 
248
+ hasCheckpointProposalForSlot(slot: SlotNumber): Promise<boolean> {
249
+ return Promise.resolve(this.checkpoints.has(slot));
250
+ }
251
+
240
252
  getProposalsForSlot(slot: SlotNumber): Promise<{
241
253
  blockProposals: BlockProposal[];
242
254
  checkpointProposals: CheckpointProposalCore[];
@@ -17,6 +17,7 @@ import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
17
17
  import { protocolContractsHash } from '@aztec/protocol-contracts';
18
18
  import type { L2BlockSource } from '@aztec/stdlib/block';
19
19
  import type { ContractDataSource } from '@aztec/stdlib/contract';
20
+ import { EmptyL1RollupConstants, type L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
20
21
  import { GasFees } from '@aztec/stdlib/gas';
21
22
  import type { ClientProtocolCircuitVerifier, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
22
23
  import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
@@ -84,6 +85,25 @@ export interface BenchReadyMessage {
84
85
  }
85
86
  const txCache = new Map<number, Tx[]>();
86
87
 
88
+ const BENCH_L1_CONSTANTS: L1RollupConstants = {
89
+ ...EmptyL1RollupConstants,
90
+ slotDuration: 12,
91
+ ethereumSlotDuration: 4,
92
+ };
93
+
94
+ class TestbenchL2BlockSource extends MockL2BlockSource {
95
+ public override getL1Constants(): Promise<L1RollupConstants> {
96
+ return Promise.resolve(BENCH_L1_CONSTANTS);
97
+ }
98
+ }
99
+
100
+ function createBenchmarkEpochCache(): EpochCacheInterface {
101
+ return {
102
+ ...createMockEpochCache(),
103
+ getL1Constants: () => BENCH_L1_CONSTANTS,
104
+ };
105
+ }
106
+
87
107
  class TestLibP2PService extends LibP2PService {
88
108
  private disableTxValidation: boolean;
89
109
  private gossipMessageCount = 0;
@@ -370,9 +390,9 @@ process.on('message', async msg => {
370
390
  workerConfig = config;
371
391
  workerTxPool = new InMemoryTxPool();
372
392
  workerAttestationPool = new InMemoryAttestationPool();
373
- const epochCache = createMockEpochCache();
393
+ const epochCache = createBenchmarkEpochCache();
374
394
  const worldState = createMockWorldStateSynchronizer();
375
- const l2BlockSource = new MockL2BlockSource();
395
+ const l2BlockSource = new TestbenchL2BlockSource();
376
396
 
377
397
  const proofVerifier = new AlwaysTrueCircuitVerifier();
378
398
  kvStore = await openTmpStore(`test-${clientIndex}`, true, BENCHMARK_CONSTANTS.KV_STORE_MAP_SIZE_KB);
@@ -1,32 +0,0 @@
1
- import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
- import { SlotNumber } from '@aztec/foundation/branded-types';
3
- /**
4
- * Maximum clock disparity tolerance for P2P message validation (in milliseconds).
5
- * Messages for the previous slot are accepted if we're within this many milliseconds
6
- * of the current slot start. This prevents penalizing peers for messages that
7
- * were valid when sent but arrived slightly late due to network latency.
8
- *
9
- * This follows Ethereum's MAXIMUM_GOSSIP_CLOCK_DISPARITY approach.
10
- */
11
- export declare const MAXIMUM_GOSSIP_CLOCK_DISPARITY_MS = 500;
12
- /**
13
- * Checks if a message for the previous slot should be accepted due to clock tolerance.
14
- *
15
- * @param messageSlot - The slot number from the received message
16
- * @param currentSlot - The current slot number
17
- * @param epochCache - EpochCache to get timing information
18
- * @returns true if the message is for the previous slot AND we're within the clock tolerance window
19
- */
20
- export declare function isWithinClockTolerance(messageSlot: SlotNumber, currentSlot: SlotNumber, epochCache: EpochCacheInterface): boolean;
21
- export declare class PipeliningWindow {
22
- private readonly epochCache;
23
- private readonly proposalWindowIntoTargetSlot;
24
- private readonly attestationWindowIntoTargetSlot;
25
- constructor(epochCache: EpochCacheInterface, opts?: {
26
- p2pPropagationTime?: number;
27
- l1PublishingTime?: number;
28
- });
29
- acceptsProposal(messageSlot: SlotNumber): boolean;
30
- acceptsAttestation(messageSlot: SlotNumber): boolean;
31
- }
32
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xvY2tfdG9sZXJhbmNlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvbXNnX3ZhbGlkYXRvcnMvY2xvY2tfdG9sZXJhbmNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDOUQsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBRzdEOzs7Ozs7O0dBT0c7QUFDSCxlQUFPLE1BQU0saUNBQWlDLE1BQU0sQ0FBQztBQUVyRDs7Ozs7OztHQU9HO0FBQ0gsd0JBQWdCLHNCQUFzQixDQUNwQyxXQUFXLEVBQUUsVUFBVSxFQUN2QixXQUFXLEVBQUUsVUFBVSxFQUN2QixVQUFVLEVBQUUsbUJBQW1CLEdBQzlCLE9BQU8sQ0EwQlQ7QUFzQ0QscUJBQWEsZ0JBQWdCO0lBS3pCLE9BQU8sQ0FBQyxRQUFRLENBQUMsVUFBVTtJQUo3QixPQUFPLENBQUMsUUFBUSxDQUFDLDRCQUE0QixDQUFTO0lBQ3RELE9BQU8sQ0FBQyxRQUFRLENBQUMsK0JBQStCLENBQVM7SUFFekQsWUFDbUIsVUFBVSxFQUFFLG1CQUFtQixFQUNoRCxJQUFJLEdBQUU7UUFDSixrQkFBa0IsQ0FBQyxFQUFFLE1BQU0sQ0FBQztRQUM1QixnQkFBZ0IsQ0FBQyxFQUFFLE1BQU0sQ0FBQztLQUN0QixFQVlQO0lBRU0sZUFBZSxDQUFDLFdBQVcsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUV2RDtJQUVNLGtCQUFrQixDQUFDLFdBQVcsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUUxRDtDQUNGIn0=
@@ -1 +0,0 @@
1
- {"version":3,"file":"clock_tolerance.d.ts","sourceRoot":"","sources":["../../src/msg_validators/clock_tolerance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAG7D;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,MAAM,CAAC;AAErD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,UAAU,EACvB,WAAW,EAAE,UAAU,EACvB,UAAU,EAAE,mBAAmB,GAC9B,OAAO,CA0BT;AAsCD,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAJ7B,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAAS;IACtD,OAAO,CAAC,QAAQ,CAAC,+BAA+B,CAAS;IAEzD,YACmB,UAAU,EAAE,mBAAmB,EAChD,IAAI,GAAE;QACJ,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;KACtB,EAYP;IAEM,eAAe,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAEvD;IAEM,kBAAkB,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAE1D;CACF"}
@@ -1,90 +0,0 @@
1
- import { SlotNumber } from '@aztec/foundation/branded-types';
2
- import { DEFAULT_P2P_PROPAGATION_TIME, createPipelinedCheckpointTimingModel } from '@aztec/stdlib/timetable';
3
- /**
4
- * Maximum clock disparity tolerance for P2P message validation (in milliseconds).
5
- * Messages for the previous slot are accepted if we're within this many milliseconds
6
- * of the current slot start. This prevents penalizing peers for messages that
7
- * were valid when sent but arrived slightly late due to network latency.
8
- *
9
- * This follows Ethereum's MAXIMUM_GOSSIP_CLOCK_DISPARITY approach.
10
- */ export const MAXIMUM_GOSSIP_CLOCK_DISPARITY_MS = 500;
11
- /**
12
- * Checks if a message for the previous slot should be accepted due to clock tolerance.
13
- *
14
- * @param messageSlot - The slot number from the received message
15
- * @param currentSlot - The current slot number
16
- * @param epochCache - EpochCache to get timing information
17
- * @returns true if the message is for the previous slot AND we're within the clock tolerance window
18
- */ export function isWithinClockTolerance(messageSlot, currentSlot, epochCache) {
19
- // Guard against slot 0 edge case (genesis)
20
- if (currentSlot === SlotNumber.ZERO) {
21
- return false;
22
- }
23
- // Only apply tolerance to messages for the previous slot
24
- const previousSlot = SlotNumber(currentSlot - 1);
25
- if (messageSlot !== previousSlot) {
26
- return false;
27
- }
28
- // Check how far we are into the current slot (in milliseconds)
29
- const { ts: slotStartTs, nowMs } = epochCache.getEpochAndSlotNow();
30
- const targetSlot = epochCache.getTargetSlot();
31
- // Sanity check: ensure the epoch cache's target slot matches the expected current slot
32
- if (targetSlot !== currentSlot) {
33
- return false;
34
- }
35
- // ts is in seconds, convert to ms; nowMs is already in milliseconds
36
- const slotStartMs = slotStartTs * 1000n;
37
- const elapsedMs = Number(nowMs - slotStartMs);
38
- return elapsedMs < MAXIMUM_GOSSIP_CLOCK_DISPARITY_MS;
39
- }
40
- /**
41
- * Checks if a straggler message for the previous target slot should be accepted.
42
- *
43
- * Proposals and attestations carry the target slot N. Most of the time the receiver is either
44
- * still in the build slot N-1 (accepted via the main `slotNumber === targetSlot` match) or in the
45
- * target slot N (accepted again via `targetSlot`). Stragglers that arrive after the receiver has
46
- * rolled past the target slot fall to this check: accept `messageSlot === slotNow` while we're
47
- * still within the first `windowSeconds + clock-disparity` of the slot.
48
- *
49
- * Under the early-pipelining schedule `windowSeconds` is small (0 for proposals,
50
- * `2*p2pPropagationTime` for attestations) since the proposer collects everything
51
- * before the slot boundary.
52
- *
53
- * @param messageSlot - The slot number from the received message
54
- * @param epochCache - EpochCache to get timing state
55
- * @param windowSeconds - How far into the current slot we still accept previous-target messages
56
- * @returns true if the message is for the current wallclock slot and we're within the grace period
57
- */ function isWithinPipeliningWindow(messageSlot, epochCache, windowSeconds) {
58
- const currentSlot = epochCache.getSlotNow();
59
- if (messageSlot !== currentSlot) {
60
- return false;
61
- }
62
- const { ts: slotStartTs, nowMs } = epochCache.getEpochAndSlotNow();
63
- const slotStartMs = slotStartTs * 1000n;
64
- const elapsedMs = Number(nowMs - slotStartMs);
65
- const windowMs = windowSeconds * 1000 + MAXIMUM_GOSSIP_CLOCK_DISPARITY_MS;
66
- return elapsedMs < windowMs;
67
- }
68
- export class PipeliningWindow {
69
- epochCache;
70
- proposalWindowIntoTargetSlot;
71
- attestationWindowIntoTargetSlot;
72
- constructor(epochCache, opts = {}){
73
- this.epochCache = epochCache;
74
- const l1Constants = epochCache.getL1Constants();
75
- const checkpointTiming = createPipelinedCheckpointTimingModel({
76
- aztecSlotDuration: l1Constants.slotDuration,
77
- ethereumSlotDuration: l1Constants.ethereumSlotDuration,
78
- l1PublishingTime: opts.l1PublishingTime ?? l1Constants.ethereumSlotDuration,
79
- p2pPropagationTime: opts.p2pPropagationTime ?? DEFAULT_P2P_PROPAGATION_TIME
80
- });
81
- this.proposalWindowIntoTargetSlot = checkpointTiming.proposalWindowIntoTargetSlot;
82
- this.attestationWindowIntoTargetSlot = checkpointTiming.attestationWindowIntoTargetSlot;
83
- }
84
- acceptsProposal(messageSlot) {
85
- return isWithinPipeliningWindow(messageSlot, this.epochCache, this.proposalWindowIntoTargetSlot);
86
- }
87
- acceptsAttestation(messageSlot) {
88
- return isWithinPipeliningWindow(messageSlot, this.epochCache, this.attestationWindowIntoTargetSlot);
89
- }
90
- }