@lodestar/reqresp 1.35.0-dev.f80d2d52da → 1.35.0-dev.fd1dac853d

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 (96) hide show
  1. package/lib/ReqResp.d.ts +1 -1
  2. package/lib/ReqResp.d.ts.map +1 -0
  3. package/lib/ReqResp.js +16 -6
  4. package/lib/ReqResp.js.map +1 -1
  5. package/lib/encoders/requestDecode.d.ts.map +1 -0
  6. package/lib/encoders/requestEncode.d.ts.map +1 -0
  7. package/lib/encoders/responseDecode.d.ts +1 -1
  8. package/lib/encoders/responseDecode.d.ts.map +1 -0
  9. package/lib/encoders/responseDecode.js.map +1 -1
  10. package/lib/encoders/responseEncode.d.ts.map +1 -0
  11. package/lib/encodingStrategies/index.d.ts.map +1 -0
  12. package/lib/encodingStrategies/sszSnappy/decode.d.ts.map +1 -0
  13. package/lib/encodingStrategies/sszSnappy/encode.d.ts.map +1 -0
  14. package/lib/encodingStrategies/sszSnappy/errors.d.ts.map +1 -0
  15. package/lib/encodingStrategies/sszSnappy/index.d.ts +1 -1
  16. package/lib/encodingStrategies/sszSnappy/index.d.ts.map +1 -0
  17. package/lib/encodingStrategies/sszSnappy/index.js +1 -1
  18. package/lib/encodingStrategies/sszSnappy/snappyFrames/common.d.ts.map +1 -0
  19. package/lib/encodingStrategies/sszSnappy/snappyFrames/compress.d.ts.map +1 -0
  20. package/lib/encodingStrategies/sszSnappy/snappyFrames/uncompress.d.ts.map +1 -0
  21. package/lib/encodingStrategies/sszSnappy/snappyFrames/uncompress.js +4 -6
  22. package/lib/encodingStrategies/sszSnappy/snappyFrames/uncompress.js.map +1 -1
  23. package/lib/encodingStrategies/sszSnappy/utils.d.ts.map +1 -0
  24. package/lib/index.d.ts +7 -7
  25. package/lib/index.d.ts.map +1 -0
  26. package/lib/index.js +5 -5
  27. package/lib/index.js.map +1 -1
  28. package/lib/interface.d.ts.map +1 -0
  29. package/lib/metrics.d.ts.map +1 -0
  30. package/lib/rate_limiter/ReqRespRateLimiter.d.ts.map +1 -0
  31. package/lib/rate_limiter/ReqRespRateLimiter.js +8 -4
  32. package/lib/rate_limiter/ReqRespRateLimiter.js.map +1 -1
  33. package/lib/rate_limiter/rateLimiterGRCA.d.ts.map +1 -0
  34. package/lib/rate_limiter/rateLimiterGRCA.js +5 -3
  35. package/lib/rate_limiter/rateLimiterGRCA.js.map +1 -1
  36. package/lib/rate_limiter/selfRateLimiter.d.ts.map +1 -0
  37. package/lib/rate_limiter/selfRateLimiter.js +9 -2
  38. package/lib/rate_limiter/selfRateLimiter.js.map +1 -1
  39. package/lib/request/errors.d.ts.map +1 -0
  40. package/lib/request/index.d.ts +1 -1
  41. package/lib/request/index.d.ts.map +1 -0
  42. package/lib/request/index.js +1 -1
  43. package/lib/request/index.js.map +1 -1
  44. package/lib/response/errors.d.ts.map +1 -0
  45. package/lib/response/errors.js +2 -0
  46. package/lib/response/errors.js.map +1 -1
  47. package/lib/response/index.d.ts.map +1 -0
  48. package/lib/response/index.js +1 -1
  49. package/lib/response/index.js.map +1 -1
  50. package/lib/types.d.ts.map +1 -0
  51. package/lib/utils/abortableSource.d.ts.map +1 -0
  52. package/lib/utils/bufferedSource.d.ts.map +1 -0
  53. package/lib/utils/bufferedSource.js +3 -1
  54. package/lib/utils/bufferedSource.js.map +1 -1
  55. package/lib/utils/collectExactOne.d.ts.map +1 -0
  56. package/lib/utils/collectMaxResponse.d.ts.map +1 -0
  57. package/lib/utils/errorMessage.d.ts.map +1 -0
  58. package/lib/utils/index.d.ts.map +1 -0
  59. package/lib/utils/onChunk.d.ts.map +1 -0
  60. package/lib/utils/peerId.d.ts.map +1 -0
  61. package/lib/utils/protocolId.d.ts.map +1 -0
  62. package/package.json +12 -12
  63. package/src/ReqResp.ts +289 -0
  64. package/src/encoders/requestDecode.ts +29 -0
  65. package/src/encoders/requestEncode.ts +18 -0
  66. package/src/encoders/responseDecode.ts +169 -0
  67. package/src/encoders/responseEncode.ts +81 -0
  68. package/src/encodingStrategies/index.ts +46 -0
  69. package/src/encodingStrategies/sszSnappy/decode.ts +111 -0
  70. package/src/encodingStrategies/sszSnappy/encode.ts +24 -0
  71. package/src/encodingStrategies/sszSnappy/errors.ts +31 -0
  72. package/src/encodingStrategies/sszSnappy/index.ts +3 -0
  73. package/src/encodingStrategies/sszSnappy/snappyFrames/common.ts +36 -0
  74. package/src/encodingStrategies/sszSnappy/snappyFrames/compress.ts +25 -0
  75. package/src/encodingStrategies/sszSnappy/snappyFrames/uncompress.ts +114 -0
  76. package/src/encodingStrategies/sszSnappy/utils.ts +7 -0
  77. package/src/index.ts +10 -0
  78. package/src/interface.ts +26 -0
  79. package/src/metrics.ts +95 -0
  80. package/src/rate_limiter/ReqRespRateLimiter.ts +107 -0
  81. package/src/rate_limiter/rateLimiterGRCA.ts +92 -0
  82. package/src/rate_limiter/selfRateLimiter.ts +112 -0
  83. package/src/request/errors.ts +119 -0
  84. package/src/request/index.ts +225 -0
  85. package/src/response/errors.ts +50 -0
  86. package/src/response/index.ts +147 -0
  87. package/src/types.ts +158 -0
  88. package/src/utils/abortableSource.ts +80 -0
  89. package/src/utils/bufferedSource.ts +46 -0
  90. package/src/utils/collectExactOne.ts +15 -0
  91. package/src/utils/collectMaxResponse.ts +19 -0
  92. package/src/utils/errorMessage.ts +51 -0
  93. package/src/utils/index.ts +8 -0
  94. package/src/utils/onChunk.ts +12 -0
  95. package/src/utils/peerId.ts +6 -0
  96. package/src/utils/protocolId.ts +44 -0
@@ -0,0 +1,147 @@
1
+ import {PeerId, Stream} from "@libp2p/interface";
2
+ import {pipe} from "it-pipe";
3
+ import {Uint8ArrayList} from "uint8arraylist";
4
+ import {Logger, TimeoutError, withTimeout} from "@lodestar/utils";
5
+ import {requestDecode} from "../encoders/requestDecode.js";
6
+ import {responseEncodeError, responseEncodeSuccess} from "../encoders/responseEncode.js";
7
+ import {RespStatus} from "../interface.js";
8
+ import {Metrics} from "../metrics.js";
9
+ import {ReqRespRateLimiter} from "../rate_limiter/ReqRespRateLimiter.js";
10
+ import {RequestError, RequestErrorCode} from "../request/errors.js";
11
+ import {Protocol, ReqRespRequest} from "../types.js";
12
+ import {prettyPrintPeerId} from "../utils/index.js";
13
+ import {ResponseError} from "./errors.js";
14
+
15
+ export {ResponseError};
16
+
17
+ // Default spec values from https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#configuration
18
+ export const DEFAULT_REQUEST_TIMEOUT = 5 * 1000; // 5 sec
19
+
20
+ export interface HandleRequestOpts {
21
+ logger: Logger;
22
+ metrics: Metrics | null;
23
+ stream: Stream;
24
+ peerId: PeerId;
25
+ protocol: Protocol;
26
+ protocolID: string;
27
+ rateLimiter: ReqRespRateLimiter;
28
+ signal?: AbortSignal;
29
+ requestId?: number;
30
+ /** Peer client type for logging and metrics: 'prysm' | 'lighthouse' */
31
+ peerClient?: string;
32
+ /** Non-spec timeout from sending request until write stream closed by responder */
33
+ requestTimeoutMs?: number;
34
+ }
35
+
36
+ /**
37
+ * Handles a ReqResp request from a peer. Throws on error. Logs each step of the response lifecycle.
38
+ *
39
+ * 1. A duplex `stream` with the peer is already available
40
+ * 2. Read and decode request from peer
41
+ * 3. Delegate to `performRequestHandler()` to perform the request job and expect
42
+ * to yield zero or more `<response_chunks>`
43
+ * 4a. Encode and write `<response_chunks>` to peer
44
+ * 4b. On error, encode and write an error `<response_chunk>` and stop
45
+ */
46
+ export async function handleRequest({
47
+ logger,
48
+ metrics,
49
+ stream,
50
+ peerId,
51
+ protocol,
52
+ protocolID,
53
+ rateLimiter,
54
+ signal,
55
+ requestId = 0,
56
+ peerClient = "unknown",
57
+ requestTimeoutMs,
58
+ }: HandleRequestOpts): Promise<void> {
59
+ const REQUEST_TIMEOUT = requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT;
60
+
61
+ const logCtx = {
62
+ method: protocol.method,
63
+ version: protocol.version,
64
+ client: peerClient,
65
+ peer: prettyPrintPeerId(peerId),
66
+ requestId,
67
+ };
68
+ metrics?.incomingOpenedStreams.inc({method: protocol.method});
69
+
70
+ let responseError: Error | null = null;
71
+ await pipe(
72
+ // Yields success chunks and error chunks in the same generator
73
+ // This syntax allows to recycle stream.sink to send success and error chunks without returning
74
+ // in case request whose body is a List fails at chunk_i > 0, without breaking out of the for..await..of
75
+ (async function* requestHandlerSource() {
76
+ try {
77
+ // TODO: Does the TTFB timer start on opening stream or after receiving request
78
+ const timerTTFB = metrics?.outgoingResponseTTFB.startTimer({method: protocol.method});
79
+
80
+ const requestBody = await withTimeout(
81
+ () => pipe(stream.source as AsyncIterable<Uint8ArrayList>, requestDecode(protocol)),
82
+ REQUEST_TIMEOUT,
83
+ signal
84
+ ).catch((e: unknown) => {
85
+ if (e instanceof TimeoutError) {
86
+ throw e; // Let outter catch (_e) {} re-type the error as SERVER_ERROR
87
+ }
88
+ throw new ResponseError(RespStatus.INVALID_REQUEST, (e as Error).message);
89
+ });
90
+
91
+ logger.debug("Req received", logCtx);
92
+
93
+ // Max count by request for byRange and byRoot
94
+ const requestCount = protocol?.inboundRateLimits?.getRequestCount?.(requestBody) ?? 1;
95
+
96
+ if (!rateLimiter.allows(peerId, protocolID, requestCount)) {
97
+ throw new RequestError({code: RequestErrorCode.REQUEST_RATE_LIMITED});
98
+ }
99
+
100
+ const requestChunk: ReqRespRequest = {
101
+ data: requestBody,
102
+ version: protocol.version,
103
+ };
104
+
105
+ yield* pipe(
106
+ // TODO: Debug the reason for type conversion here
107
+ protocol.handler(requestChunk, peerId),
108
+ // NOTE: Do not log the resp chunk contents, logs get extremely cluttered
109
+ // Note: Not logging on each chunk since after 1 year it hasn't add any value when debugging
110
+ // onChunk(() => logger.debug("Resp sending chunk", logCtx)),
111
+ responseEncodeSuccess(protocol, {
112
+ onChunk(chunkIndex) {
113
+ if (chunkIndex === 0) timerTTFB?.();
114
+ },
115
+ })
116
+ );
117
+ } catch (e) {
118
+ const status = e instanceof ResponseError ? e.status : RespStatus.SERVER_ERROR;
119
+ yield* responseEncodeError(protocol, status, (e as Error).message);
120
+
121
+ // Should not throw an error here or libp2p-mplex throws with 'AbortError: stream reset'
122
+ // throw e;
123
+ responseError = e as Error;
124
+ }
125
+ })(),
126
+ stream.sink
127
+ );
128
+
129
+ // If streak.sink throws, libp2p-mplex will close stream.source
130
+ // If `requestDecode()` throws the stream.source must be closed manually
131
+ // To ensure the stream.source it-pushable instance is always closed, stream.close() is called always
132
+ await stream.close();
133
+ metrics?.incomingClosedStreams.inc({method: protocol.method});
134
+
135
+ // TODO: It may happen that stream.sink returns before returning stream.source first,
136
+ // so you never see "Resp received request" in the logs and the response ends without
137
+ // sending any chunk, triggering EMPTY_RESPONSE error on the requesting side
138
+ // It has only happened when doing a request too fast upon immediate connection on inbound peer
139
+ // investigate a potential race condition there
140
+
141
+ if (responseError !== null) {
142
+ logger.verbose("Resp error", logCtx, responseError);
143
+ throw responseError;
144
+ }
145
+ // NOTE: Only log once per request to verbose, intermediate steps to debug
146
+ logger.verbose("Resp done", logCtx);
147
+ }
package/src/types.ts ADDED
@@ -0,0 +1,158 @@
1
+ import {PeerId} from "@libp2p/interface";
2
+ import {BeaconConfig, ForkBoundary} from "@lodestar/config";
3
+ import {ForkName} from "@lodestar/params";
4
+ import {LodestarError} from "@lodestar/utils";
5
+ import {RateLimiterQuota} from "./rate_limiter/rateLimiterGRCA.js";
6
+
7
+ export const protocolPrefix = "/eth2/beacon_chain/req";
8
+
9
+ /**
10
+ * Available request/response encoding strategies:
11
+ * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#encoding-strategies
12
+ */
13
+ export enum Encoding {
14
+ SSZ_SNAPPY = "ssz_snappy",
15
+ }
16
+
17
+ export const CONTEXT_BYTES_FORK_DIGEST_LENGTH = 4;
18
+
19
+ /**
20
+ * Wrapper for the request/response payload
21
+ */
22
+ export type ResponseIncoming = {
23
+ data: Uint8Array;
24
+ fork: ForkName;
25
+ protocolVersion: number;
26
+ };
27
+
28
+ export type ResponseOutgoing = {
29
+ data: Uint8Array;
30
+ /**
31
+ * Reason why outgoing needs fork boundary but incoming only needs fork is because we can deserialize incoming data
32
+ * with only fork info, but for outgoing we also need to compute fork digest, which requires fork boundary.
33
+ */
34
+ boundary: ForkBoundary;
35
+ };
36
+
37
+ /**
38
+ * Rate limiter options for the requests
39
+ */
40
+ export interface ReqRespRateLimiterOpts {
41
+ rateLimitMultiplier?: number;
42
+ onRateLimit?: (peer: PeerId, method: string) => void;
43
+ }
44
+
45
+ /**
46
+ * Inbound rate limiter quota for the protocol
47
+ */
48
+ export interface InboundRateLimitQuota {
49
+ /**
50
+ * Will be tracked for the protocol per peer
51
+ */
52
+ byPeer?: RateLimiterQuota;
53
+ /**
54
+ * Will be tracked regardless of the peer
55
+ */
56
+ total?: RateLimiterQuota;
57
+ /**
58
+ * Some requests may be counted multiple e.g. getBlocksByRange
59
+ * for such implement this method else `1` will be used default
60
+ */
61
+ getRequestCount?: (req: Uint8Array) => number;
62
+ }
63
+
64
+ export type ReqRespRequest = {
65
+ data: Uint8Array;
66
+ version: number;
67
+ };
68
+
69
+ /**
70
+ * Request handler
71
+ */
72
+ export type ProtocolHandler = (req: ReqRespRequest, peerId: PeerId) => AsyncIterable<ResponseOutgoing>;
73
+
74
+ /**
75
+ * ReqResp Protocol Deceleration
76
+ */
77
+ export interface ProtocolAttributes {
78
+ readonly protocolPrefix: string;
79
+ /** Protocol name identifier `beacon_blocks_by_range` or `status` */
80
+ readonly method: string;
81
+ /** Version counter: `1`, `2` etc */
82
+ readonly version: number;
83
+ readonly encoding: Encoding;
84
+ }
85
+
86
+ export interface ProtocolDescriptor extends Omit<ProtocolAttributes, "protocolPrefix"> {
87
+ contextBytes: ContextBytesFactory;
88
+ ignoreResponse?: boolean;
89
+ }
90
+
91
+ // `protocolPrefix` is added runtime so not part of definition
92
+ /**
93
+ * ReqResp Protocol definition for full duplex protocols
94
+ */
95
+ export interface Protocol extends ProtocolDescriptor {
96
+ handler: ProtocolHandler;
97
+ inboundRateLimits?: InboundRateLimitQuota;
98
+ requestSizes: TypeSizes | null;
99
+ responseSizes: (fork: ForkName) => TypeSizes;
100
+ }
101
+
102
+ /**
103
+ * ReqResp Protocol definition for dial only protocols
104
+ */
105
+ export interface DialOnlyProtocol extends Omit<Protocol, "handler" | "inboundRateLimits" | "renderRequestBody"> {
106
+ handler?: never;
107
+ inboundRateLimits?: never;
108
+ renderRequestBody?: never;
109
+ }
110
+
111
+ /**
112
+ * ReqResp Protocol definition for full duplex and dial only protocols
113
+ */
114
+ export type MixedProtocol = DialOnlyProtocol | Protocol;
115
+
116
+ /**
117
+ * ReqResp protocol definition descriptor for full duplex and dial only protocols
118
+ * If handler is not provided, the protocol will be dial only
119
+ * If handler is provided, the protocol will be full duplex
120
+ */
121
+ export type MixedProtocolGenerators = <H extends ProtocolHandler | undefined = undefined>(
122
+ // "inboundRateLimiter" is available only on handler context not on generator
123
+ modules: {config: BeaconConfig},
124
+ handler?: H
125
+ ) => H extends undefined ? DialOnlyProtocol : Protocol;
126
+
127
+ /**
128
+ * ReqResp protocol definition descriptor for full duplex protocols
129
+ */
130
+ export type ProtocolGenerator = (modules: {config: BeaconConfig}, handler: ProtocolHandler) => Protocol;
131
+
132
+ export type HandlerTypeFromMessage<T> = T extends ProtocolGenerator ? ProtocolHandler : never;
133
+
134
+ export type ContextBytesFactory =
135
+ | {type: ContextBytesType.Empty}
136
+ | {type: ContextBytesType.ForkDigest; config: BeaconConfig};
137
+
138
+ export type ContextBytes = {type: ContextBytesType.Empty} | {type: ContextBytesType.ForkDigest; fork: ForkName};
139
+
140
+ export enum ContextBytesType {
141
+ /** 0 bytes chunk, can be ignored */
142
+ Empty,
143
+ /** A fixed-width 4 byte <context-bytes>, set to the ForkDigest matching the chunk: compute_fork_digest(fork_version, genesis_validators_root) */
144
+ ForkDigest,
145
+ }
146
+
147
+ export enum LightClientServerErrorCode {
148
+ RESOURCE_UNAVAILABLE = "RESOURCE_UNAVAILABLE",
149
+ }
150
+
151
+ export type LightClientServerErrorType = {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE};
152
+
153
+ export class LightClientServerError extends LodestarError<LightClientServerErrorType> {}
154
+
155
+ export type TypeSizes = {
156
+ maxSize: number;
157
+ minSize: number;
158
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Wraps an AsyncIterable and rejects early if any signal aborts.
3
+ * Throws the error returned by `getError()` of each signal options.
4
+ *
5
+ * Simplified fork of `"abortable-iterator"`.
6
+ * Read function's source for reasoning of the fork.
7
+ */
8
+ export function abortableSource<T>(
9
+ sourceArg: AsyncIterable<T>,
10
+ signals: {
11
+ signal: AbortSignal;
12
+ getError: () => Error;
13
+ }[]
14
+ ): AsyncIterable<T> {
15
+ const source = sourceArg as AsyncGenerator<T>;
16
+
17
+ async function* abortable(): AsyncIterable<T> {
18
+ // Handler that will hold a reference to the `abort()` promise,
19
+ // necessary for the signal abort listeners to reject the iterable promise
20
+ let nextAbortHandler: ((error: Error) => void) | null = null;
21
+
22
+ // For each signal register an abortHandler(), and prepare clean-up with `onDoneCbs`
23
+ const onDoneCbs: (() => void)[] = [];
24
+ for (const {signal, getError} of signals) {
25
+ const abortHandler = (): void => {
26
+ if (nextAbortHandler) nextAbortHandler(getError());
27
+ };
28
+ signal.addEventListener("abort", abortHandler);
29
+ onDoneCbs.push(() => {
30
+ signal.removeEventListener("abort", abortHandler);
31
+ });
32
+ }
33
+
34
+ try {
35
+ while (true) {
36
+ // Abort early if any signal is aborted
37
+ for (const {signal, getError} of signals) {
38
+ if (signal.aborted) {
39
+ throw getError();
40
+ }
41
+ }
42
+
43
+ // Race the iterator and the abort signals
44
+ const result = await Promise.race([
45
+ new Promise<never>((_, reject) => {
46
+ nextAbortHandler = (error) => reject(error);
47
+ }),
48
+ source.next(),
49
+ ]);
50
+
51
+ // source.next() resolved first
52
+ nextAbortHandler = null;
53
+
54
+ if (result.done) {
55
+ return;
56
+ }
57
+
58
+ yield result.value;
59
+ }
60
+ } catch (err) {
61
+ // End the iterator if it is a generator
62
+ if (typeof source.return === "function") {
63
+ // This source.return() function may never resolve depending on the source AsyncGenerator implementation.
64
+ // This is the main reason to fork "abortable-iterator", which caused our node to get stuck during Sync.
65
+ // We choose to call .return() but not await it. In general, source.return should never throw. If it does,
66
+ // it a problem of the source implementor, and thus logged as an unhandled rejection. If that happens,
67
+ // the source implementor should fix the upstream code.
68
+ void source.return(null);
69
+ }
70
+
71
+ throw err;
72
+ } finally {
73
+ for (const cb of onDoneCbs) {
74
+ cb();
75
+ }
76
+ }
77
+ }
78
+
79
+ return abortable();
80
+ }
@@ -0,0 +1,46 @@
1
+ import {Uint8ArrayList} from "uint8arraylist";
2
+
3
+ /**
4
+ * Wraps a buffer chunk stream source with another async iterable
5
+ * so it can be reused in multiple for..of statements.
6
+ *
7
+ * Uses a BufferList internally to make sure all chunks are consumed
8
+ * when switching consumers
9
+ */
10
+ export class BufferedSource {
11
+ isDone = false;
12
+ private buffer: Uint8ArrayList;
13
+ private source: AsyncGenerator<Uint8ArrayList | Uint8Array>;
14
+
15
+ constructor(source: AsyncGenerator<Uint8ArrayList | Uint8Array>) {
16
+ this.buffer = new Uint8ArrayList();
17
+ this.source = source;
18
+ }
19
+
20
+ [Symbol.asyncIterator](): AsyncIterator<Uint8ArrayList> {
21
+ const that = this;
22
+
23
+ let firstNext = true;
24
+
25
+ return {
26
+ async next() {
27
+ // Prevent fetching a new chunk if there are pending bytes
28
+ // not processed by a previous consumer of this BufferedSource
29
+ if (firstNext && that.buffer.length > 0) {
30
+ firstNext = false;
31
+ return {done: false, value: that.buffer};
32
+ }
33
+
34
+ const {done, value: chunk} = await that.source.next();
35
+ if (done === true) {
36
+ that.isDone = true;
37
+ return {done: true, value: undefined};
38
+ }
39
+
40
+ // Concat new chunk and return a reference to its BufferList instance
41
+ that.buffer.append(chunk);
42
+ return {done: false, value: that.buffer};
43
+ },
44
+ };
45
+ }
46
+ }
@@ -0,0 +1,15 @@
1
+ import {RequestError, RequestErrorCode} from "../request/errors.js";
2
+
3
+ /**
4
+ * Sink for `<response_chunk>*`, from
5
+ * ```bnf
6
+ * response ::= <response_chunk>*
7
+ * ```
8
+ * Expects exactly one response
9
+ */
10
+ export async function collectExactOne<T>(source: AsyncIterable<T>): Promise<T> {
11
+ for await (const response of source) {
12
+ return response;
13
+ }
14
+ throw new RequestError({code: RequestErrorCode.EMPTY_RESPONSE});
15
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Sink for `<response_chunk>*`, from
3
+ * ```bnf
4
+ * response ::= <response_chunk>*
5
+ * ```
6
+ * Collects a bounded list of responses up to `maxResponses`
7
+ */
8
+ export async function collectMaxResponse<T>(source: AsyncIterable<T>, maxResponses: number): Promise<T[]> {
9
+ // else: zero or more responses
10
+ const responses: T[] = [];
11
+ for await (const response of source) {
12
+ responses.push(response);
13
+
14
+ if (maxResponses !== undefined && responses.length >= maxResponses) {
15
+ break;
16
+ }
17
+ }
18
+ return responses;
19
+ }
@@ -0,0 +1,51 @@
1
+ import {decode as varintDecode, encodingLength as varintEncodingLength} from "uint8-varint";
2
+ import {Uint8ArrayList} from "uint8arraylist";
3
+ import {writeSszSnappyPayload} from "../encodingStrategies/sszSnappy/encode.js";
4
+ import {SnappyFramesUncompress} from "../encodingStrategies/sszSnappy/snappyFrames/uncompress.js";
5
+ import {Encoding} from "../types.js";
6
+
7
+ // ErrorMessage schema:
8
+ //
9
+ // (
10
+ // error_message: List[byte, 256]
11
+ // )
12
+ //
13
+ // By convention, the error_message is a sequence of bytes that MAY be interpreted as a
14
+ // UTF-8 string (for debugging purposes). Clients MUST treat as valid any byte sequences
15
+ //
16
+ // Spec v1.1.10 https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#responding-side
17
+
18
+ /**
19
+ * Encodes a UTF-8 string to 256 bytes max
20
+ */
21
+ export async function* encodeErrorMessage(errorMessage: string, encoding: Encoding): AsyncGenerator<Buffer> {
22
+ const encoder = new TextEncoder();
23
+ const bytes = encoder.encode(errorMessage).slice(0, 256);
24
+
25
+ switch (encoding) {
26
+ case Encoding.SSZ_SNAPPY:
27
+ yield* writeSszSnappyPayload(bytes);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Decodes error message from network bytes and removes non printable, non ascii characters.
33
+ */
34
+ export async function decodeErrorMessage(encodedErrorMessage: Uint8Array): Promise<string> {
35
+ const encoder = new TextDecoder();
36
+ let sszDataLength: number;
37
+ try {
38
+ sszDataLength = varintDecode(encodedErrorMessage);
39
+ const decompressor = new SnappyFramesUncompress();
40
+ const varintBytes = varintEncodingLength(sszDataLength);
41
+ const errorMessage = decompressor.uncompress(new Uint8ArrayList(encodedErrorMessage.subarray(varintBytes)));
42
+ if (errorMessage == null || errorMessage.length !== sszDataLength) {
43
+ throw new Error("Malformed input: data length mismatch");
44
+ }
45
+ // remove non ascii characters from string
46
+ return encoder.decode(errorMessage.subarray(0)).replace(/[^\x20-\x7F]/g, "");
47
+ } catch (_e) {
48
+ // remove non ascii characters from string
49
+ return encoder.decode(encodedErrorMessage.slice(0, 256)).replace(/[^\x20-\x7F]/g, "");
50
+ }
51
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./abortableSource.js";
2
+ export * from "./bufferedSource.js";
3
+ export * from "./collectExactOne.js";
4
+ export * from "./collectMaxResponse.js";
5
+ export * from "./errorMessage.js";
6
+ export * from "./onChunk.js";
7
+ export * from "./peerId.js";
8
+ export * from "./protocolId.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Calls `callback` with each `chunk` received from the `source` AsyncIterable
3
+ * Useful for logging, or cancelling timeouts
4
+ */
5
+ export function onChunk<T>(callback: (chunk: T) => void): (source: AsyncIterable<T>) => AsyncIterable<T> {
6
+ return async function* onChunkTransform(source) {
7
+ for await (const chunk of source) {
8
+ callback(chunk);
9
+ yield chunk;
10
+ }
11
+ };
12
+ }
@@ -0,0 +1,6 @@
1
+ import {PeerId} from "@libp2p/interface";
2
+
3
+ export function prettyPrintPeerId(peerId: PeerId): string {
4
+ const id = peerId.toString();
5
+ return `${id.substr(0, 2)}...${id.substr(id.length - 6, id.length)}`;
6
+ }
@@ -0,0 +1,44 @@
1
+ import {Encoding, ProtocolAttributes} from "../types.js";
2
+
3
+ /**
4
+ * https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#protocol-identification
5
+ */
6
+ export function formatProtocolID(protocolPrefix: string, method: string, version: number, encoding: Encoding): string {
7
+ return `${protocolPrefix}/${method}/${version}/${encoding}`;
8
+ }
9
+
10
+ /**
11
+ * https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#protocol-identification
12
+ */
13
+ export function parseProtocolID(protocolId: string): ProtocolAttributes {
14
+ const result = protocolId.split("/");
15
+ if (result.length < 4) {
16
+ throw new Error(`Invalid protocol id: ${protocolId}`);
17
+ }
18
+
19
+ const encoding = result.at(-1) as Encoding;
20
+ if (!Object.values(Encoding).includes(encoding)) {
21
+ throw new Error(`Invalid protocol encoding: ${encoding}`);
22
+ }
23
+
24
+ const versionStr = result.at(-2) as string;
25
+ if (!/^-?[0-9]+$/.test(versionStr)) {
26
+ throw new Error(`Invalid protocol version: ${versionStr}`);
27
+ }
28
+
29
+ // an ordinal version number (e.g. 1, 2, 3…).
30
+ const version = parseInt(versionStr);
31
+
32
+ // each request is identified by a name consisting of English alphabet, digits and underscores (_).
33
+ const method = result.at(-3) as string;
34
+
35
+ // messages are grouped into families identified by a shared libp2p protocol name prefix
36
+ const protocolPrefix = result.slice(0, result.length - 3).join("/");
37
+
38
+ return {
39
+ protocolPrefix,
40
+ method,
41
+ version,
42
+ encoding,
43
+ };
44
+ }