@lodestar/reqresp 1.41.0-dev.aeb5a213ee → 1.41.0-dev.bb273175f2

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 (133) hide show
  1. package/README.md +1 -1
  2. package/lib/ReqResp.d.ts +3 -3
  3. package/lib/ReqResp.js +4 -4
  4. package/lib/ReqResp.js.map +1 -1
  5. package/lib/encoders/requestDecode.d.ts +2 -3
  6. package/lib/encoders/requestDecode.d.ts.map +1 -1
  7. package/lib/encoders/requestDecode.js +28 -11
  8. package/lib/encoders/requestDecode.js.map +1 -1
  9. package/lib/encoders/requestEncode.d.ts +1 -1
  10. package/lib/encoders/requestEncode.d.ts.map +1 -1
  11. package/lib/encoders/requestEncode.js +1 -1
  12. package/lib/encoders/requestEncode.js.map +1 -1
  13. package/lib/encoders/responseDecode.d.ts +10 -10
  14. package/lib/encoders/responseDecode.d.ts.map +1 -1
  15. package/lib/encoders/responseDecode.js +63 -60
  16. package/lib/encoders/responseDecode.js.map +1 -1
  17. package/lib/encoders/responseEncode.d.ts +2 -4
  18. package/lib/encoders/responseEncode.d.ts.map +1 -1
  19. package/lib/encoders/responseEncode.js +13 -22
  20. package/lib/encoders/responseEncode.js.map +1 -1
  21. package/lib/encodingStrategies/index.d.ts +4 -3
  22. package/lib/encodingStrategies/index.d.ts.map +1 -1
  23. package/lib/encodingStrategies/index.js +4 -4
  24. package/lib/encodingStrategies/index.js.map +1 -1
  25. package/lib/encodingStrategies/sszSnappy/decode.d.ts +5 -4
  26. package/lib/encodingStrategies/sszSnappy/decode.d.ts.map +1 -1
  27. package/lib/encodingStrategies/sszSnappy/decode.js +83 -52
  28. package/lib/encodingStrategies/sszSnappy/decode.js.map +1 -1
  29. package/lib/encodingStrategies/sszSnappy/encode.d.ts +2 -2
  30. package/lib/encodingStrategies/sszSnappy/encode.d.ts.map +1 -1
  31. package/lib/encodingStrategies/sszSnappy/encode.js +1 -1
  32. package/lib/encodingStrategies/sszSnappy/encode.js.map +1 -1
  33. package/lib/encodingStrategies/sszSnappy/errors.d.ts +0 -8
  34. package/lib/encodingStrategies/sszSnappy/errors.d.ts.map +1 -1
  35. package/lib/encodingStrategies/sszSnappy/errors.js +0 -2
  36. package/lib/encodingStrategies/sszSnappy/errors.js.map +1 -1
  37. package/lib/encodingStrategies/sszSnappy/index.d.ts +0 -1
  38. package/lib/encodingStrategies/sszSnappy/index.d.ts.map +1 -1
  39. package/lib/encodingStrategies/sszSnappy/index.js +0 -1
  40. package/lib/encodingStrategies/sszSnappy/index.js.map +1 -1
  41. package/lib/metrics.d.ts +1 -7
  42. package/lib/metrics.d.ts.map +1 -1
  43. package/lib/metrics.js +1 -17
  44. package/lib/metrics.js.map +1 -1
  45. package/lib/request/errors.d.ts +1 -7
  46. package/lib/request/errors.d.ts.map +1 -1
  47. package/lib/request/errors.js +1 -5
  48. package/lib/request/errors.js.map +1 -1
  49. package/lib/request/index.d.ts +0 -3
  50. package/lib/request/index.d.ts.map +1 -1
  51. package/lib/request/index.js +58 -70
  52. package/lib/request/index.js.map +1 -1
  53. package/lib/response/index.d.ts +1 -1
  54. package/lib/response/index.d.ts.map +1 -1
  55. package/lib/response/index.js +46 -50
  56. package/lib/response/index.js.map +1 -1
  57. package/lib/types.d.ts +1 -2
  58. package/lib/types.d.ts.map +1 -1
  59. package/lib/types.js +1 -2
  60. package/lib/types.js.map +1 -1
  61. package/lib/utils/collectMaxResponse.d.ts.map +1 -1
  62. package/lib/utils/collectMaxResponse.js +1 -2
  63. package/lib/utils/collectMaxResponse.js.map +1 -1
  64. package/lib/utils/errorMessage.d.ts +3 -3
  65. package/lib/utils/errorMessage.d.ts.map +1 -1
  66. package/lib/utils/errorMessage.js +14 -13
  67. package/lib/utils/errorMessage.js.map +1 -1
  68. package/lib/utils/index.d.ts +1 -3
  69. package/lib/utils/index.d.ts.map +1 -1
  70. package/lib/utils/index.js +1 -3
  71. package/lib/utils/index.js.map +1 -1
  72. package/lib/utils/protocolId.d.ts +2 -2
  73. package/lib/utils/protocolId.js +2 -2
  74. package/lib/utils/snappyCompress.d.ts +1 -1
  75. package/lib/utils/snappyCompress.d.ts.map +1 -1
  76. package/lib/utils/snappyCompress.js +1 -1
  77. package/lib/utils/snappyCompress.js.map +1 -1
  78. package/lib/utils/snappyIndex.d.ts +1 -1
  79. package/lib/utils/snappyIndex.d.ts.map +1 -1
  80. package/lib/utils/snappyIndex.js +1 -1
  81. package/lib/utils/snappyIndex.js.map +1 -1
  82. package/lib/utils/snappyUncompress.d.ts +7 -11
  83. package/lib/utils/snappyUncompress.d.ts.map +1 -1
  84. package/lib/utils/snappyUncompress.js +68 -68
  85. package/lib/utils/snappyUncompress.js.map +1 -1
  86. package/lib/utils/stream.d.ts +6 -0
  87. package/lib/utils/stream.d.ts.map +1 -0
  88. package/lib/utils/stream.js +21 -0
  89. package/lib/utils/stream.js.map +1 -0
  90. package/package.json +14 -16
  91. package/src/ReqResp.ts +4 -4
  92. package/src/encoders/requestDecode.ts +32 -16
  93. package/src/encoders/requestEncode.ts +1 -1
  94. package/src/encoders/responseDecode.ts +68 -72
  95. package/src/encoders/responseEncode.ts +17 -29
  96. package/src/encodingStrategies/index.ts +8 -6
  97. package/src/encodingStrategies/sszSnappy/decode.ts +111 -53
  98. package/src/encodingStrategies/sszSnappy/encode.ts +2 -2
  99. package/src/encodingStrategies/sszSnappy/errors.ts +0 -4
  100. package/src/encodingStrategies/sszSnappy/index.ts +0 -1
  101. package/src/metrics.ts +1 -17
  102. package/src/request/errors.ts +1 -6
  103. package/src/request/index.ts +74 -86
  104. package/src/response/index.ts +55 -61
  105. package/src/types.ts +1 -3
  106. package/src/utils/collectMaxResponse.ts +1 -2
  107. package/src/utils/errorMessage.ts +14 -13
  108. package/src/utils/index.ts +1 -3
  109. package/src/utils/protocolId.ts +2 -2
  110. package/src/utils/snappyCompress.ts +1 -1
  111. package/src/utils/snappyIndex.ts +1 -1
  112. package/src/utils/snappyUncompress.ts +73 -75
  113. package/src/utils/stream.ts +34 -0
  114. package/lib/utils/abortableSource.d.ts +0 -12
  115. package/lib/utils/abortableSource.d.ts.map +0 -1
  116. package/lib/utils/abortableSource.js +0 -69
  117. package/lib/utils/abortableSource.js.map +0 -1
  118. package/lib/utils/bufferedSource.d.ts +0 -16
  119. package/lib/utils/bufferedSource.d.ts.map +0 -1
  120. package/lib/utils/bufferedSource.js +0 -40
  121. package/lib/utils/bufferedSource.js.map +0 -1
  122. package/lib/utils/onChunk.d.ts +0 -6
  123. package/lib/utils/onChunk.d.ts.map +0 -1
  124. package/lib/utils/onChunk.js +0 -13
  125. package/lib/utils/onChunk.js.map +0 -1
  126. package/lib/utils/snappy.d.ts +0 -3
  127. package/lib/utils/snappy.d.ts.map +0 -1
  128. package/lib/utils/snappy.js +0 -3
  129. package/lib/utils/snappy.js.map +0 -1
  130. package/src/utils/abortableSource.ts +0 -80
  131. package/src/utils/bufferedSource.ts +0 -46
  132. package/src/utils/onChunk.ts +0 -12
  133. package/src/utils/snappy.ts +0 -2
@@ -1,31 +1,46 @@
1
+ import type {Stream} from "@libp2p/interface";
1
2
  import {PeerId} from "@libp2p/interface";
2
- import {pipe} from "it-pipe";
3
3
  import type {Libp2p} from "libp2p";
4
- import {Uint8ArrayList} from "uint8arraylist";
5
4
  import {ErrorAborted, Logger, TimeoutError, withTimeout} from "@lodestar/utils";
6
5
  import {requestEncode} from "../encoders/requestEncode.js";
7
6
  import {responseDecode} from "../encoders/responseDecode.js";
8
7
  import {Metrics} from "../metrics.js";
9
8
  import {ResponseError} from "../response/index.js";
10
9
  import {MixedProtocol, ResponseIncoming} from "../types.js";
11
- import {abortableSource, prettyPrintPeerId} from "../utils/index.js";
10
+ import {prettyPrintPeerId, sendChunks} from "../utils/index.js";
12
11
  import {RequestError, RequestErrorCode, responseStatusErrorToRequestError} from "./errors.js";
13
12
 
14
13
  export {RequestError, RequestErrorCode};
15
14
 
16
- // Default spec values from https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#configuration
15
+ // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/p2p-interface.md#the-reqresp-domain
17
16
  export const DEFAULT_DIAL_TIMEOUT = 5 * 1000; // 5 sec
18
17
  export const DEFAULT_REQUEST_TIMEOUT = 5 * 1000; // 5 sec
19
- export const DEFAULT_TTFB_TIMEOUT = 5 * 1000; // 5 sec
20
18
  export const DEFAULT_RESP_TIMEOUT = 10 * 1000; // 10 sec
21
19
 
20
+ function getStreamNotFullyConsumedError(): Error {
21
+ return new Error("ReqResp stream was not fully consumed");
22
+ }
23
+
24
+ function scheduleStreamAbortIfNotClosed(stream: Stream, timeoutMs: number): void {
25
+ const onClose = (): void => {
26
+ clearTimeout(timeout);
27
+ };
28
+
29
+ const timeout = setTimeout(() => {
30
+ stream.removeEventListener("close", onClose);
31
+ if (stream.status === "open" && stream.remoteWriteStatus === "writable") {
32
+ stream.abort(getStreamNotFullyConsumedError());
33
+ }
34
+ }, timeoutMs);
35
+
36
+ stream.addEventListener("close", onClose, {once: true});
37
+ }
38
+
22
39
  export interface SendRequestOpts {
23
40
  /** The maximum time for complete response transfer. */
24
41
  respTimeoutMs?: number;
25
42
  /** Non-spec timeout from sending request until write stream closed by responder */
26
43
  requestTimeoutMs?: number;
27
- /** The maximum time to wait for first byte of request response (time-to-first-byte). */
28
- ttfbTimeoutMs?: number;
29
44
  /** Non-spec timeout from dialing protocol until stream opened */
30
45
  dialTimeoutMs?: number;
31
46
  }
@@ -64,7 +79,6 @@ export async function* sendRequest(
64
79
 
65
80
  const DIAL_TIMEOUT = opts?.dialTimeoutMs ?? DEFAULT_DIAL_TIMEOUT;
66
81
  const REQUEST_TIMEOUT = opts?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT;
67
- const TTFB_TIMEOUT = opts?.ttfbTimeoutMs ?? DEFAULT_TTFB_TIMEOUT;
68
82
  const RESP_TIMEOUT = opts?.respTimeoutMs ?? DEFAULT_RESP_TIMEOUT;
69
83
 
70
84
  const peerIdStrShort = prettyPrintPeerId(peerId);
@@ -83,17 +97,6 @@ export async function* sendRequest(
83
97
  // the picked protocol in `connection.protocol`
84
98
  const protocolsMap = new Map<string, MixedProtocol>(protocols.map((protocol, i) => [protocolIDs[i], protocol]));
85
99
 
86
- // As of October 2020 we can't rely on libp2p.dialProtocol timeout to work so
87
- // this function wraps the dialProtocol promise with an extra timeout
88
- //
89
- // > The issue might be: you add the peer's addresses to the AddressBook,
90
- // which will result in autoDial to kick in and dial your peer. In parallel,
91
- // you do a manual dial and it will wait for the previous one without using
92
- // the abort signal:
93
- //
94
- // https://github.com/ChainSafe/lodestar/issues/1597#issuecomment-703394386
95
-
96
- // DIAL_TIMEOUT: Non-spec timeout from dialing protocol until stream opened
97
100
  const stream = await withTimeout(
98
101
  async (timeoutAndParentSignal) => {
99
102
  const protocolIds = Array.from(protocolsMap.keys());
@@ -112,9 +115,6 @@ export async function* sendRequest(
112
115
 
113
116
  metrics?.outgoingOpenedStreams?.inc({method});
114
117
 
115
- // TODO: Does the TTFB timer start on opening stream or after receiving request
116
- const timerTTFB = metrics?.outgoingResponseTTFB.startTimer({method});
117
-
118
118
  // Parse protocol selected by the responder
119
119
  const protocolId = stream.protocol ?? "unknown";
120
120
  const protocol = protocolsMap.get(protocolId);
@@ -126,21 +126,24 @@ export async function* sendRequest(
126
126
  logger.debug("Req sending request", logCtx);
127
127
 
128
128
  // Spec: The requester MUST close the write side of the stream once it finishes writing the request message
129
- // Impl: stream.sink is closed automatically by js-libp2p-mplex when piped source is exhausted
130
129
 
131
130
  // REQUEST_TIMEOUT: Non-spec timeout from sending request until write stream closed by responder
132
- // Note: libp2p.stop() will close all connections, so not necessary to abort this pipe on parent stop
133
- await withTimeout(() => pipe(requestEncode(protocol, requestBody), stream.sink), REQUEST_TIMEOUT, signal).catch(
134
- (e) => {
135
- // Must close the stream read side (stream.source) manually AND the write side
136
- stream.abort(e);
137
-
138
- if (e instanceof TimeoutError) {
139
- throw new RequestError({code: RequestErrorCode.REQUEST_TIMEOUT});
140
- }
141
- throw new RequestError({code: RequestErrorCode.REQUEST_ERROR, error: e as Error});
131
+ // Note: libp2p.stop() will close all connections, so not necessary to abort this send on parent stop
132
+ await withTimeout(
133
+ async (timeoutAndParentSignal) => {
134
+ await sendChunks(stream, requestEncode(protocol, requestBody), timeoutAndParentSignal);
135
+ await stream.close({signal: timeoutAndParentSignal});
136
+ },
137
+ REQUEST_TIMEOUT,
138
+ signal
139
+ ).catch((e) => {
140
+ stream.abort(e as Error);
141
+
142
+ if (e instanceof TimeoutError) {
143
+ throw new RequestError({code: RequestErrorCode.REQUEST_TIMEOUT});
142
144
  }
143
- );
145
+ throw new RequestError({code: RequestErrorCode.REQUEST_ERROR, error: e as Error});
146
+ });
144
147
 
145
148
  logger.debug("Req request sent", logCtx);
146
149
 
@@ -150,67 +153,52 @@ export async function* sendRequest(
150
153
  return;
151
154
  }
152
155
 
153
- // - TTFB_TIMEOUT: The requester MUST wait a maximum of TTFB_TIMEOUT for the first response byte to arrive
154
- // - RESP_TIMEOUT: Requester allows a further RESP_TIMEOUT for each subsequent response_chunk
155
- // - Max total timeout: This timeout is not required by the spec. It may not be necessary, but it's kept as
156
- // safe-guard to close. streams in case of bugs on other timeout mechanisms.
157
- const ttfbTimeoutController = new AbortController();
158
- const respTimeoutController = new AbortController();
156
+ // RESP_TIMEOUT: Maximum time for complete response transfer
157
+ const respSignal = signal
158
+ ? AbortSignal.any([signal, AbortSignal.timeout(RESP_TIMEOUT)])
159
+ : AbortSignal.timeout(RESP_TIMEOUT);
159
160
 
160
- let timeoutRESP: NodeJS.Timeout | null = null;
161
-
162
- const timeoutTTFB = setTimeout(() => {
163
- // If we abort on first byte delay, don't need to abort for response delay
164
- if (timeoutRESP) clearTimeout(timeoutRESP);
165
- ttfbTimeoutController.abort();
166
- }, TTFB_TIMEOUT);
167
-
168
- const restartRespTimeout = (): void => {
169
- if (timeoutRESP) clearTimeout(timeoutRESP);
170
- timeoutRESP = setTimeout(() => respTimeoutController.abort(), RESP_TIMEOUT);
171
- };
161
+ let responseError: Error | null = null;
162
+ let responseFullyConsumed = false;
172
163
 
173
164
  try {
174
- // Note: libp2p.stop() will close all connections, so not necessary to abort this pipe on parent stop
175
- yield* pipe(
176
- abortableSource(stream.source as AsyncIterable<Uint8ArrayList>, [
177
- {
178
- signal: ttfbTimeoutController.signal,
179
- getError: () => new RequestError({code: RequestErrorCode.TTFB_TIMEOUT}),
180
- },
181
- {
182
- signal: respTimeoutController.signal,
183
- getError: () => new RequestError({code: RequestErrorCode.RESP_TIMEOUT}),
184
- },
185
- ]),
186
-
187
- // Transforms `Buffer` chunks to yield `ResponseBody` chunks
188
- responseDecode(protocol, {
189
- onFirstHeader() {
190
- // On first byte, cancel the single use TTFB_TIMEOUT, and start RESP_TIMEOUT
191
- clearTimeout(timeoutTTFB);
192
- timerTTFB?.();
193
- restartRespTimeout();
194
- },
195
- onFirstResponseChunk() {
196
- // On <response_chunk>, cancel this chunk's RESP_TIMEOUT and start next's
197
- restartRespTimeout();
198
- },
199
- })
200
- );
165
+ yield* responseDecode(protocol, stream, {
166
+ signal: respSignal,
167
+ getError: () =>
168
+ signal?.aborted ? new ErrorAborted("sendRequest") : new RequestError({code: RequestErrorCode.RESP_TIMEOUT}),
169
+ });
170
+ responseFullyConsumed = true;
201
171
 
202
172
  // NOTE: Only log once per request to verbose, intermediate steps to debug
203
173
  // NOTE: Do not log the response, logs get extremely cluttered
204
174
  // NOTE: add double space after "Req " to align log with the "Resp " log
205
175
  logger.verbose("Req done", logCtx);
176
+ } catch (e) {
177
+ responseError = e as Error;
178
+ throw e;
206
179
  } finally {
207
- clearTimeout(timeoutTTFB);
208
- if (timeoutRESP !== null) clearTimeout(timeoutRESP);
209
-
210
- // Necessary to call `stream.close()` since collectResponses() may break out of the source before exhausting it
211
- // `stream.close()` libp2p-mplex will .end() the source (it-pushable instance)
212
- // If collectResponses() exhausts the source, it-pushable.end() can be safely called multiple times
213
- await stream.close();
180
+ // On decode/timeout failures abort immediately so mplex can reclaim stream state.
181
+ // On normal early consumer exit, close gracefully to avoid stream-id desync with peers.
182
+ if (responseError !== null || signal?.aborted) {
183
+ stream.abort(responseError ?? new ErrorAborted("sendRequest"));
184
+ } else {
185
+ await stream.close().catch((e) => {
186
+ stream.abort(e as Error);
187
+ });
188
+
189
+ if (!responseFullyConsumed) {
190
+ // Stop buffering unread inbound data after caller exits early.
191
+ // mplex does not support propagating closeRead to the remote, so still
192
+ // abort later if the remote never closes write.
193
+ await stream.closeRead().catch(() => {
194
+ // Ignore closeRead errors - close/abort path below will reclaim stream.
195
+ });
196
+
197
+ if (stream.remoteWriteStatus === "writable") {
198
+ scheduleStreamAbortIfNotClosed(stream, RESP_TIMEOUT);
199
+ }
200
+ }
201
+ }
214
202
  metrics?.outgoingClosedStreams?.inc({method});
215
203
  logger.verbose("Req stream closed", logCtx);
216
204
  }
@@ -1,5 +1,4 @@
1
1
  import {PeerId, Stream} from "@libp2p/interface";
2
- import {pipe} from "it-pipe";
3
2
  import {Logger, TimeoutError, withTimeout} from "@lodestar/utils";
4
3
  import {requestDecode} from "../encoders/requestDecode.js";
5
4
  import {responseEncodeError, responseEncodeSuccess} from "../encoders/responseEncode.js";
@@ -8,12 +7,12 @@ import {Metrics} from "../metrics.js";
8
7
  import {ReqRespRateLimiter} from "../rate_limiter/ReqRespRateLimiter.js";
9
8
  import {RequestError, RequestErrorCode} from "../request/errors.js";
10
9
  import {Protocol, ReqRespRequest} from "../types.js";
11
- import {prettyPrintPeerId} from "../utils/index.js";
10
+ import {prettyPrintPeerId, sendChunks} from "../utils/index.js";
12
11
  import {ResponseError} from "./errors.js";
13
12
 
14
13
  export {ResponseError};
15
14
 
16
- // Default spec values from https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#configuration
15
+ // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/p2p-interface.md#the-reqresp-domain
17
16
  export const DEFAULT_REQUEST_TIMEOUT = 5 * 1000; // 5 sec
18
17
 
19
18
  export interface HandleRequestOpts {
@@ -28,7 +27,7 @@ export interface HandleRequestOpts {
28
27
  requestId?: number;
29
28
  /** Peer client type for logging and metrics: 'prysm' | 'lighthouse' */
30
29
  peerClient?: string;
31
- /** Non-spec timeout from sending request until write stream closed by responder */
30
+ /** Timeout for reading the incoming request payload */
32
31
  requestTimeoutMs?: number;
33
32
  }
34
33
 
@@ -67,71 +66,66 @@ export async function handleRequest({
67
66
  metrics?.incomingOpenedStreams.inc({method: protocol.method});
68
67
 
69
68
  let responseError: Error | null = null;
70
- await pipe(
69
+ let streamSendError: Error | null = null;
70
+
71
+ try {
71
72
  // Yields success chunks and error chunks in the same generator
72
- // This syntax allows to recycle stream.sink to send success and error chunks without returning
73
+ // This syntax allows to recycle stream to send success and error chunks without returning
73
74
  // in case request whose body is a List fails at chunk_i > 0, without breaking out of the for..await..of
74
- (async function* requestHandlerSource() {
75
- try {
76
- // TODO: Does the TTFB timer start on opening stream or after receiving request
77
- const timerTTFB = metrics?.outgoingResponseTTFB.startTimer({method: protocol.method});
78
-
79
- const requestBody = await withTimeout(
80
- () => pipe(stream.source, requestDecode(protocol)),
81
- REQUEST_TIMEOUT,
82
- signal
83
- ).catch((e: unknown) => {
84
- if (e instanceof TimeoutError) {
85
- throw e; // Let outter catch (_e) {} re-type the error as SERVER_ERROR
75
+ await sendChunks(
76
+ stream,
77
+ (async function* requestHandlerSource() {
78
+ try {
79
+ const requestBody = await withTimeout(
80
+ (timeoutAndParentSignal) => requestDecode(protocol, stream, timeoutAndParentSignal),
81
+ REQUEST_TIMEOUT,
82
+ signal
83
+ ).catch((e: unknown) => {
84
+ if (e instanceof TimeoutError) {
85
+ throw e; // Let outer catch re-type the error as SERVER_ERROR
86
+ }
87
+ throw new ResponseError(RespStatus.INVALID_REQUEST, (e as Error).message);
88
+ });
89
+
90
+ logger.debug("Req received", logCtx);
91
+
92
+ // Max count by request for byRange and byRoot
93
+ const requestCount = protocol?.inboundRateLimits?.getRequestCount?.(requestBody) ?? 1;
94
+
95
+ if (!rateLimiter.allows(peerId, protocolID, requestCount)) {
96
+ throw new RequestError({code: RequestErrorCode.REQUEST_RATE_LIMITED});
86
97
  }
87
- throw new ResponseError(RespStatus.INVALID_REQUEST, (e as Error).message);
88
- });
89
98
 
90
- logger.debug("Req received", logCtx);
99
+ const requestChunk: ReqRespRequest = {
100
+ data: requestBody,
101
+ version: protocol.version,
102
+ };
91
103
 
92
- // Max count by request for byRange and byRoot
93
- const requestCount = protocol?.inboundRateLimits?.getRequestCount?.(requestBody) ?? 1;
104
+ yield* responseEncodeSuccess(protocol, protocol.handler(requestChunk, peerId, peerClient));
105
+ } catch (e) {
106
+ const status = e instanceof ResponseError ? e.status : RespStatus.SERVER_ERROR;
107
+ yield* responseEncodeError(protocol, status, (e as Error).message);
94
108
 
95
- if (!rateLimiter.allows(peerId, protocolID, requestCount)) {
96
- throw new RequestError({code: RequestErrorCode.REQUEST_RATE_LIMITED});
109
+ responseError = e as Error;
97
110
  }
111
+ })(),
112
+ signal
113
+ );
114
+ } catch (e) {
115
+ streamSendError = e as Error;
116
+ throw e;
117
+ } finally {
118
+ if (streamSendError) {
119
+ stream.abort(streamSendError);
120
+ } else {
121
+ await stream.close().catch((e) => {
122
+ stream.abort(e as Error);
123
+ });
124
+ }
125
+ metrics?.incomingClosedStreams.inc({method: protocol.method});
126
+ }
98
127
 
99
- const requestChunk: ReqRespRequest = {
100
- data: requestBody,
101
- version: protocol.version,
102
- };
103
-
104
- yield* pipe(
105
- // TODO: Debug the reason for type conversion here
106
- protocol.handler(requestChunk, peerId, peerClient),
107
- // NOTE: Do not log the resp chunk contents, logs get extremely cluttered
108
- // Note: Not logging on each chunk since after 1 year it hasn't add any value when debugging
109
- // onChunk(() => logger.debug("Resp sending chunk", logCtx)),
110
- responseEncodeSuccess(protocol, {
111
- onChunk(chunkIndex) {
112
- if (chunkIndex === 0) timerTTFB?.();
113
- },
114
- })
115
- );
116
- } catch (e) {
117
- const status = e instanceof ResponseError ? e.status : RespStatus.SERVER_ERROR;
118
- yield* responseEncodeError(protocol, status, (e as Error).message);
119
-
120
- // Should not throw an error here or libp2p-mplex throws with 'AbortError: stream reset'
121
- // throw e;
122
- responseError = e as Error;
123
- }
124
- })(),
125
- stream.sink
126
- );
127
-
128
- // If streak.sink throws, libp2p-mplex will close stream.source
129
- // If `requestDecode()` throws the stream.source must be closed manually
130
- // To ensure the stream.source it-pushable instance is always closed, stream.close() is called always
131
- await stream.close();
132
- metrics?.incomingClosedStreams.inc({method: protocol.method});
133
-
134
- // TODO: It may happen that stream.sink returns before returning stream.source first,
128
+ // TODO: It may happen that the response write completes before the request is fully read,
135
129
  // so you never see "Resp received request" in the logs and the response ends without
136
130
  // sending any chunk, triggering EMPTY_RESPONSE error on the requesting side
137
131
  // It has only happened when doing a request too fast upon immediate connection on inbound peer
package/src/types.ts CHANGED
@@ -4,11 +4,9 @@ import {ForkName} from "@lodestar/params";
4
4
  import {LodestarError} from "@lodestar/utils";
5
5
  import {RateLimiterQuota} from "./rate_limiter/rateLimiterGRCA.js";
6
6
 
7
- export const protocolPrefix = "/eth2/beacon_chain/req";
8
-
9
7
  /**
10
8
  * Available request/response encoding strategies:
11
- * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#encoding-strategies
9
+ * https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/p2p-interface.md#encoding-strategies
12
10
  */
13
11
  export enum Encoding {
14
12
  SSZ_SNAPPY = "ssz_snappy",
@@ -6,12 +6,11 @@
6
6
  * Collects a bounded list of responses up to `maxResponses`
7
7
  */
8
8
  export async function collectMaxResponse<T>(source: AsyncIterable<T>, maxResponses: number): Promise<T[]> {
9
- // else: zero or more responses
10
9
  const responses: T[] = [];
11
10
  for await (const response of source) {
12
11
  responses.push(response);
13
12
 
14
- if (maxResponses !== undefined && responses.length >= maxResponses) {
13
+ if (responses.length >= maxResponses) {
15
14
  break;
16
15
  }
17
16
  }
@@ -1,8 +1,7 @@
1
1
  import {decode as varintDecode, encodingLength as varintEncodingLength} from "uint8-varint";
2
- import {Uint8ArrayList} from "uint8arraylist";
3
2
  import {writeSszSnappyPayload} from "../encodingStrategies/sszSnappy/encode.js";
4
3
  import {Encoding} from "../types.js";
5
- import {SnappyFramesUncompress} from "./snappyIndex.js";
4
+ import {decodeSnappyFrames} from "./snappyIndex.js";
6
5
 
7
6
  // ErrorMessage schema:
8
7
  //
@@ -13,18 +12,21 @@ import {SnappyFramesUncompress} from "./snappyIndex.js";
13
12
  // By convention, the error_message is a sequence of bytes that MAY be interpreted as a
14
13
  // UTF-8 string (for debugging purposes). Clients MUST treat as valid any byte sequences
15
14
  //
16
- // Spec v1.1.10 https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#responding-side
15
+ // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/p2p-interface.md#responding-side
17
16
 
18
17
  /**
19
18
  * Encodes a UTF-8 string to 256 bytes max
20
19
  */
21
- export async function* encodeErrorMessage(errorMessage: string, encoding: Encoding): AsyncGenerator<Buffer> {
20
+ export function* encodeErrorMessage(errorMessage: string, encoding: Encoding): Generator<Buffer> {
22
21
  const encoder = new TextEncoder();
23
22
  const bytes = encoder.encode(errorMessage).slice(0, 256);
24
23
 
25
24
  switch (encoding) {
26
25
  case Encoding.SSZ_SNAPPY:
27
26
  yield* writeSszSnappyPayload(bytes);
27
+ break;
28
+ default:
29
+ throw Error("Unsupported encoding");
28
30
  }
29
31
  }
30
32
 
@@ -32,9 +34,9 @@ export async function* encodeErrorMessage(errorMessage: string, encoding: Encodi
32
34
  * Encodes a UTF-8 error message string into a single buffer (max 256 bytes before encoding).
33
35
  * Unlike `encodeErrorMessage`, this collects all encoded chunks into one buffer.
34
36
  */
35
- export async function encodeErrorMessageToBuffer(errorMessage: string, encoding: Encoding): Promise<Buffer> {
37
+ export function encodeErrorMessageToBuffer(errorMessage: string, encoding: Encoding): Buffer {
36
38
  const chunks: Buffer[] = [];
37
- for await (const chunk of encodeErrorMessage(errorMessage, encoding)) {
39
+ for (const chunk of encodeErrorMessage(errorMessage, encoding)) {
38
40
  chunks.push(chunk);
39
41
  }
40
42
  return Buffer.concat(chunks);
@@ -43,21 +45,20 @@ export async function encodeErrorMessageToBuffer(errorMessage: string, encoding:
43
45
  /**
44
46
  * Decodes error message from network bytes and removes non printable, non ascii characters.
45
47
  */
46
- export async function decodeErrorMessage(encodedErrorMessage: Uint8Array): Promise<string> {
47
- const encoder = new TextDecoder();
48
+ export function decodeErrorMessage(encodedErrorMessage: Uint8Array): string {
49
+ const decoder = new TextDecoder();
48
50
  let sszDataLength: number;
49
51
  try {
50
52
  sszDataLength = varintDecode(encodedErrorMessage);
51
- const decompressor = new SnappyFramesUncompress();
52
53
  const varintBytes = varintEncodingLength(sszDataLength);
53
- const errorMessage = decompressor.uncompress(new Uint8ArrayList(encodedErrorMessage.subarray(varintBytes)));
54
- if (errorMessage == null || errorMessage.length !== sszDataLength) {
54
+ const errorMessage = decodeSnappyFrames(encodedErrorMessage.subarray(varintBytes));
55
+ if (errorMessage.length !== sszDataLength) {
55
56
  throw new Error("Malformed input: data length mismatch");
56
57
  }
57
58
  // remove non ascii characters from string
58
- return encoder.decode(errorMessage.subarray(0)).replace(/[^\x20-\x7F]/g, "");
59
+ return decoder.decode(errorMessage.subarray(0)).replace(/[^\x20-\x7F]/g, "");
59
60
  } catch (_e) {
60
61
  // remove non ascii characters from string
61
- return encoder.decode(encodedErrorMessage.slice(0, 256)).replace(/[^\x20-\x7F]/g, "");
62
+ return decoder.decode(encodedErrorMessage.slice(0, 256)).replace(/[^\x20-\x7F]/g, "");
62
63
  }
63
64
  }
@@ -1,9 +1,7 @@
1
- export * from "./abortableSource.js";
2
- export * from "./bufferedSource.js";
3
1
  export * from "./collectExactOne.js";
4
2
  export * from "./collectMaxResponse.js";
5
3
  export * from "./errorMessage.js";
6
- export * from "./onChunk.js";
7
4
  export * from "./peerId.js";
8
5
  export * from "./protocolId.js";
9
6
  export * from "./snappyIndex.js";
7
+ export * from "./stream.js";
@@ -1,14 +1,14 @@
1
1
  import {Encoding, ProtocolAttributes} from "../types.js";
2
2
 
3
3
  /**
4
- * https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#protocol-identification
4
+ * https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/p2p-interface.md#protocol-identification
5
5
  */
6
6
  export function formatProtocolID(protocolPrefix: string, method: string, version: number, encoding: Encoding): string {
7
7
  return `${protocolPrefix}/${method}/${version}/${encoding}`;
8
8
  }
9
9
 
10
10
  /**
11
- * https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#protocol-identification
11
+ * https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/p2p-interface.md#protocol-identification
12
12
  */
13
13
  export function parseProtocolID(protocolId: string): ProtocolAttributes {
14
14
  const result = protocolId.split("/");
@@ -3,7 +3,7 @@ import {compressSync} from "snappy";
3
3
  import {ChunkType, IDENTIFIER_FRAME, UNCOMPRESSED_CHUNK_SIZE, crc} from "./snappyCommon.js";
4
4
 
5
5
  // The logic in this file is largely copied (in simplified form) from https://github.com/ChainSafe/node-snappy-stream/
6
- export async function* encodeSnappy(bytes: Buffer): AsyncGenerator<Buffer> {
6
+ export function* encodeSnappy(bytes: Buffer): Generator<Buffer> {
7
7
  yield IDENTIFIER_FRAME;
8
8
 
9
9
  for (let i = 0; i < bytes.length; i += UNCOMPRESSED_CHUNK_SIZE) {
@@ -1,3 +1,3 @@
1
1
  export * from "./snappyCommon.js";
2
2
  export {encodeSnappy} from "./snappyCompress.js";
3
- export {SnappyFramesUncompress} from "./snappyUncompress.js";
3
+ export * from "./snappyUncompress.js";