@aztec/p2p 0.0.1-commit.ffe5b04ea → 0.0.1-commit.fff30aa

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 (144) hide show
  1. package/README.md +129 -3
  2. package/dest/client/factory.d.ts +2 -2
  3. package/dest/client/factory.d.ts.map +1 -1
  4. package/dest/client/factory.js +23 -11
  5. package/dest/client/p2p_client.d.ts +1 -1
  6. package/dest/client/p2p_client.d.ts.map +1 -1
  7. package/dest/client/p2p_client.js +16 -6
  8. package/dest/config.d.ts +15 -3
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +21 -1
  11. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +1 -1
  12. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  13. package/dest/mem_pools/attestation_pool/attestation_pool.js +1 -5
  14. package/dest/mem_pools/instrumentation.d.ts +2 -4
  15. package/dest/mem_pools/instrumentation.d.ts.map +1 -1
  16. package/dest/mem_pools/instrumentation.js +14 -16
  17. package/dest/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.d.ts +1 -1
  18. package/dest/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.d.ts.map +1 -1
  19. package/dest/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.js +2 -1
  20. package/dest/mem_pools/tx_pool/priority.d.ts +2 -2
  21. package/dest/mem_pools/tx_pool/priority.d.ts.map +1 -1
  22. package/dest/mem_pools/tx_pool/priority.js +4 -4
  23. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts +1 -1
  24. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts.map +1 -1
  25. package/dest/mem_pools/tx_pool/tx_pool_test_suite.js +3 -1
  26. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.d.ts +1 -1
  27. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.d.ts.map +1 -1
  28. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +2 -1
  29. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +3 -3
  30. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  31. package/dest/mem_pools/tx_pool_v2/interfaces.js +0 -1
  32. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +14 -6
  33. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  34. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +7 -1
  35. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +1 -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 +43 -26
  38. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +4 -2
  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 +3 -3
  41. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +1 -2
  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 +11 -14
  44. package/dest/msg_validators/tx_validator/contract_instance_validator.d.ts +9 -0
  45. package/dest/msg_validators/tx_validator/contract_instance_validator.d.ts.map +1 -0
  46. package/dest/msg_validators/tx_validator/contract_instance_validator.js +48 -0
  47. package/dest/msg_validators/tx_validator/data_validator.d.ts +1 -1
  48. package/dest/msg_validators/tx_validator/data_validator.d.ts.map +1 -1
  49. package/dest/msg_validators/tx_validator/data_validator.js +35 -2
  50. package/dest/msg_validators/tx_validator/factory.d.ts +23 -4
  51. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  52. package/dest/msg_validators/tx_validator/factory.js +36 -10
  53. package/dest/msg_validators/tx_validator/gas_validator.d.ts +13 -4
  54. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  55. package/dest/msg_validators/tx_validator/gas_validator.js +39 -9
  56. package/dest/msg_validators/tx_validator/metadata_validator.d.ts +1 -1
  57. package/dest/msg_validators/tx_validator/metadata_validator.d.ts.map +1 -1
  58. package/dest/msg_validators/tx_validator/metadata_validator.js +4 -4
  59. package/dest/msg_validators/tx_validator/phases_validator.d.ts +21 -1
  60. package/dest/msg_validators/tx_validator/phases_validator.d.ts.map +1 -1
  61. package/dest/msg_validators/tx_validator/phases_validator.js +28 -1
  62. package/dest/services/encoding.d.ts +5 -1
  63. package/dest/services/encoding.d.ts.map +1 -1
  64. package/dest/services/encoding.js +7 -1
  65. package/dest/services/libp2p/libp2p_service.d.ts +1 -1
  66. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  67. package/dest/services/libp2p/libp2p_service.js +15 -5
  68. package/dest/services/peer-manager/metrics.d.ts +1 -3
  69. package/dest/services/peer-manager/metrics.d.ts.map +1 -1
  70. package/dest/services/peer-manager/metrics.js +0 -6
  71. package/dest/services/peer-manager/peer_manager.d.ts +1 -1
  72. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  73. package/dest/services/peer-manager/peer_manager.js +1 -2
  74. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts +1 -1
  75. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts.map +1 -1
  76. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.js +37 -14
  77. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts +11 -17
  78. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts.map +1 -1
  79. package/dest/services/reqresp/batch-tx-requester/peer_collection.js +15 -49
  80. package/dest/services/reqresp/rate-limiter/rate_limiter.d.ts +5 -4
  81. package/dest/services/reqresp/rate-limiter/rate_limiter.d.ts.map +1 -1
  82. package/dest/services/reqresp/rate-limiter/rate_limiter.js +10 -8
  83. package/dest/services/reqresp/reqresp.d.ts +1 -1
  84. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  85. package/dest/services/reqresp/reqresp.js +18 -11
  86. package/dest/services/tx_collection/file_store_tx_source.d.ts +5 -4
  87. package/dest/services/tx_collection/file_store_tx_source.d.ts.map +1 -1
  88. package/dest/services/tx_collection/file_store_tx_source.js +39 -29
  89. package/dest/services/tx_collection/tx_source.d.ts +6 -5
  90. package/dest/services/tx_collection/tx_source.d.ts.map +1 -1
  91. package/dest/services/tx_collection/tx_source.js +9 -7
  92. package/dest/test-helpers/make-test-p2p-clients.d.ts +1 -1
  93. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  94. package/dest/test-helpers/reqresp-nodes.d.ts +1 -1
  95. package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
  96. package/dest/test-helpers/testbench-utils.d.ts +1 -1
  97. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  98. package/dest/test-helpers/testbench-utils.js +2 -1
  99. package/dest/testbench/worker_client_manager.d.ts +1 -1
  100. package/dest/testbench/worker_client_manager.d.ts.map +1 -1
  101. package/dest/testbench/worker_client_manager.js +1 -2
  102. package/dest/util.d.ts +1 -1
  103. package/package.json +14 -14
  104. package/src/client/factory.ts +42 -15
  105. package/src/client/p2p_client.ts +16 -8
  106. package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +1 -1
  107. package/src/config.ts +35 -2
  108. package/src/mem_pools/attestation_pool/attestation_pool.ts +4 -5
  109. package/src/mem_pools/instrumentation.ts +13 -17
  110. package/src/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.ts +2 -1
  111. package/src/mem_pools/tx_pool/priority.ts +4 -4
  112. package/src/mem_pools/tx_pool/tx_pool_test_suite.ts +3 -1
  113. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +2 -1
  114. package/src/mem_pools/tx_pool_v2/interfaces.ts +2 -3
  115. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +15 -5
  116. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +43 -29
  117. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +13 -4
  118. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +12 -15
  119. package/src/msg_validators/attestation_validator/README.md +49 -0
  120. package/src/msg_validators/proposal_validator/README.md +123 -0
  121. package/src/msg_validators/tx_validator/README.md +5 -1
  122. package/src/msg_validators/tx_validator/contract_instance_validator.ts +56 -0
  123. package/src/msg_validators/tx_validator/data_validator.ts +42 -1
  124. package/src/msg_validators/tx_validator/factory.ts +43 -3
  125. package/src/msg_validators/tx_validator/gas_validator.ts +41 -8
  126. package/src/msg_validators/tx_validator/metadata_validator.ts +4 -12
  127. package/src/msg_validators/tx_validator/phases_validator.ts +31 -1
  128. package/src/services/encoding.ts +9 -1
  129. package/src/services/libp2p/libp2p_service.ts +16 -5
  130. package/src/services/peer-manager/metrics.ts +0 -7
  131. package/src/services/peer-manager/peer_manager.ts +1 -2
  132. package/src/services/reqresp/README.md +229 -0
  133. package/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +42 -14
  134. package/src/services/reqresp/batch-tx-requester/peer_collection.ts +24 -63
  135. package/src/services/reqresp/rate-limiter/rate_limiter.ts +13 -9
  136. package/src/services/reqresp/reqresp.ts +20 -14
  137. package/src/services/tx_collection/file_store_tx_source.ts +43 -31
  138. package/src/services/tx_collection/tx_source.ts +8 -7
  139. package/src/test-helpers/make-test-p2p-clients.ts +1 -1
  140. package/src/test-helpers/reqresp-nodes.ts +1 -1
  141. package/src/test-helpers/testbench-utils.ts +1 -0
  142. package/src/testbench/p2p_client_testbench_worker.ts +1 -1
  143. package/src/testbench/worker_client_manager.ts +1 -2
  144. package/src/util.ts +1 -1
@@ -0,0 +1,229 @@
1
+ # ReqResp Protocols
2
+
3
+ This module implements libp2p request-response protocols for the Aztec P2P network. All protocols share common transport-level validation (rate limiting, timeouts, Snappy decompression, error penalties) with protocol-specific logic layered on top.
4
+
5
+ ## Common Transport Validation
6
+
7
+ ### Rate Limiting (Responder Side)
8
+
9
+ Applied before the protocol handler runs.
10
+
11
+ | Protocol | Peer Limit | Global Limit | File |
12
+ |----------|-----------|-------------|------|
13
+ | PING | 5/s | 10/s | `rate-limiter/rate_limits.ts` |
14
+ | STATUS | 5/s | 10/s | same |
15
+ | AUTH | 5/s | 10/s | same |
16
+ | GOODBYE | 5/s | 10/s | same |
17
+ | BLOCK | 2/s | 5/s | same |
18
+ | BLOCK_TXS | 10/s | 200/s | same |
19
+ | TX | (see rate limits file) | (see rate limits file) | same |
20
+
21
+ - Per-peer limit exceeded: `HighToleranceError` penalty + `RATE_LIMIT_EXCEEDED` status. Penalty fires inside `RequestResponseRateLimiter.allow()`, not the stream handler.
22
+ - Global limit exceeded: `RATE_LIMIT_EXCEEDED` status only (no peer penalty).
23
+
24
+ ### Response Status Byte (Requester Side)
25
+
26
+ | Rule | Consequence | File |
27
+ |------|-------------|------|
28
+ | First chunk must be exactly 1 byte | `ReqRespStatusError(UNKNOWN)` | `status.ts` |
29
+ | Byte must be valid `ReqRespStatus` enum (0-4, 126, 127) | `ReqRespStatusError(UNKNOWN)` | same |
30
+
31
+ Note: `prettyPrintReqRespStatus` is missing a `NOT_FOUND` case (minor logging bug).
32
+
33
+ ### Snappy Decompression (Requester Side)
34
+
35
+ Per-protocol size limits checked via preamble before decompression.
36
+
37
+ ### Timeouts (Requester Side)
38
+
39
+ | Timeout | Default | Penalty |
40
+ |---------|---------|---------|
41
+ | Individual request | 10s | HighToleranceError |
42
+ | Dial | 5s | HighToleranceError |
43
+
44
+ ### Error Penalty Categorization (Requester Side)
45
+
46
+ | Error Type | Severity |
47
+ |------------|----------|
48
+ | GOODBYE subprotocol errors | None |
49
+ | `CollectiveReqRespTimeoutError` / `InvalidResponseError` | None |
50
+ | `AbortError` / connection close / muxer closed | None |
51
+ | `ECONNRESET` / `EPIPE` / `ECONNREFUSED` / `ERR_UNEXPECTED_EOF` | HighToleranceError |
52
+ | `ERR_UNSUPPORTED_PROTOCOL` | HighToleranceError |
53
+ | `IndividualReqRespTimeoutError` / `TimeoutError` | HighToleranceError |
54
+ | Catch-all | HighToleranceError |
55
+
56
+ ### Request Error Penalty (Responder Side)
57
+
58
+ | Error Type | Severity |
59
+ |------------|----------|
60
+ | `BADLY_FORMED_REQUEST` | LowToleranceError |
61
+ | All others | None |
62
+
63
+ ### Notes
64
+
65
+ - Request payloads are NOT snappy-compressed (asymmetric: only responses use snappy).
66
+
67
+ ---
68
+
69
+ ## Handshake Protocols
70
+
71
+ ### Connection-Level Gating (Before Any Handshake)
72
+
73
+ | Rule | Consequence | File |
74
+ |------|-------------|------|
75
+ | Deny inbound connection from IP/peerId with too many failed auth handshakes | Connection denied | `libp2p_service.ts` |
76
+ | Threshold: `p2pMaxFailedAuthAttemptsAllowed` (default 3) | Tracked per peerId AND per IP | `peer_manager.ts` |
77
+ | Failed auth entries expire after 1 hour | Peer can reconnect; no escalating penalty for repeat offenders | same |
78
+
79
+ ### Handshake Trigger Logic (`peer:connect`)
80
+
81
+ 1. `p2pDisableStatusHandshake` = true: no handshake
82
+ 2. `p2pAllowOnlyValidators` = false: STATUS handshake
83
+ 3. Peer is protected (trusted/private/preferred): STATUS handshake
84
+ 4. Otherwise: AUTH handshake (superset of STATUS)
85
+
86
+ Config constraint: `p2pDisableStatusHandshake && p2pAllowOnlyValidators` is disallowed.
87
+
88
+ ### STATUS Protocol (`/aztec/req/status/1.0.0`)
89
+
90
+ **Requester side** (`peer_manager.ts`):
91
+
92
+ | Rule | Consequence |
93
+ |------|-------------|
94
+ | Response status must be SUCCESS | Peer scheduled for disconnect |
95
+ | `compressedComponentsVersion` must match | Peer scheduled for disconnect |
96
+ | Any exception | Peer scheduled for disconnect |
97
+
98
+ `StatusMessage.validate()` currently only checks `compressedComponentsVersion`. Fields `latestBlockNumber`, `latestBlockHash`, `finalizedBlockNumber` are NOT validated (TODO in code).
99
+
100
+ **Responder side**: no validation of incoming request content (always responds with own status). This means the requester leaks its blockchain state to any peer before validation.
101
+
102
+ **Deserialization bounds**: `MAX_VERSION_STRING_LENGTH` = 64 bytes, `MAX_BLOCK_HASH_STRING_LENGTH` = 128 bytes. Expected response size: 1 KB.
103
+
104
+ ### AUTH Protocol (`/aztec/req/auth/1.0.0`)
105
+
106
+ **Requester side** (`peer_manager.ts`):
107
+
108
+ | # | Rule | Consequence |
109
+ |---|------|-------------|
110
+ | 1 | Response status is SUCCESS | `markAuthHandshakeFailed` + disconnect |
111
+ | 2 | `compressedComponentsVersion` match | `markAuthHandshakeFailed` + disconnect |
112
+ | 3 | Valid ECDSA signature recovery from challenge response | `markAuthHandshakeFailed` + disconnect |
113
+ | 4 | Recovered address is a registered validator | `markAuthHandshakeFailed` + disconnect |
114
+ | 5 | Validator address not already authenticated to different peerId | Silent return (no disconnect, no failure marking -- peer stays connected but unauthenticated) |
115
+ | 6 | Any exception | `markAuthHandshakeFailed` + disconnect |
116
+
117
+ Challenge: random `Fr`, payload = `keccak256("Aztec Validator Challenge:" + challenge)`, signed with `eth_sign` style. Challenge is NOT bound to peer identity (transport encryption via Noise is the binding layer).
118
+
119
+ On success: peer added to authenticated maps, prior failures cleared (including IP-based ones -- shared-IP peers benefit from a legitimate validator's success).
120
+
121
+ **Responder side** (`validator-client/src/validator.ts` + `peer_manager.ts`):
122
+
123
+ | # | Rule | Consequence |
124
+ |---|------|-------------|
125
+ | 1 | Peer must be protected (`shouldTrustWithIdentity` in `peer_manager.ts`) | Returns empty buffer (SUCCESS status + empty payload -> requester gets parse error -> `markAuthHandshakeFailed`) |
126
+ | 2 | Node must have registered validator address | Returns empty buffer (same consequence) |
127
+
128
+ **Unauthenticated peer gossip**: when `p2pAllowOnlyValidators` is true, unauthenticated peers get `appSpecificScore = -Infinity`, completely excluding them from all gossip.
129
+
130
+ ### PING Protocol (`/aztec/req/ping/1.0.0`)
131
+
132
+ No validation on either side. Responder returns `Buffer.from('pong')`. Expected response: 1 KB.
133
+
134
+ ### GOODBYE Protocol (`/aztec/req/goodbye/1.0.0`)
135
+
136
+ **Responder**: buffer must be 1 byte (defaults to `UNKNOWN` on invalid length). Goodbye reason byte is NOT validated against the enum -- any byte 0-255 accepted. Peer scheduled for disconnect regardless of reason.
137
+
138
+ **Requester**: response errors are never penalized (GOODBYE subprotocol exempt from error categorization).
139
+
140
+ ### Periodic Re-validation
141
+
142
+ | Rule | Interval | File |
143
+ |------|----------|------|
144
+ | Authenticated validators re-checked against current validator set | Every heartbeat (`peerCheckIntervalMS`) | `peer_manager.ts` |
145
+ | If validator address no longer registered, auth entry removed | Same | same |
146
+
147
+ Protected peers (private/trusted/preferred) are always considered "authenticated" without AUTH handshake.
148
+
149
+ ---
150
+
151
+ ## Block Data Protocols
152
+
153
+ ### BLOCK Protocol (`/aztec/req/block/1.0.0`)
154
+
155
+ **Server side**:
156
+
157
+ | Rule | Consequence | File |
158
+ |------|-------------|------|
159
+ | Request must parse as `Fr` | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/block.ts` |
160
+ | Block lookup throws | `INTERNAL_ERROR` status | same |
161
+ | Block not found | SUCCESS + empty buffer (design choice; no `NOT_FOUND` status used) | same |
162
+
163
+ **Requester side** (Snappy limit: 3 MB):
164
+
165
+ | Rule | Consequence | File |
166
+ |------|-------------|------|
167
+ | Response block number must match requested | LowToleranceError; rejected | `libp2p_service.ts` (`validateRequestedBlock`) |
168
+ | Local block must exist for hash verification | Rejected (no penalty) | same |
169
+ | Response block hash must equal local block hash | MidToleranceError; rejected | same |
170
+
171
+ **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.
172
+
173
+ ### BLOCK_TXS Protocol (`/aztec/req/block_txs/1.0.0`)
174
+
175
+ **Server side**:
176
+
177
+ | Rule | Consequence | File |
178
+ |------|-------------|------|
179
+ | Request must parse as `BlockTxsRequest` (Fr + TxHashArray + BitVector) | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/block_txs/block_txs_handler.ts` |
180
+ | BitVector length: non-negative and <= `MAX_TXS_PER_BLOCK` (65536) | Deserialization throws -> `BADLY_FORMED_REQUEST` | `protocols/block_txs/bitvector.ts` |
181
+ | Archive root not found and no explicit txHashes | `NOT_FOUND` status | handler |
182
+ | Internal error during lookup | Unhandled exception -> stream abort (no `INTERNAL_ERROR` status, unlike BLOCK) | handler |
183
+
184
+ Conditional registration: BLOCK_TXS handler only registered when `config.disableTransactions` is false. Otherwise peers get `ERR_UNSUPPORTED_PROTOCOL`.
185
+
186
+ **Requester side via `sendBatchRequest`** (Snappy limit: `max(N, 1) * 512 + 1` KB):
187
+
188
+ | Rule | Consequence | File |
189
+ |------|-------------|------|
190
+ | Archive root must match request | MidToleranceError | `libp2p_service.ts` (`validateRequestedBlockTxs`) |
191
+ | BitVector length must match request | MidToleranceError | same |
192
+ | No duplicate tx hashes | MidToleranceError | same |
193
+ | Tx count within bounds | MidToleranceError | same |
194
+ | Local block proposal must exist for archive root | Rejected (no penalty) | same |
195
+ | All tx hashes must be in proposal's tx list at allowed indices | LowToleranceError | same |
196
+ | Txs in strictly increasing index order | LowToleranceError | same |
197
+ | Each tx passes well-formedness (Metadata [4 fields], Size, Data, Proof) | LowToleranceError | same |
198
+
199
+ **Requester side via `BatchTxRequester`** (separate validation path):
200
+
201
+ | Rule | Consequence | File |
202
+ |------|-------------|------|
203
+ | Non-SUCCESS status: `FAILURE`/`UNKNOWN` | HighToleranceError + "bad peer" tracking | `batch-tx-requester/batch_tx_requester.ts` |
204
+ | `RATE_LIMIT_EXCEEDED` | Peer marked rate-limited (cooldown) | same |
205
+ | `NOT_FOUND` / `BADLY_FORMED_REQUEST` / `INTERNAL_ERROR` | Falls through silently (no penalty) | same |
206
+ | Each tx validated (Metadata + Size + Data + Proof) | LowToleranceError per invalid tx; valid txs from same response still accepted | same |
207
+ | Archive root match + non-empty txIndices | No penalty on mismatch; peer not promoted to "smart" | same |
208
+
209
+ **Double penalty on transport errors**: when `BatchTxRequester` encounters a transport error (e.g., ECONNRESET), both `sendRequestToPeer`'s internal handler and the `BatchTxRequester`'s catch block penalize the peer, resulting in double HighToleranceError.
210
+
211
+ See [BatchTxRequester README](batch-tx-requester/README.md) for the full architecture (peer classification, worker model, wire protocol).
212
+
213
+ ### TX Protocol (`/aztec/req/tx/1.0.0`)
214
+
215
+ **Server side**:
216
+
217
+ | Rule | Consequence | File |
218
+ |------|-------------|------|
219
+ | Request must parse as `TxHashArray` | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/tx.ts` |
220
+
221
+ **Requester side** (validator registered at startup, not the default noop):
222
+
223
+ | Rule | Consequence | File |
224
+ |------|-------------|------|
225
+ | Each returned tx hash must be in the requested set | MidToleranceError | `libp2p_service.ts` (`validateRequestedTxs`) |
226
+ | Each tx passes well-formedness (Metadata + Size + Data + Proof) | LowToleranceError | same |
227
+
228
+ Snappy limit: `max(N, 1) * 512 + 1` KB.
229
+
@@ -8,6 +8,7 @@ import { PeerErrorSeverity } from '@aztec/stdlib/p2p';
8
8
  import { Tx, TxArray, TxHash } from '@aztec/stdlib/tx';
9
9
 
10
10
  import type { PeerId } from '@libp2p/interface';
11
+ import { peerIdFromString } from '@libp2p/peer-id';
11
12
 
12
13
  import type { IMissingTxsTracker } from '../../tx_collection/missing_txs_tracker.js';
13
14
  import { ReqRespSubProtocol } from '.././interface.js';
@@ -89,9 +90,10 @@ export class BatchTxRequester {
89
90
  if (this.opts.peerCollection) {
90
91
  this.peers = this.opts.peerCollection;
91
92
  } else {
93
+ const initialPeers = this.p2pService.connectionSampler.getPeerListSortedByConnectionCountAsc();
92
94
  const badPeerThreshold = this.opts.badPeerThreshold ?? DEFAULT_BATCH_TX_REQUESTER_BAD_PEER_THRESHOLD;
93
95
  this.peers = new PeerCollection(
94
- this.p2pService.connectionSampler,
96
+ initialPeers,
95
97
  this.pinnedPeer,
96
98
  this.dateProvider,
97
99
  badPeerThreshold,
@@ -225,6 +227,7 @@ export class BatchTxRequester {
225
227
  * Starts dumb worker loops
226
228
  * */
227
229
  private async dumbRequester() {
230
+ const nextPeerIndex = this.makeRoundRobinIndexer();
228
231
  const nextBatchIndex = this.makeRoundRobinIndexer();
229
232
 
230
233
  // Chunk missing tx hashes into batches of txBatchSize, wrapping around to ensure no peer gets less than txBatchSize
@@ -260,9 +263,15 @@ export class BatchTxRequester {
260
263
  return { blockRequest, txs };
261
264
  };
262
265
 
263
- const workerCount = this.dumbParallelWorkerCount;
266
+ const nextPeer = () => {
267
+ const peers = this.peers.getDumbPeersToQuery();
268
+ const idx = nextPeerIndex(() => peers.length);
269
+ return idx === undefined ? undefined : peerIdFromString(peers[idx]);
270
+ };
271
+
272
+ const workerCount = Math.min(this.dumbParallelWorkerCount, this.peers.getAllPeers().size);
264
273
  const workers = Array.from({ length: workerCount }, (_, index) =>
265
- this.dumbWorkerLoop(this.peers.nextDumbPeerToQuery.bind(this.peers), makeRequest, index + 1),
274
+ this.dumbWorkerLoop(nextPeer, makeRequest, index + 1),
266
275
  );
267
276
 
268
277
  await Promise.allSettled(workers);
@@ -323,6 +332,14 @@ export class BatchTxRequester {
323
332
  * Starts smart worker loops
324
333
  * */
325
334
  private async smartRequester() {
335
+ const nextPeerIndex = this.makeRoundRobinIndexer();
336
+
337
+ const nextPeer = () => {
338
+ const peers = this.peers.getSmartPeersToQuery();
339
+ const idx = nextPeerIndex(() => peers.length);
340
+ return idx === undefined ? undefined : peerIdFromString(peers[idx]);
341
+ };
342
+
326
343
  const makeRequest = (pid: PeerId) => {
327
344
  const txs = this.txsMetadata.getTxsToRequestFromThePeer(pid);
328
345
  const blockRequest = BlockTxsRequest.fromTxsSourceAndMissingTxs(this.blockTxsSource, txs);
@@ -333,8 +350,9 @@ export class BatchTxRequester {
333
350
  return { blockRequest, txs };
334
351
  };
335
352
 
336
- const workers = Array.from({ length: this.smartParallelWorkerCount }, (_, index) =>
337
- this.smartWorkerLoop(this.peers.nextSmartPeerToQuery.bind(this.peers), makeRequest, index + 1),
353
+ const workers = Array.from(
354
+ { length: Math.min(this.smartParallelWorkerCount, this.peers.getAllPeers().size) },
355
+ (_, index) => this.smartWorkerLoop(nextPeer, makeRequest, index + 1),
338
356
  );
339
357
 
340
358
  await Promise.allSettled(workers);
@@ -369,18 +387,26 @@ export class BatchTxRequester {
369
387
  if (weRanOutOfPeersToQuery) {
370
388
  this.logger.debug(`Worker loop smart: No more peers to query`);
371
389
 
372
- // If we have rate limited peers wait for them.
373
- const nextSmartPeerDelay = this.peers.getNextSmartPeerAvailabilityDelayMs();
374
- const thereAreSomeRateLimitedSmartPeers = nextSmartPeerDelay !== undefined;
375
- if (thereAreSomeRateLimitedSmartPeers) {
376
- await this.sleepClampedToDeadline(nextSmartPeerDelay);
377
- continue;
390
+ // If there are no more dumb peers to query then none of our peers can become smart,
391
+ // thus we can simply exit this worker
392
+ const noMoreDumbPeersToQuery = this.peers.getDumbPeersToQuery().length === 0;
393
+ if (noMoreDumbPeersToQuery) {
394
+ // These might be either smart peers that will get unblocked after _some time_
395
+ const nextSmartPeerDelay = this.peers.getNextSmartPeerAvailabilityDelayMs();
396
+ const thereAreSomeRateLimitedSmartPeers = nextSmartPeerDelay !== undefined;
397
+ if (thereAreSomeRateLimitedSmartPeers) {
398
+ await this.sleepClampedToDeadline(nextSmartPeerDelay);
399
+ continue;
400
+ }
401
+
402
+ this.logger.debug(`Worker loop smart: No more smart peers to query killing ${workerIndex}`);
403
+ break;
378
404
  }
379
405
 
406
+ // Otherwise there are still some dumb peers that could become smart.
380
407
  // We end up here when all known smart peers became temporarily unavailable via combination of
381
408
  // (bad, in-flight, or rate-limited) or in some weird scenario all current smart peers turn bad which is permanent
382
- // but there are dumb peers that could be promoted
383
- // or new peer can join as dumb and be promoted later
409
+ // but dumb peers still exist that could become smart.
384
410
  //
385
411
  // When a dumb peer responds with valid txIndices, it gets
386
412
  // promoted to smart and releases the semaphore, waking this worker.
@@ -573,7 +599,9 @@ export class BatchTxRequester {
573
599
  this.markTxsPeerHas(peerId, response);
574
600
 
575
601
  // Unblock smart workers
576
- this.smartRequesterSemaphore.release();
602
+ if (this.peers.getSmartPeersToQuery().length <= this.smartParallelWorkerCount) {
603
+ this.smartRequesterSemaphore.release();
604
+ }
577
605
  }
578
606
 
579
607
  private isBlockResponseValid(response: BlockTxsResponse): boolean {
@@ -2,22 +2,18 @@ import type { DateProvider } from '@aztec/foundation/timer';
2
2
  import type { PeerErrorSeverity } from '@aztec/stdlib/p2p';
3
3
 
4
4
  import type { PeerId } from '@libp2p/interface';
5
- import { peerIdFromString } from '@libp2p/peer-id';
6
5
 
7
- import type { ConnectionSampler } from '../connection-sampler/connection_sampler.js';
8
6
  import { DEFAULT_BATCH_TX_REQUESTER_BAD_PEER_THRESHOLD } from './config.js';
9
7
  import type { IPeerPenalizer } from './interface.js';
10
8
 
11
9
  export const RATE_LIMIT_EXCEEDED_PEER_CACHE_TTL = 1000; // 1s
12
10
 
13
11
  export interface IPeerCollection {
12
+ getAllPeers(): Set<string>;
13
+ getSmartPeers(): Set<string>;
14
14
  markPeerSmart(peerId: PeerId): void;
15
-
16
- /** Sample next peer in round-robin fashion. No smart peers if returns undefined */
17
- nextSmartPeerToQuery(): PeerId | undefined;
18
- /** Sample next peer in round-robin fashion. No dumb peers if returns undefined */
19
- nextDumbPeerToQuery(): PeerId | undefined;
20
-
15
+ getSmartPeersToQuery(): Array<string>;
16
+ getDumbPeersToQuery(): Array<string>;
21
17
  thereAreSomeDumbRatelimitExceededPeers(): boolean;
22
18
  penalisePeer(peerId: PeerId, severity: PeerErrorSeverity): void;
23
19
  unMarkPeerAsBad(peerId: PeerId): void;
@@ -32,6 +28,8 @@ export interface IPeerCollection {
32
28
  }
33
29
 
34
30
  export class PeerCollection implements IPeerCollection {
31
+ private readonly peers;
32
+
35
33
  private readonly smartPeers = new Set<string>();
36
34
  private readonly inFlightPeers = new Set<string>();
37
35
  private readonly rateLimitExceededPeers = new Map<string, number>();
@@ -39,60 +37,46 @@ export class PeerCollection implements IPeerCollection {
39
37
  private readonly badPeers = new Set<string>();
40
38
 
41
39
  constructor(
42
- private readonly connectionSampler: Pick<ConnectionSampler, 'getPeerListSortedByConnectionCountAsc'>,
40
+ initialPeers: PeerId[],
43
41
  private readonly pinnedPeerId: PeerId | undefined,
44
42
  private readonly dateProvider: DateProvider,
45
43
  private readonly badPeerThreshold: number = DEFAULT_BATCH_TX_REQUESTER_BAD_PEER_THRESHOLD,
46
44
  private readonly peerPenalizer?: IPeerPenalizer,
47
45
  ) {
48
- // Pinned peer is treated specially, always mark it as in-flight
46
+ this.peers = new Set(initialPeers.map(peer => peer.toString()));
47
+
48
+ // Pinned peer is treaded specially, always mark it as in-flight
49
49
  // and never return it as part of smart/dumb peers
50
50
  if (this.pinnedPeerId) {
51
51
  const peerIdStr = this.pinnedPeerId.toString();
52
52
  this.inFlightPeers.add(peerIdStr);
53
+ this.peers.delete(peerIdStr);
53
54
  }
54
55
  }
55
56
 
56
- public markPeerSmart(peerId: PeerId): void {
57
- this.smartPeers.add(peerId.toString());
58
- }
59
-
60
- // We keep track of all peers that are queried for peer sampling algorithm
61
- private queriedSmartPeers: Set<string> = new Set<string>();
62
- private queriedDumbPeers: Set<string> = new Set<string>();
63
-
64
- private static nextPeer(allPeers: Set<string>, queried: Set<string>): PeerId | undefined {
65
- if (allPeers.size === 0) {
66
- return undefined;
67
- }
68
- const availablePeers = allPeers.difference(queried);
69
- let [first] = availablePeers;
70
- if (first === undefined) {
71
- // We queried all peers. Start over
72
- [first] = allPeers;
73
- queried.clear();
74
- }
75
- queried.add(first);
76
- return peerIdFromString(first);
57
+ public getAllPeers(): Set<string> {
58
+ return this.peers;
77
59
  }
78
60
 
79
- public nextSmartPeerToQuery(): PeerId | undefined {
80
- return PeerCollection.nextPeer(this.availableSmartPeers, this.queriedSmartPeers);
61
+ public getSmartPeers(): Set<string> {
62
+ return this.smartPeers;
81
63
  }
82
64
 
83
- public nextDumbPeerToQuery(): PeerId | undefined {
84
- return PeerCollection.nextPeer(this.availableDumbPeers, this.queriedDumbPeers);
65
+ public markPeerSmart(peerId: PeerId): void {
66
+ this.smartPeers.add(peerId.toString());
85
67
  }
86
68
 
87
- private get availableSmartPeers(): Set<string> {
88
- return this.peers.intersection(
69
+ public getSmartPeersToQuery(): Array<string> {
70
+ return Array.from(
89
71
  this.smartPeers.difference(this.getBadPeers().union(this.inFlightPeers).union(this.getRateLimitExceededPeers())),
90
72
  );
91
73
  }
92
74
 
93
- private get availableDumbPeers(): Set<string> {
94
- return this.peers.difference(
95
- this.smartPeers.union(this.getBadPeers()).union(this.inFlightPeers).union(this.getRateLimitExceededPeers()),
75
+ public getDumbPeersToQuery(): Array<string> {
76
+ return Array.from(
77
+ this.peers.difference(
78
+ this.smartPeers.union(this.getBadPeers()).union(this.inFlightPeers).union(this.getRateLimitExceededPeers()),
79
+ ),
96
80
  );
97
81
  }
98
82
 
@@ -218,27 +202,4 @@ export class PeerCollection implements IPeerCollection {
218
202
 
219
203
  return minExpiry! - now;
220
204
  }
221
-
222
- private orderedPeers: Set<string> = new Set();
223
-
224
- private get peers(): Set<string> {
225
- const pinnedStr = this.pinnedPeerId?.toString();
226
- const currentlyConnected = new Set(
227
- this.connectionSampler
228
- .getPeerListSortedByConnectionCountAsc()
229
- .map(p => p.toString())
230
- .filter(p => p !== pinnedStr),
231
- );
232
-
233
- // Remove disconnected peers, preserving order of the rest.
234
- this.orderedPeers = this.orderedPeers.intersection(currentlyConnected);
235
-
236
- // Append newly connected peers at the end (lowest priority).
237
- for (const peer of currentlyConnected) {
238
- if (!this.orderedPeers.has(peer)) {
239
- this.orderedPeers.add(peer);
240
- }
241
- }
242
- return this.orderedPeers;
243
- }
244
205
  }
@@ -97,9 +97,10 @@ export function prettyPrintRateLimitStatus(status: RateLimitStatus) {
97
97
  * 2. Individual rate limits for each peer.
98
98
  *
99
99
  * How it works:
100
- * - When a request comes in, it first checks against the global rate limit.
101
- * - If the global limit allows, it then checks against the specific peer's rate limit.
102
- * - The request is only allowed if both the global and peer-specific limits allow it.
100
+ * - When a request comes in, it first checks against the peer's individual rate limit.
101
+ * - If the peer limit allows, it then checks against the global rate limit.
102
+ * - The request is only allowed if both the peer-specific and global limits allow it.
103
+ * - Checking peer limit first ensures a rate-limited peer cannot exhaust the global quota.
103
104
  * - It automatically creates and manages rate limiters for new peers as they make requests.
104
105
  * - It periodically cleans up rate limiters for inactive peers to conserve memory.
105
106
  *
@@ -119,10 +120,6 @@ export class SubProtocolRateLimiter {
119
120
  }
120
121
 
121
122
  allow(peerId: PeerId): RateLimitStatus {
122
- if (!this.globalLimiter.allow()) {
123
- return RateLimitStatus.DeniedGlobal;
124
- }
125
-
126
123
  const peerIdStr = peerId.toString();
127
124
  let peerLimiter: PeerRateLimiter | undefined = this.peerLimiters.get(peerIdStr);
128
125
  if (!peerLimiter) {
@@ -135,10 +132,17 @@ export class SubProtocolRateLimiter {
135
132
  } else {
136
133
  peerLimiter.lastAccess = Date.now();
137
134
  }
138
- const peerLimitAllowed = peerLimiter.limiter.allow();
139
- if (!peerLimitAllowed) {
135
+
136
+ // Check peer limit first: a rate-limited peer must not consume global quota,
137
+ // otherwise one spamming peer can starve all others by exhausting the global bucket.
138
+ if (!peerLimiter.limiter.allow()) {
140
139
  return RateLimitStatus.DeniedPeer;
141
140
  }
141
+
142
+ if (!this.globalLimiter.allow()) {
143
+ return RateLimitStatus.DeniedGlobal;
144
+ }
145
+
142
146
  return RateLimitStatus.Allowed;
143
147
  }
144
148
 
@@ -16,7 +16,7 @@ import {
16
16
  IndividualReqRespTimeoutError,
17
17
  InvalidResponseError,
18
18
  } from '../../errors/reqresp.error.js';
19
- import { SnappyTransform } from '../encoding.js';
19
+ import { OversizedSnappyResponseError, SnappyTransform } from '../encoding.js';
20
20
  import type { PeerScoring } from '../peer-manager/peer_scoring.js';
21
21
  import {
22
22
  DEFAULT_INDIVIDUAL_REQUEST_TIMEOUT_MS,
@@ -462,7 +462,7 @@ export class ReqResp implements ReqRespInterface {
462
462
  );
463
463
  return resp;
464
464
  } catch (e: any) {
465
- this.logger.warn(`SUBPROTOCOL: ${subProtocol}\n`, e);
465
+ this.logger.debug(`SUBPROTOCOL: ${subProtocol}\n`, e);
466
466
  // On error we immediately abort the stream, this is preferred way,
467
467
  // because it signals to the sender that error happened, whereas
468
468
  // closing the stream only closes our side and is much slower
@@ -553,16 +553,10 @@ export class ReqResp implements ReqRespInterface {
553
553
  data: message,
554
554
  };
555
555
  } catch (e: any) {
556
+ // All errors (invalid status bytes, oversized snappy responses, corrupt data, etc.)
557
+ // are re-thrown so the caller can penalize the peer via handleResponseError.
556
558
  this.logger.debug(`Reading message failed: ${e.message}`);
557
-
558
- let status = ReqRespStatus.UNKNOWN;
559
- if (e instanceof ReqRespStatusError) {
560
- status = e.status;
561
- }
562
-
563
- return {
564
- status,
565
- };
559
+ throw e;
566
560
  }
567
561
  }
568
562
 
@@ -627,9 +621,7 @@ export class ReqResp implements ReqRespInterface {
627
621
  // and that this stream should be dropped
628
622
  const isMessageToNotWarn =
629
623
  err instanceof Error &&
630
- ['stream reset', 'Cannot push value onto an ended pushable', 'read ECONNRESET'].some(msg =>
631
- err.message.includes(msg),
632
- );
624
+ ['stream reset', 'Cannot push value onto an ended pushable'].some(msg => err.message.includes(msg));
633
625
  const level = isMessageToNotWarn ? 'debug' : 'warn';
634
626
  this.logger[level]('Unknown stream error while handling the stream, aborting', {
635
627
  protocol,
@@ -780,6 +772,20 @@ export class ReqResp implements ReqRespInterface {
780
772
  return undefined;
781
773
  }
782
774
 
775
+ // Invalid status byte: the peer sent a status byte that doesn't match any known status code.
776
+ // This is a protocol violation, penalize harshly.
777
+ if (e instanceof ReqRespStatusError) {
778
+ this.logger.warn(`Invalid status byte from peer ${peerId.toString()} in ${subProtocol}: ${e.message}`, logTags);
779
+ return PeerErrorSeverity.LowToleranceError;
780
+ }
781
+
782
+ // Oversized snappy response: the peer is sending data that exceeds the allowed size.
783
+ // This is a protocol violation that wastes bandwidth, so penalize harshly.
784
+ if (e instanceof OversizedSnappyResponseError) {
785
+ this.logger.warn(`Oversized response from peer ${peerId.toString()} in ${subProtocol}: ${e.message}`, logTags);
786
+ return PeerErrorSeverity.LowToleranceError;
787
+ }
788
+
783
789
  return this.categorizeConnectionErrors(e, peerId, subProtocol);
784
790
  }
785
791