@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.
- package/README.md +1 -1
- package/lib/ReqResp.d.ts +3 -3
- package/lib/ReqResp.js +4 -4
- package/lib/ReqResp.js.map +1 -1
- package/lib/encoders/requestDecode.d.ts +2 -3
- package/lib/encoders/requestDecode.d.ts.map +1 -1
- package/lib/encoders/requestDecode.js +28 -11
- package/lib/encoders/requestDecode.js.map +1 -1
- package/lib/encoders/requestEncode.d.ts +1 -1
- package/lib/encoders/requestEncode.d.ts.map +1 -1
- package/lib/encoders/requestEncode.js +1 -1
- package/lib/encoders/requestEncode.js.map +1 -1
- package/lib/encoders/responseDecode.d.ts +10 -10
- package/lib/encoders/responseDecode.d.ts.map +1 -1
- package/lib/encoders/responseDecode.js +63 -60
- package/lib/encoders/responseDecode.js.map +1 -1
- package/lib/encoders/responseEncode.d.ts +2 -4
- package/lib/encoders/responseEncode.d.ts.map +1 -1
- package/lib/encoders/responseEncode.js +13 -22
- package/lib/encoders/responseEncode.js.map +1 -1
- package/lib/encodingStrategies/index.d.ts +4 -3
- package/lib/encodingStrategies/index.d.ts.map +1 -1
- package/lib/encodingStrategies/index.js +4 -4
- package/lib/encodingStrategies/index.js.map +1 -1
- package/lib/encodingStrategies/sszSnappy/decode.d.ts +5 -4
- package/lib/encodingStrategies/sszSnappy/decode.d.ts.map +1 -1
- package/lib/encodingStrategies/sszSnappy/decode.js +83 -52
- package/lib/encodingStrategies/sszSnappy/decode.js.map +1 -1
- package/lib/encodingStrategies/sszSnappy/encode.d.ts +2 -2
- package/lib/encodingStrategies/sszSnappy/encode.d.ts.map +1 -1
- package/lib/encodingStrategies/sszSnappy/encode.js +1 -1
- package/lib/encodingStrategies/sszSnappy/encode.js.map +1 -1
- package/lib/encodingStrategies/sszSnappy/errors.d.ts +0 -8
- package/lib/encodingStrategies/sszSnappy/errors.d.ts.map +1 -1
- package/lib/encodingStrategies/sszSnappy/errors.js +0 -2
- package/lib/encodingStrategies/sszSnappy/errors.js.map +1 -1
- package/lib/encodingStrategies/sszSnappy/index.d.ts +0 -1
- package/lib/encodingStrategies/sszSnappy/index.d.ts.map +1 -1
- package/lib/encodingStrategies/sszSnappy/index.js +0 -1
- package/lib/encodingStrategies/sszSnappy/index.js.map +1 -1
- package/lib/metrics.d.ts +1 -7
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js +1 -17
- package/lib/metrics.js.map +1 -1
- package/lib/request/errors.d.ts +1 -7
- package/lib/request/errors.d.ts.map +1 -1
- package/lib/request/errors.js +1 -5
- package/lib/request/errors.js.map +1 -1
- package/lib/request/index.d.ts +0 -3
- package/lib/request/index.d.ts.map +1 -1
- package/lib/request/index.js +58 -70
- package/lib/request/index.js.map +1 -1
- package/lib/response/index.d.ts +1 -1
- package/lib/response/index.d.ts.map +1 -1
- package/lib/response/index.js +46 -50
- package/lib/response/index.js.map +1 -1
- package/lib/types.d.ts +1 -2
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +1 -2
- package/lib/types.js.map +1 -1
- package/lib/utils/collectMaxResponse.d.ts.map +1 -1
- package/lib/utils/collectMaxResponse.js +1 -2
- package/lib/utils/collectMaxResponse.js.map +1 -1
- package/lib/utils/errorMessage.d.ts +3 -3
- package/lib/utils/errorMessage.d.ts.map +1 -1
- package/lib/utils/errorMessage.js +14 -13
- package/lib/utils/errorMessage.js.map +1 -1
- package/lib/utils/index.d.ts +1 -3
- package/lib/utils/index.d.ts.map +1 -1
- package/lib/utils/index.js +1 -3
- package/lib/utils/index.js.map +1 -1
- package/lib/utils/protocolId.d.ts +2 -2
- package/lib/utils/protocolId.js +2 -2
- package/lib/utils/snappyCompress.d.ts +1 -1
- package/lib/utils/snappyCompress.d.ts.map +1 -1
- package/lib/utils/snappyCompress.js +1 -1
- package/lib/utils/snappyCompress.js.map +1 -1
- package/lib/utils/snappyIndex.d.ts +1 -1
- package/lib/utils/snappyIndex.d.ts.map +1 -1
- package/lib/utils/snappyIndex.js +1 -1
- package/lib/utils/snappyIndex.js.map +1 -1
- package/lib/utils/snappyUncompress.d.ts +7 -11
- package/lib/utils/snappyUncompress.d.ts.map +1 -1
- package/lib/utils/snappyUncompress.js +68 -68
- package/lib/utils/snappyUncompress.js.map +1 -1
- package/lib/utils/stream.d.ts +6 -0
- package/lib/utils/stream.d.ts.map +1 -0
- package/lib/utils/stream.js +21 -0
- package/lib/utils/stream.js.map +1 -0
- package/package.json +14 -16
- package/src/ReqResp.ts +4 -4
- package/src/encoders/requestDecode.ts +32 -16
- package/src/encoders/requestEncode.ts +1 -1
- package/src/encoders/responseDecode.ts +68 -72
- package/src/encoders/responseEncode.ts +17 -29
- package/src/encodingStrategies/index.ts +8 -6
- package/src/encodingStrategies/sszSnappy/decode.ts +111 -53
- package/src/encodingStrategies/sszSnappy/encode.ts +2 -2
- package/src/encodingStrategies/sszSnappy/errors.ts +0 -4
- package/src/encodingStrategies/sszSnappy/index.ts +0 -1
- package/src/metrics.ts +1 -17
- package/src/request/errors.ts +1 -6
- package/src/request/index.ts +74 -86
- package/src/response/index.ts +55 -61
- package/src/types.ts +1 -3
- package/src/utils/collectMaxResponse.ts +1 -2
- package/src/utils/errorMessage.ts +14 -13
- package/src/utils/index.ts +1 -3
- package/src/utils/protocolId.ts +2 -2
- package/src/utils/snappyCompress.ts +1 -1
- package/src/utils/snappyIndex.ts +1 -1
- package/src/utils/snappyUncompress.ts +73 -75
- package/src/utils/stream.ts +34 -0
- package/lib/utils/abortableSource.d.ts +0 -12
- package/lib/utils/abortableSource.d.ts.map +0 -1
- package/lib/utils/abortableSource.js +0 -69
- package/lib/utils/abortableSource.js.map +0 -1
- package/lib/utils/bufferedSource.d.ts +0 -16
- package/lib/utils/bufferedSource.d.ts.map +0 -1
- package/lib/utils/bufferedSource.js +0 -40
- package/lib/utils/bufferedSource.js.map +0 -1
- package/lib/utils/onChunk.d.ts +0 -6
- package/lib/utils/onChunk.d.ts.map +0 -1
- package/lib/utils/onChunk.js +0 -13
- package/lib/utils/onChunk.js.map +0 -1
- package/lib/utils/snappy.d.ts +0 -3
- package/lib/utils/snappy.d.ts.map +0 -1
- package/lib/utils/snappy.js +0 -3
- package/lib/utils/snappy.js.map +0 -1
- package/src/utils/abortableSource.ts +0 -80
- package/src/utils/bufferedSource.ts +0 -46
- package/src/utils/onChunk.ts +0 -12
- package/src/utils/snappy.ts +0 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {Stream} from "@libp2p/interface";
|
|
2
|
+
import type {ByteStream} from "@libp2p/utils";
|
|
3
|
+
import {byteStream} from "@libp2p/utils";
|
|
2
4
|
import {ForkName} from "@lodestar/params";
|
|
3
5
|
import {readEncodedPayload} from "../encodingStrategies/index.js";
|
|
4
6
|
import {RespStatus} from "../interface.js";
|
|
@@ -10,7 +12,7 @@ import {
|
|
|
10
12
|
MixedProtocol,
|
|
11
13
|
ResponseIncoming,
|
|
12
14
|
} from "../types.js";
|
|
13
|
-
import {
|
|
15
|
+
import {decodeErrorMessage, drainByteStream} from "../utils/index.js";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Internal helper type to signal stream ended early
|
|
@@ -27,22 +29,17 @@ enum StreamStatus {
|
|
|
27
29
|
* result ::= "0" | "1" | "2" | ["128" ... "255"]
|
|
28
30
|
* ```
|
|
29
31
|
*/
|
|
30
|
-
export function responseDecode(
|
|
32
|
+
export async function* responseDecode(
|
|
31
33
|
protocol: MixedProtocol,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return async function* responseDecodeSink(source) {
|
|
38
|
-
const bufferedSource = new BufferedSource(source as AsyncGenerator<Uint8ArrayList>);
|
|
39
|
-
|
|
40
|
-
let readFirstHeader = false;
|
|
41
|
-
let readFirstResponseChunk = false;
|
|
34
|
+
stream: Stream,
|
|
35
|
+
opts: {signal?: AbortSignal; getError?: () => Error} = {}
|
|
36
|
+
): AsyncIterable<ResponseIncoming> {
|
|
37
|
+
const bytes = byteStream(stream);
|
|
38
|
+
let responseReadDone = false;
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
while (
|
|
45
|
-
const status = await readResultHeader(
|
|
40
|
+
try {
|
|
41
|
+
while (true) {
|
|
42
|
+
const status = await readResultHeader(bytes, opts.signal);
|
|
46
43
|
|
|
47
44
|
// Stream is only allowed to end at the start of a <response_chunk> block
|
|
48
45
|
// The happens when source ends before readResultHeader() can fetch 1 byte
|
|
@@ -50,34 +47,41 @@ export function responseDecode(
|
|
|
50
47
|
break;
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
if (!readFirstHeader) {
|
|
54
|
-
cbs.onFirstHeader();
|
|
55
|
-
readFirstHeader = true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
50
|
// For multiple chunks, only the last chunk is allowed to have a non-zero error
|
|
59
51
|
// code (i.e. The chunk stream is terminated once an error occurs
|
|
60
52
|
if (status !== RespStatus.SUCCESS) {
|
|
61
|
-
const errorMessage = await readErrorMessage(
|
|
53
|
+
const errorMessage = await readErrorMessage(bytes, opts.signal);
|
|
62
54
|
throw new ResponseError(status, errorMessage);
|
|
63
55
|
}
|
|
64
56
|
|
|
65
|
-
const forkName = await readContextBytes(protocol.contextBytes,
|
|
57
|
+
const forkName = await readContextBytes(protocol.contextBytes, bytes, opts.signal);
|
|
66
58
|
const typeSizes = protocol.responseSizes(forkName);
|
|
67
|
-
const chunkData = await readEncodedPayload(
|
|
59
|
+
const chunkData = await readEncodedPayload(bytes, protocol.encoding, typeSizes, opts.signal);
|
|
68
60
|
|
|
69
61
|
yield {
|
|
70
62
|
data: chunkData,
|
|
71
63
|
fork: forkName,
|
|
72
64
|
protocolVersion: protocol.version,
|
|
73
65
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
}
|
|
67
|
+
responseReadDone = true;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (opts.signal?.aborted && opts.getError) {
|
|
70
|
+
throw opts.getError();
|
|
71
|
+
}
|
|
72
|
+
throw e;
|
|
73
|
+
} finally {
|
|
74
|
+
try {
|
|
75
|
+
if (!responseReadDone) {
|
|
76
|
+
// Do not push partial bytes back into the stream on decode failure/abort.
|
|
77
|
+
// This stream is consumed by req/resp only once.
|
|
78
|
+
drainByteStream(bytes);
|
|
78
79
|
}
|
|
80
|
+
bytes.unwrap();
|
|
81
|
+
} catch {
|
|
82
|
+
// Ignore unwrap errors - stream may already be closed
|
|
79
83
|
}
|
|
80
|
-
}
|
|
84
|
+
}
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/**
|
|
@@ -87,18 +91,17 @@ export function responseDecode(
|
|
|
87
91
|
* ```
|
|
88
92
|
* `<response_chunk>` starts with a single-byte response code which determines the contents of the response_chunk
|
|
89
93
|
*/
|
|
90
|
-
export async function readResultHeader(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return StreamStatus.Ended;
|
|
94
|
+
export async function readResultHeader(
|
|
95
|
+
bytes: ByteStream<Stream>,
|
|
96
|
+
signal?: AbortSignal
|
|
97
|
+
): Promise<RespStatus | StreamStatus> {
|
|
98
|
+
const chunk = await bytes.read({bytes: 1, signal}).catch((e) => {
|
|
99
|
+
if ((e as Error).name === "UnexpectedEOFError") return null;
|
|
100
|
+
throw e;
|
|
101
|
+
});
|
|
102
|
+
if (chunk === null) return StreamStatus.Ended;
|
|
103
|
+
|
|
104
|
+
return chunk.get(0);
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
/**
|
|
@@ -108,28 +111,29 @@ export async function readResultHeader(bufferedSource: BufferedSource): Promise<
|
|
|
108
111
|
* result ::= "1" | "2" | ["128" ... "255"]
|
|
109
112
|
* ```
|
|
110
113
|
*/
|
|
111
|
-
export async function readErrorMessage(
|
|
112
|
-
|
|
113
|
-
let
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
export async function readErrorMessage(bytes: ByteStream<Stream>, signal?: AbortSignal): Promise<string> {
|
|
115
|
+
const chunks: Uint8Array[] = [];
|
|
116
|
+
let total = 0;
|
|
117
|
+
|
|
118
|
+
while (total < 256) {
|
|
119
|
+
const chunk = await bytes.read({signal}).catch((e) => {
|
|
120
|
+
if ((e as Error).name === "UnexpectedEOFError") return null;
|
|
121
|
+
throw e;
|
|
122
|
+
});
|
|
123
|
+
if (chunk === null) {
|
|
124
|
+
// If EOF is reached while satisfying a larger read, libp2p v3 may still have
|
|
125
|
+
// buffered bytes available. Drain them so error_message matches pre-v3 behavior.
|
|
126
|
+
const remaining = drainByteStream(bytes);
|
|
127
|
+
if (remaining) {
|
|
128
|
+
chunks.push(remaining);
|
|
129
|
+
}
|
|
119
130
|
break;
|
|
120
131
|
}
|
|
121
|
-
|
|
132
|
+
chunks.push(chunk.subarray());
|
|
133
|
+
total += chunk.byteLength;
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
|
|
125
|
-
const bytes = bufferedSource["buffer"].slice(0, length);
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
return decodeErrorMessage(bytes);
|
|
129
|
-
} catch (_e) {
|
|
130
|
-
// Error message is optional and may not be included in the response stream
|
|
131
|
-
return Buffer.prototype.toString.call(bytes, "hex");
|
|
132
|
-
}
|
|
136
|
+
return decodeErrorMessage(Buffer.concat(chunks).subarray(0, 256));
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
/**
|
|
@@ -139,14 +143,15 @@ export async function readErrorMessage(bufferedSource: BufferedSource): Promise<
|
|
|
139
143
|
*/
|
|
140
144
|
export async function readContextBytes(
|
|
141
145
|
contextBytes: ContextBytesFactory,
|
|
142
|
-
|
|
146
|
+
bytes: ByteStream<Stream>,
|
|
147
|
+
signal?: AbortSignal
|
|
143
148
|
): Promise<ForkName> {
|
|
144
149
|
switch (contextBytes.type) {
|
|
145
150
|
case ContextBytesType.Empty:
|
|
146
151
|
return ForkName.phase0;
|
|
147
152
|
|
|
148
153
|
case ContextBytesType.ForkDigest: {
|
|
149
|
-
const forkDigest = await readContextBytesForkDigest(
|
|
154
|
+
const forkDigest = await readContextBytesForkDigest(bytes, signal);
|
|
150
155
|
return contextBytes.config.forkDigest2ForkBoundary(forkDigest).fork;
|
|
151
156
|
}
|
|
152
157
|
}
|
|
@@ -155,15 +160,6 @@ export async function readContextBytes(
|
|
|
155
160
|
/**
|
|
156
161
|
* Consumes a stream source to read `<context-bytes>`, where it's a fixed-width 4 byte
|
|
157
162
|
*/
|
|
158
|
-
export async function readContextBytesForkDigest(
|
|
159
|
-
|
|
160
|
-
if (buffer.length >= CONTEXT_BYTES_FORK_DIGEST_LENGTH) {
|
|
161
|
-
const bytes = buffer.slice(0, CONTEXT_BYTES_FORK_DIGEST_LENGTH);
|
|
162
|
-
buffer.consume(CONTEXT_BYTES_FORK_DIGEST_LENGTH);
|
|
163
|
-
return bytes;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// TODO: Use typed error
|
|
168
|
-
throw Error("Source ended while reading context bytes");
|
|
163
|
+
export async function readContextBytesForkDigest(bytes: ByteStream<Stream>, signal?: AbortSignal): Promise<Uint8Array> {
|
|
164
|
+
return (await bytes.read({bytes: CONTEXT_BYTES_FORK_DIGEST_LENGTH, signal})).subarray();
|
|
169
165
|
}
|
|
@@ -14,30 +14,23 @@ const SUCCESS_BUFFER = Buffer.from([RespStatus.SUCCESS]);
|
|
|
14
14
|
* ```
|
|
15
15
|
* Note: `response` has zero or more chunks (denoted by `<>*`)
|
|
16
16
|
*/
|
|
17
|
-
export function responseEncodeSuccess(
|
|
17
|
+
export async function* responseEncodeSuccess(
|
|
18
18
|
protocol: Protocol,
|
|
19
|
-
|
|
20
|
-
):
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
source: AsyncIterable<ResponseOutgoing>
|
|
20
|
+
): AsyncIterable<Uint8Array> {
|
|
21
|
+
for await (const chunk of source) {
|
|
22
|
+
// <result>
|
|
23
|
+
yield SUCCESS_BUFFER;
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// <result>
|
|
29
|
-
yield SUCCESS_BUFFER;
|
|
30
|
-
|
|
31
|
-
// <context-bytes> - from altair
|
|
32
|
-
const contextBytes = getContextBytes(protocol.contextBytes, chunk);
|
|
33
|
-
if (contextBytes) {
|
|
34
|
-
yield contextBytes as Buffer;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// <encoding-dependent-header> | <encoded-payload>
|
|
38
|
-
yield* writeEncodedPayload(chunk.data, protocol.encoding);
|
|
25
|
+
// <context-bytes> - from altair
|
|
26
|
+
const contextBytes = getContextBytes(protocol.contextBytes, chunk);
|
|
27
|
+
if (contextBytes) {
|
|
28
|
+
yield contextBytes;
|
|
39
29
|
}
|
|
40
|
-
|
|
30
|
+
|
|
31
|
+
// <encoding-dependent-header> | <encoded-payload>
|
|
32
|
+
yield* writeEncodedPayload(chunk.data, protocol.encoding);
|
|
33
|
+
}
|
|
41
34
|
}
|
|
42
35
|
|
|
43
36
|
/**
|
|
@@ -50,20 +43,15 @@ export function responseEncodeSuccess(
|
|
|
50
43
|
* Only the last `<response_chunk>` is allowed to have a non-zero error code, so this
|
|
51
44
|
* fn yields exactly one `<error_response>` and afterwards the stream must be terminated
|
|
52
45
|
*/
|
|
53
|
-
export
|
|
46
|
+
export function* responseEncodeError(
|
|
54
47
|
protocol: Pick<MixedProtocol, "encoding">,
|
|
55
48
|
status: RpcResponseStatusError,
|
|
56
49
|
errorMessage: string
|
|
57
|
-
):
|
|
58
|
-
if (!errorMessage) {
|
|
59
|
-
yield Buffer.from([status]);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
50
|
+
): Generator<Buffer> {
|
|
63
51
|
// Combine <result> and <error_message> into a single chunk for atomic delivery.
|
|
64
52
|
// Yielding them separately causes a race condition where the stream closes after the
|
|
65
53
|
// status byte but before the error message arrives on the reader side.
|
|
66
|
-
const errorMessageBuffer =
|
|
54
|
+
const errorMessageBuffer = encodeErrorMessageToBuffer(errorMessage, protocol.encoding);
|
|
67
55
|
yield Buffer.concat([Buffer.from([status]), errorMessageBuffer]);
|
|
68
56
|
}
|
|
69
57
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type {Stream} from "@libp2p/interface";
|
|
2
|
+
import type {ByteStream} from "@libp2p/utils";
|
|
1
3
|
import {Encoding, TypeSizes} from "../types.js";
|
|
2
|
-
import {BufferedSource} from "../utils/index.js";
|
|
3
4
|
import {readSszSnappyPayload} from "./sszSnappy/decode.js";
|
|
4
5
|
import {writeSszSnappyPayload} from "./sszSnappy/encode.js";
|
|
5
6
|
|
|
6
7
|
// For more info about Ethereum Consensus request/response encoding strategies, see:
|
|
7
|
-
// https://github.com/ethereum/consensus-specs/blob/v1.1
|
|
8
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/p2p-interface.md#encoding-strategies
|
|
8
9
|
// Supported encoding strategies:
|
|
9
10
|
// - ssz_snappy
|
|
10
11
|
|
|
@@ -15,13 +16,14 @@ import {writeSszSnappyPayload} from "./sszSnappy/encode.js";
|
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
18
|
export async function readEncodedPayload(
|
|
18
|
-
|
|
19
|
+
stream: ByteStream<Stream>,
|
|
19
20
|
encoding: Encoding,
|
|
20
|
-
type: TypeSizes
|
|
21
|
+
type: TypeSizes,
|
|
22
|
+
signal?: AbortSignal
|
|
21
23
|
): Promise<Uint8Array> {
|
|
22
24
|
switch (encoding) {
|
|
23
25
|
case Encoding.SSZ_SNAPPY:
|
|
24
|
-
return readSszSnappyPayload(
|
|
26
|
+
return readSszSnappyPayload(stream, type, signal);
|
|
25
27
|
|
|
26
28
|
default:
|
|
27
29
|
throw Error("Unsupported encoding");
|
|
@@ -34,7 +36,7 @@ export async function readEncodedPayload(
|
|
|
34
36
|
* <encoding-dependent-header> | <encoded-payload>
|
|
35
37
|
* ```
|
|
36
38
|
*/
|
|
37
|
-
export
|
|
39
|
+
export function* writeEncodedPayload(chunkData: Uint8Array, encoding: Encoding): Generator<Buffer> {
|
|
38
40
|
switch (encoding) {
|
|
39
41
|
case Encoding.SSZ_SNAPPY:
|
|
40
42
|
yield* writeSszSnappyPayload(chunkData);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type {Stream} from "@libp2p/interface";
|
|
2
|
+
import type {ByteStream} from "@libp2p/utils";
|
|
1
3
|
import {decode as varintDecode, encodingLength as varintEncodingLength} from "uint8-varint";
|
|
2
4
|
import {Uint8ArrayList} from "uint8arraylist";
|
|
3
5
|
import {TypeSizes} from "../../types.js";
|
|
4
|
-
import {
|
|
5
|
-
import {SnappyFramesUncompress} from "../../utils/snappyIndex.js";
|
|
6
|
+
import {ChunkType, decodeSnappyFrameData, parseSnappyFrameHeader} from "../../utils/snappyIndex.js";
|
|
6
7
|
import {SszSnappyError, SszSnappyErrorCode} from "./errors.js";
|
|
7
8
|
import {maxEncodedLen} from "./utils.js";
|
|
8
9
|
|
|
@@ -15,76 +16,115 @@ export const MAX_VARINT_BYTES = 10;
|
|
|
15
16
|
* <encoding-dependent-header> | <encoded-payload>
|
|
16
17
|
* ```
|
|
17
18
|
*/
|
|
18
|
-
export async function readSszSnappyPayload(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
export async function readSszSnappyPayload(
|
|
20
|
+
stream: ByteStream<Stream>,
|
|
21
|
+
type: TypeSizes,
|
|
22
|
+
signal?: AbortSignal
|
|
23
|
+
): Promise<Uint8Array> {
|
|
24
|
+
const sszDataLength = await readSszSnappyHeader(stream, type, signal);
|
|
25
|
+
|
|
26
|
+
return readSszSnappyBody(stream, sszDataLength, signal);
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Reads `<encoding-dependent-header>` for ssz-snappy.
|
|
26
31
|
* encoding-header ::= the length of the raw SSZ bytes, encoded as an unsigned protobuf varint
|
|
27
32
|
*/
|
|
28
|
-
export async function readSszSnappyHeader(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
export async function readSszSnappyHeader(
|
|
34
|
+
stream: ByteStream<Stream>,
|
|
35
|
+
type: TypeSizes,
|
|
36
|
+
signal?: AbortSignal
|
|
37
|
+
): Promise<number> {
|
|
38
|
+
const varintBytes: number[] = [];
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
sszDataLength = varintDecode(buffer.subarray());
|
|
38
|
-
} catch (_e) {
|
|
39
|
-
throw new SszSnappyError({code: SszSnappyErrorCode.INVALID_VARINT_BYTES_COUNT, bytes: Infinity});
|
|
40
|
-
}
|
|
40
|
+
while (true) {
|
|
41
|
+
const byte = await readExactOrSourceAborted(stream, 1, signal);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
// encodingLength function only returns 1-8 inclusive
|
|
44
|
-
const varintBytes = varintEncodingLength(sszDataLength);
|
|
45
|
-
buffer.consume(varintBytes);
|
|
43
|
+
const value = byte.get(0);
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (sszDataLength < minSize) {
|
|
51
|
-
throw new SszSnappyError({code: SszSnappyErrorCode.UNDER_SSZ_MIN_SIZE, minSize, sszDataLength});
|
|
52
|
-
}
|
|
53
|
-
if (sszDataLength > maxSize) {
|
|
54
|
-
throw new SszSnappyError({code: SszSnappyErrorCode.OVER_SSZ_MAX_SIZE, maxSize, sszDataLength});
|
|
45
|
+
varintBytes.push(value);
|
|
46
|
+
if (varintBytes.length > MAX_VARINT_BYTES) {
|
|
47
|
+
throw new SszSnappyError({code: SszSnappyErrorCode.INVALID_VARINT_BYTES_COUNT, bytes: varintBytes.length});
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
// MSB not set => varint terminated
|
|
51
|
+
if ((value & 0x80) === 0) break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let sszDataLength: number;
|
|
55
|
+
try {
|
|
56
|
+
sszDataLength = varintDecode(Uint8Array.from(varintBytes));
|
|
57
|
+
} catch {
|
|
58
|
+
throw new SszSnappyError({code: SszSnappyErrorCode.INVALID_VARINT_BYTES_COUNT, bytes: Infinity});
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
// MUST validate: the unsigned protobuf varint used for the length-prefix MUST not be longer than 10 bytes
|
|
62
|
+
// encodingLength function only returns 1-8 inclusive
|
|
63
|
+
const varintByteLength = varintEncodingLength(sszDataLength);
|
|
64
|
+
if (varintByteLength > MAX_VARINT_BYTES) {
|
|
65
|
+
throw new SszSnappyError({code: SszSnappyErrorCode.INVALID_VARINT_BYTES_COUNT, bytes: varintByteLength});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// MUST validate: the length-prefix is within the expected size bounds derived from the payload SSZ type.
|
|
69
|
+
const minSize = type.minSize;
|
|
70
|
+
const maxSize = type.maxSize;
|
|
71
|
+
if (sszDataLength < minSize) {
|
|
72
|
+
throw new SszSnappyError({code: SszSnappyErrorCode.UNDER_SSZ_MIN_SIZE, minSize, sszDataLength});
|
|
73
|
+
}
|
|
74
|
+
if (sszDataLength > maxSize) {
|
|
75
|
+
throw new SszSnappyError({code: SszSnappyErrorCode.OVER_SSZ_MAX_SIZE, maxSize, sszDataLength});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return sszDataLength;
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
/**
|
|
64
82
|
* Reads `<encoded-payload>` for ssz-snappy and decompress.
|
|
65
83
|
* The returned bytes can be SSZ deseralized
|
|
66
84
|
*/
|
|
67
|
-
export async function readSszSnappyBody(
|
|
68
|
-
|
|
85
|
+
export async function readSszSnappyBody(
|
|
86
|
+
stream: ByteStream<Stream>,
|
|
87
|
+
sszDataLength: number,
|
|
88
|
+
signal?: AbortSignal
|
|
89
|
+
): Promise<Uint8Array> {
|
|
69
90
|
const uncompressedData = new Uint8ArrayList();
|
|
70
|
-
let
|
|
91
|
+
let encodedBytesRead = 0;
|
|
92
|
+
const maxBytes = maxEncodedLen(sszDataLength);
|
|
93
|
+
let foundIdentifier = false;
|
|
94
|
+
|
|
95
|
+
while (uncompressedData.length < sszDataLength) {
|
|
96
|
+
const header = await readExactOrSourceAborted(stream, 4, signal);
|
|
71
97
|
|
|
72
|
-
for await (const buffer of bufferedSource) {
|
|
73
98
|
// SHOULD NOT read more than max_encoded_len(n) bytes after reading the SSZ length-prefix n from the header
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
encodedBytesRead = addEncodedBytesReadOrThrow(encodedBytesRead, header.length, maxBytes, sszDataLength);
|
|
100
|
+
|
|
101
|
+
let headerParsed: {type: ChunkType; frameSize: number};
|
|
102
|
+
try {
|
|
103
|
+
headerParsed = parseSnappyFrameHeader(header.subarray());
|
|
104
|
+
if (!foundIdentifier && headerParsed.type !== ChunkType.IDENTIFIER) {
|
|
105
|
+
throw new Error("malformed input: must begin with an identifier");
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
throw new SszSnappyError({code: SszSnappyErrorCode.DECOMPRESSOR_ERROR, decompressorError: e as Error});
|
|
77
109
|
}
|
|
78
110
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
111
|
+
if (headerParsed.frameSize > maxBytes - encodedBytesRead) {
|
|
112
|
+
throw new SszSnappyError({
|
|
113
|
+
code: SszSnappyErrorCode.TOO_MUCH_BYTES_READ,
|
|
114
|
+
readBytes: encodedBytesRead + headerParsed.frameSize,
|
|
115
|
+
sszDataLength,
|
|
116
|
+
});
|
|
82
117
|
}
|
|
118
|
+
const frame = await readExactOrSourceAborted(stream, headerParsed.frameSize, signal);
|
|
119
|
+
|
|
120
|
+
encodedBytesRead = addEncodedBytesReadOrThrow(encodedBytesRead, frame.length, maxBytes, sszDataLength);
|
|
83
121
|
|
|
84
|
-
// stream contents can be passed through a buffered Snappy reader to decompress frame by frame
|
|
85
122
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
123
|
+
if (headerParsed.type === ChunkType.IDENTIFIER) {
|
|
124
|
+
foundIdentifier = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const uncompressed = decodeSnappyFrameData(headerParsed.type, frame.subarray());
|
|
88
128
|
if (uncompressed !== null) {
|
|
89
129
|
uncompressedData.append(uncompressed);
|
|
90
130
|
}
|
|
@@ -96,16 +136,34 @@ export async function readSszSnappyBody(bufferedSource: BufferedSource, sszDataL
|
|
|
96
136
|
if (uncompressedData.length > sszDataLength) {
|
|
97
137
|
throw new SszSnappyError({code: SszSnappyErrorCode.TOO_MANY_BYTES, sszDataLength});
|
|
98
138
|
}
|
|
139
|
+
}
|
|
99
140
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
141
|
+
// buffer.length === n
|
|
142
|
+
return uncompressedData.subarray(0, sszDataLength);
|
|
143
|
+
}
|
|
104
144
|
|
|
105
|
-
|
|
106
|
-
|
|
145
|
+
function addEncodedBytesReadOrThrow(
|
|
146
|
+
encodedBytesRead: number,
|
|
147
|
+
bytesToAdd: number,
|
|
148
|
+
maxBytes: number,
|
|
149
|
+
sszDataLength: number
|
|
150
|
+
): number {
|
|
151
|
+
const nextReadBytes = encodedBytesRead + bytesToAdd;
|
|
152
|
+
if (nextReadBytes > maxBytes) {
|
|
153
|
+
throw new SszSnappyError({code: SszSnappyErrorCode.TOO_MUCH_BYTES_READ, readBytes: nextReadBytes, sszDataLength});
|
|
107
154
|
}
|
|
155
|
+
return nextReadBytes;
|
|
156
|
+
}
|
|
108
157
|
|
|
109
|
-
|
|
110
|
-
|
|
158
|
+
async function readExactOrSourceAborted(
|
|
159
|
+
stream: ByteStream<Stream>,
|
|
160
|
+
bytes: number,
|
|
161
|
+
signal?: AbortSignal
|
|
162
|
+
): Promise<Uint8ArrayList> {
|
|
163
|
+
return stream.read({bytes, signal}).catch((e) => {
|
|
164
|
+
if ((e as Error).name === "UnexpectedEOFError") {
|
|
165
|
+
throw new SszSnappyError({code: SszSnappyErrorCode.SOURCE_ABORTED});
|
|
166
|
+
}
|
|
167
|
+
throw e;
|
|
168
|
+
});
|
|
111
169
|
}
|
|
@@ -8,12 +8,12 @@ import {encodeSnappy} from "../../utils/snappyIndex.js";
|
|
|
8
8
|
* <encoding-dependent-header> | <encoded-payload>
|
|
9
9
|
* ```
|
|
10
10
|
*/
|
|
11
|
-
export const writeSszSnappyPayload = encodeSszSnappy as (bytes: Uint8Array) =>
|
|
11
|
+
export const writeSszSnappyPayload = encodeSszSnappy as (bytes: Uint8Array) => Generator<Buffer>;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Buffered Snappy writer
|
|
15
15
|
*/
|
|
16
|
-
export
|
|
16
|
+
export function* encodeSszSnappy(bytes: Buffer): Generator<Buffer> {
|
|
17
17
|
// MUST encode the length of the raw SSZ bytes, encoded as an unsigned protobuf varint
|
|
18
18
|
const varint = varintEncode(bytes.length);
|
|
19
19
|
yield Buffer.from(varint.buffer, varint.byteOffset, varint.byteLength);
|
|
@@ -9,8 +9,6 @@ export enum SszSnappyErrorCode {
|
|
|
9
9
|
OVER_SSZ_MAX_SIZE = "SSZ_SNAPPY_ERROR_OVER_SSZ_MAX_SIZE",
|
|
10
10
|
TOO_MUCH_BYTES_READ = "SSZ_SNAPPY_ERROR_TOO_MUCH_BYTES_READ",
|
|
11
11
|
DECOMPRESSOR_ERROR = "SSZ_SNAPPY_ERROR_DECOMPRESSOR_ERROR",
|
|
12
|
-
DESERIALIZE_ERROR = "SSZ_SNAPPY_ERROR_DESERIALIZE_ERROR",
|
|
13
|
-
SERIALIZE_ERROR = "SSZ_SNAPPY_ERROR_SERIALIZE_ERROR",
|
|
14
12
|
/** Received more bytes than specified sszDataLength */
|
|
15
13
|
TOO_MANY_BYTES = "SSZ_SNAPPY_ERROR_TOO_MANY_BYTES",
|
|
16
14
|
/** Source aborted before reading sszDataLength bytes */
|
|
@@ -23,8 +21,6 @@ type SszSnappyErrorType =
|
|
|
23
21
|
| {code: SszSnappyErrorCode.OVER_SSZ_MAX_SIZE; maxSize: number; sszDataLength: number}
|
|
24
22
|
| {code: SszSnappyErrorCode.TOO_MUCH_BYTES_READ; readBytes: number; sszDataLength: number}
|
|
25
23
|
| {code: SszSnappyErrorCode.DECOMPRESSOR_ERROR; decompressorError: Error}
|
|
26
|
-
| {code: SszSnappyErrorCode.DESERIALIZE_ERROR; deserializeError: Error}
|
|
27
|
-
| {code: SszSnappyErrorCode.SERIALIZE_ERROR; serializeError: Error}
|
|
28
24
|
| {code: SszSnappyErrorCode.TOO_MANY_BYTES; sszDataLength: number}
|
|
29
25
|
| {code: SszSnappyErrorCode.SOURCE_ABORTED};
|
|
30
26
|
|
package/src/metrics.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {RequestErrorCode} from "./request/errors.js";
|
|
|
4
4
|
export type Metrics = ReturnType<typeof getMetrics>;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* A collection of metrics used throughout the
|
|
7
|
+
* A collection of metrics used throughout the Req/Resp domain.
|
|
8
8
|
*/
|
|
9
9
|
export function getMetrics(register: MetricsRegisterExtra) {
|
|
10
10
|
// Using function style instead of class to prevent having to re-declare all MetricsPrometheus types.
|
|
@@ -29,7 +29,6 @@ export function getMetrics(register: MetricsRegisterExtra) {
|
|
|
29
29
|
name: "beacon_reqresp_outgoing_request_roundtrip_time_seconds",
|
|
30
30
|
help: "Histogram of outgoing requests round-trip time",
|
|
31
31
|
labelNames: ["method"],
|
|
32
|
-
// Spec sets RESP_TIMEOUT = 10 sec
|
|
33
32
|
buckets: [0.1, 0.2, 0.5, 1, 5, 10, 15, 60],
|
|
34
33
|
}),
|
|
35
34
|
outgoingErrors: register.gauge<{method: string}>({
|
|
@@ -61,7 +60,6 @@ export function getMetrics(register: MetricsRegisterExtra) {
|
|
|
61
60
|
name: "beacon_reqresp_incoming_request_handler_time_seconds",
|
|
62
61
|
help: "Histogram of incoming requests internal handling time",
|
|
63
62
|
labelNames: ["method"],
|
|
64
|
-
// Spec sets RESP_TIMEOUT = 10 sec
|
|
65
63
|
buckets: [0.1, 0.2, 0.5, 1, 5, 10],
|
|
66
64
|
}),
|
|
67
65
|
incomingErrors: register.gauge<{method: string}>({
|
|
@@ -69,20 +67,6 @@ export function getMetrics(register: MetricsRegisterExtra) {
|
|
|
69
67
|
help: "Counts total failed responses handled per method",
|
|
70
68
|
labelNames: ["method"],
|
|
71
69
|
}),
|
|
72
|
-
outgoingResponseTTFB: register.histogram<{method: string}>({
|
|
73
|
-
name: "beacon_reqresp_outgoing_response_ttfb_seconds",
|
|
74
|
-
help: "Time to first byte (TTFB) for outgoing responses",
|
|
75
|
-
labelNames: ["method"],
|
|
76
|
-
// Spec sets TTFB_TIMEOUT = 5 sec
|
|
77
|
-
buckets: [0.1, 1, 5],
|
|
78
|
-
}),
|
|
79
|
-
incomingResponseTTFB: register.histogram<{method: string}>({
|
|
80
|
-
name: "beacon_reqresp_incoming_response_ttfb_seconds",
|
|
81
|
-
help: "Time to first byte (TTFB) for incoming responses",
|
|
82
|
-
labelNames: ["method"],
|
|
83
|
-
// Spec sets TTFB_TIMEOUT = 5 sec
|
|
84
|
-
buckets: [0.1, 1, 5],
|
|
85
|
-
}),
|
|
86
70
|
dialErrors: register.gauge({
|
|
87
71
|
name: "beacon_reqresp_dial_errors_total",
|
|
88
72
|
help: "Count total dial errors",
|
package/src/request/errors.ts
CHANGED
|
@@ -21,13 +21,9 @@ export enum RequestErrorCode {
|
|
|
21
21
|
REQUEST_TIMEOUT = "REQUEST_ERROR_REQUEST_TIMEOUT",
|
|
22
22
|
/** Error when sending request to responder */
|
|
23
23
|
REQUEST_ERROR = "REQUEST_ERROR_REQUEST_ERROR",
|
|
24
|
-
/** Reponder did not deliver a full reponse before max maxTotalResponseTimeout() */
|
|
25
|
-
RESPONSE_TIMEOUT = "REQUEST_ERROR_RESPONSE_TIMEOUT",
|
|
26
24
|
/** A single-response method returned 0 chunks */
|
|
27
25
|
EMPTY_RESPONSE = "REQUEST_ERROR_EMPTY_RESPONSE",
|
|
28
|
-
/**
|
|
29
|
-
TTFB_TIMEOUT = "REQUEST_ERROR_TTFB_TIMEOUT",
|
|
30
|
-
/** Timeout between `<response_chunk>` exceed */
|
|
26
|
+
/** Response transfer timeout exceeded */
|
|
31
27
|
RESP_TIMEOUT = "REQUEST_ERROR_RESP_TIMEOUT",
|
|
32
28
|
/** Request rate limited */
|
|
33
29
|
REQUEST_RATE_LIMITED = "REQUEST_ERROR_RATE_LIMITED",
|
|
@@ -50,7 +46,6 @@ type RequestErrorType =
|
|
|
50
46
|
| {code: RequestErrorCode.REQUEST_TIMEOUT}
|
|
51
47
|
| {code: RequestErrorCode.REQUEST_ERROR; error: Error}
|
|
52
48
|
| {code: RequestErrorCode.EMPTY_RESPONSE}
|
|
53
|
-
| {code: RequestErrorCode.TTFB_TIMEOUT}
|
|
54
49
|
| {code: RequestErrorCode.RESP_TIMEOUT}
|
|
55
50
|
| {code: RequestErrorCode.REQUEST_RATE_LIMITED}
|
|
56
51
|
| {code: RequestErrorCode.REQUEST_SELF_RATE_LIMITED}
|