@lodestar/reqresp 1.35.0-dev.c85be4e26c → 1.35.0-dev.c9deb9b59f
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/lib/ReqResp.d.ts +1 -1
- package/lib/ReqResp.d.ts.map +1 -0
- package/lib/ReqResp.js +16 -6
- package/lib/ReqResp.js.map +1 -1
- package/lib/encoders/requestDecode.d.ts.map +1 -0
- package/lib/encoders/requestEncode.d.ts.map +1 -0
- package/lib/encoders/responseDecode.d.ts +1 -1
- package/lib/encoders/responseDecode.d.ts.map +1 -0
- package/lib/encoders/responseDecode.js.map +1 -1
- package/lib/encoders/responseEncode.d.ts.map +1 -0
- package/lib/encodingStrategies/index.d.ts.map +1 -0
- package/lib/encodingStrategies/sszSnappy/decode.d.ts.map +1 -0
- package/lib/encodingStrategies/sszSnappy/encode.d.ts.map +1 -0
- package/lib/encodingStrategies/sszSnappy/errors.d.ts.map +1 -0
- package/lib/encodingStrategies/sszSnappy/index.d.ts +1 -1
- package/lib/encodingStrategies/sszSnappy/index.d.ts.map +1 -0
- package/lib/encodingStrategies/sszSnappy/index.js +1 -1
- package/lib/encodingStrategies/sszSnappy/snappyFrames/common.d.ts.map +1 -0
- package/lib/encodingStrategies/sszSnappy/snappyFrames/compress.d.ts.map +1 -0
- package/lib/encodingStrategies/sszSnappy/snappyFrames/uncompress.d.ts.map +1 -0
- package/lib/encodingStrategies/sszSnappy/snappyFrames/uncompress.js +4 -6
- package/lib/encodingStrategies/sszSnappy/snappyFrames/uncompress.js.map +1 -1
- package/lib/encodingStrategies/sszSnappy/utils.d.ts.map +1 -0
- package/lib/index.d.ts +7 -7
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +5 -5
- package/lib/index.js.map +1 -1
- package/lib/interface.d.ts.map +1 -0
- package/lib/metrics.d.ts.map +1 -0
- package/lib/rate_limiter/ReqRespRateLimiter.d.ts.map +1 -0
- package/lib/rate_limiter/ReqRespRateLimiter.js +8 -4
- package/lib/rate_limiter/ReqRespRateLimiter.js.map +1 -1
- package/lib/rate_limiter/rateLimiterGRCA.d.ts.map +1 -0
- package/lib/rate_limiter/rateLimiterGRCA.js +5 -3
- package/lib/rate_limiter/rateLimiterGRCA.js.map +1 -1
- package/lib/rate_limiter/selfRateLimiter.d.ts.map +1 -0
- package/lib/rate_limiter/selfRateLimiter.js +9 -2
- package/lib/rate_limiter/selfRateLimiter.js.map +1 -1
- package/lib/request/errors.d.ts.map +1 -0
- package/lib/request/index.d.ts +1 -1
- package/lib/request/index.d.ts.map +1 -0
- package/lib/request/index.js +1 -1
- package/lib/request/index.js.map +1 -1
- package/lib/response/errors.d.ts.map +1 -0
- package/lib/response/errors.js +2 -0
- package/lib/response/errors.js.map +1 -1
- package/lib/response/index.d.ts.map +1 -0
- package/lib/response/index.js +1 -1
- package/lib/response/index.js.map +1 -1
- package/lib/types.d.ts.map +1 -0
- package/lib/utils/abortableSource.d.ts.map +1 -0
- package/lib/utils/bufferedSource.d.ts.map +1 -0
- package/lib/utils/bufferedSource.js +3 -1
- package/lib/utils/bufferedSource.js.map +1 -1
- package/lib/utils/collectExactOne.d.ts.map +1 -0
- package/lib/utils/collectMaxResponse.d.ts.map +1 -0
- package/lib/utils/errorMessage.d.ts.map +1 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/onChunk.d.ts.map +1 -0
- package/lib/utils/peerId.d.ts.map +1 -0
- package/lib/utils/protocolId.d.ts.map +1 -0
- package/package.json +12 -12
- package/src/ReqResp.ts +289 -0
- package/src/encoders/requestDecode.ts +29 -0
- package/src/encoders/requestEncode.ts +18 -0
- package/src/encoders/responseDecode.ts +169 -0
- package/src/encoders/responseEncode.ts +81 -0
- package/src/encodingStrategies/index.ts +46 -0
- package/src/encodingStrategies/sszSnappy/decode.ts +111 -0
- package/src/encodingStrategies/sszSnappy/encode.ts +24 -0
- package/src/encodingStrategies/sszSnappy/errors.ts +31 -0
- package/src/encodingStrategies/sszSnappy/index.ts +3 -0
- package/src/encodingStrategies/sszSnappy/snappyFrames/common.ts +36 -0
- package/src/encodingStrategies/sszSnappy/snappyFrames/compress.ts +25 -0
- package/src/encodingStrategies/sszSnappy/snappyFrames/uncompress.ts +114 -0
- package/src/encodingStrategies/sszSnappy/utils.ts +7 -0
- package/src/index.ts +10 -0
- package/src/interface.ts +26 -0
- package/src/metrics.ts +95 -0
- package/src/rate_limiter/ReqRespRateLimiter.ts +107 -0
- package/src/rate_limiter/rateLimiterGRCA.ts +92 -0
- package/src/rate_limiter/selfRateLimiter.ts +112 -0
- package/src/request/errors.ts +119 -0
- package/src/request/index.ts +225 -0
- package/src/response/errors.ts +50 -0
- package/src/response/index.ts +147 -0
- package/src/types.ts +158 -0
- package/src/utils/abortableSource.ts +80 -0
- package/src/utils/bufferedSource.ts +46 -0
- package/src/utils/collectExactOne.ts +15 -0
- package/src/utils/collectMaxResponse.ts +19 -0
- package/src/utils/errorMessage.ts +51 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/onChunk.ts +12 -0
- package/src/utils/peerId.ts +6 -0
- package/src/utils/protocolId.ts +44 -0
package/package.json
CHANGED
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/ChainSafe/lodestar/issues"
|
|
13
13
|
},
|
|
14
|
-
"version": "1.35.0-dev.
|
|
14
|
+
"version": "1.35.0-dev.c9deb9b59f",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
+
"bun": "./src/index.ts",
|
|
18
19
|
"import": "./lib/index.js"
|
|
19
20
|
},
|
|
20
21
|
"./utils": {
|
|
22
|
+
"bun": "./src/utils/index.ts",
|
|
21
23
|
"import": "./lib/utils/index.js"
|
|
22
24
|
}
|
|
23
25
|
},
|
|
@@ -32,11 +34,9 @@
|
|
|
32
34
|
},
|
|
33
35
|
"types": "./lib/index.d.ts",
|
|
34
36
|
"files": [
|
|
35
|
-
"
|
|
36
|
-
"lib
|
|
37
|
-
"
|
|
38
|
-
"*.d.ts",
|
|
39
|
-
"*.js"
|
|
37
|
+
"src",
|
|
38
|
+
"lib",
|
|
39
|
+
"!**/*.tsbuildinfo"
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
42
|
"clean": "rm -rf lib && rm -f *.tsbuildinfo",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@chainsafe/fast-crc32c": "^4.2.0",
|
|
56
56
|
"@libp2p/interface": "^2.7.0",
|
|
57
|
-
"@lodestar/config": "1.35.0-dev.
|
|
58
|
-
"@lodestar/params": "1.35.0-dev.
|
|
59
|
-
"@lodestar/utils": "1.35.0-dev.
|
|
57
|
+
"@lodestar/config": "1.35.0-dev.c9deb9b59f",
|
|
58
|
+
"@lodestar/params": "1.35.0-dev.c9deb9b59f",
|
|
59
|
+
"@lodestar/utils": "1.35.0-dev.c9deb9b59f",
|
|
60
60
|
"it-all": "^3.0.4",
|
|
61
61
|
"it-pipe": "^3.0.1",
|
|
62
62
|
"snappy": "^7.2.2",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"uint8arraylist": "^2.4.7"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@lodestar/logger": "1.35.0-dev.
|
|
69
|
-
"@lodestar/types": "1.35.0-dev.
|
|
68
|
+
"@lodestar/logger": "1.35.0-dev.c9deb9b59f",
|
|
69
|
+
"@lodestar/types": "1.35.0-dev.c9deb9b59f",
|
|
70
70
|
"libp2p": "2.9.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"reqresp",
|
|
81
81
|
"blockchain"
|
|
82
82
|
],
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "9f18a0bd68957d2324cc3b05f36910ab08f28672"
|
|
84
84
|
}
|
package/src/ReqResp.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import {setMaxListeners} from "node:events";
|
|
2
|
+
import {Connection, PeerId, Stream} from "@libp2p/interface";
|
|
3
|
+
import type {Libp2p} from "libp2p";
|
|
4
|
+
import {Logger, MetricsRegisterExtra} from "@lodestar/utils";
|
|
5
|
+
import {Metrics, getMetrics} from "./metrics.js";
|
|
6
|
+
import {ReqRespRateLimiter} from "./rate_limiter/ReqRespRateLimiter.js";
|
|
7
|
+
import {SelfRateLimiter} from "./rate_limiter/selfRateLimiter.js";
|
|
8
|
+
import {RequestError, RequestErrorCode, SendRequestOpts, sendRequest} from "./request/index.js";
|
|
9
|
+
import {handleRequest} from "./response/index.js";
|
|
10
|
+
import {
|
|
11
|
+
DialOnlyProtocol,
|
|
12
|
+
Encoding,
|
|
13
|
+
MixedProtocol,
|
|
14
|
+
Protocol,
|
|
15
|
+
ProtocolDescriptor,
|
|
16
|
+
ReqRespRateLimiterOpts,
|
|
17
|
+
ResponseIncoming,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
import {formatProtocolID} from "./utils/protocolId.js";
|
|
20
|
+
|
|
21
|
+
type ProtocolID = string;
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_PROTOCOL_PREFIX = "/eth2/beacon_chain/req";
|
|
24
|
+
|
|
25
|
+
export interface ReqRespProtocolModules {
|
|
26
|
+
libp2p: Libp2p;
|
|
27
|
+
logger: Logger;
|
|
28
|
+
metricsRegister: MetricsRegisterExtra | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ReqRespOpts extends SendRequestOpts, ReqRespRateLimiterOpts {
|
|
32
|
+
/** Custom prefix for `/ProtocolPrefix/MessageName/SchemaVersion/Encoding` */
|
|
33
|
+
protocolPrefix?: string;
|
|
34
|
+
getPeerLogMetadata?: (peerId: string) => string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Implementation of Ethereum Consensus p2p Req/Resp domain.
|
|
39
|
+
* For the spec that this code is based on, see:
|
|
40
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-reqresp-domain
|
|
41
|
+
* https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#the-reqresp-domain
|
|
42
|
+
*/
|
|
43
|
+
export class ReqResp {
|
|
44
|
+
// protected to be usable by extending class
|
|
45
|
+
protected readonly libp2p: Libp2p;
|
|
46
|
+
protected readonly logger: Logger;
|
|
47
|
+
protected readonly metrics: Metrics | null;
|
|
48
|
+
|
|
49
|
+
// rate limit requests coming to this node to not be used by extending class
|
|
50
|
+
private readonly rateLimiter: ReqRespRateLimiter;
|
|
51
|
+
// rate limit requests sending to other peers
|
|
52
|
+
private readonly selfRateLimiter: SelfRateLimiter;
|
|
53
|
+
|
|
54
|
+
private controller = new AbortController();
|
|
55
|
+
/** Tracks request and responses in a sequential counter */
|
|
56
|
+
private reqCount = 0;
|
|
57
|
+
private readonly protocolPrefix: string;
|
|
58
|
+
|
|
59
|
+
/** `${protocolPrefix}/${method}/${version}/${encoding}` */
|
|
60
|
+
private readonly registeredProtocols = new Map<ProtocolID, MixedProtocol>();
|
|
61
|
+
private readonly dialOnlyProtocols = new Map<ProtocolID, boolean>();
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
modules: ReqRespProtocolModules,
|
|
65
|
+
private readonly opts: ReqRespOpts = {}
|
|
66
|
+
) {
|
|
67
|
+
this.libp2p = modules.libp2p;
|
|
68
|
+
this.logger = modules.logger;
|
|
69
|
+
this.metrics = modules.metricsRegister ? getMetrics(modules.metricsRegister) : null;
|
|
70
|
+
this.protocolPrefix = opts.protocolPrefix ?? DEFAULT_PROTOCOL_PREFIX;
|
|
71
|
+
this.rateLimiter = new ReqRespRateLimiter(opts);
|
|
72
|
+
this.selfRateLimiter = new SelfRateLimiter();
|
|
73
|
+
|
|
74
|
+
this.metrics?.selfRateLimiterPeerCount.addCollect(() => {
|
|
75
|
+
this.metrics?.selfRateLimiterPeerCount.set(this.selfRateLimiter.getPeerCount());
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Register protocol which will be used only to dial to other peers
|
|
81
|
+
* The libp2p instance will not handle that protocol
|
|
82
|
+
*
|
|
83
|
+
* Made it explicit method to avoid any developer mistake
|
|
84
|
+
*/
|
|
85
|
+
registerDialOnlyProtocol(protocol: DialOnlyProtocol): void {
|
|
86
|
+
const protocolID = this.formatProtocolID(protocol);
|
|
87
|
+
|
|
88
|
+
this.registeredProtocols.set(protocolID, protocol);
|
|
89
|
+
this.dialOnlyProtocols.set(protocolID, true);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Register protocol as supported and to libp2p.
|
|
94
|
+
* async because libp2p registrar persists the new protocol list in the peer-store.
|
|
95
|
+
* Overrides handler and rate limits in case protocol is already registered.
|
|
96
|
+
* Can be called at any time, no concept of started / stopped
|
|
97
|
+
*/
|
|
98
|
+
async registerProtocol(protocol: Protocol): Promise<void> {
|
|
99
|
+
const protocolID = this.formatProtocolID(protocol);
|
|
100
|
+
const {handler: _handler, inboundRateLimits, ...rest} = protocol;
|
|
101
|
+
|
|
102
|
+
this.registerDialOnlyProtocol(rest);
|
|
103
|
+
this.dialOnlyProtocols.set(protocolID, false);
|
|
104
|
+
|
|
105
|
+
if (inboundRateLimits) {
|
|
106
|
+
this.rateLimiter.setRateLimits(protocolID, inboundRateLimits);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this.libp2p.handle(protocolID, this.getRequestHandler(protocol, protocolID), {force: true});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Remove protocol as supported and from libp2p.
|
|
114
|
+
* async because libp2p registrar persists the new protocol list in the peer-store.
|
|
115
|
+
* Does NOT throw if the protocolID is unknown.
|
|
116
|
+
* Can be called at any time, no concept of started / stopped
|
|
117
|
+
*/
|
|
118
|
+
async unregisterProtocol(protocolID: ProtocolID): Promise<void> {
|
|
119
|
+
this.registeredProtocols.delete(protocolID);
|
|
120
|
+
|
|
121
|
+
return this.libp2p.unhandle(protocolID);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Remove all registered protocols from libp2p
|
|
126
|
+
*/
|
|
127
|
+
async unregisterAllProtocols(): Promise<void> {
|
|
128
|
+
for (const protocolID of this.registeredProtocols.keys()) {
|
|
129
|
+
await this.unregisterProtocol(protocolID);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getRegisteredProtocols(): ProtocolID[] {
|
|
134
|
+
return Array.from(this.registeredProtocols.values()).map((protocol) => this.formatProtocolID(protocol));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async start(): Promise<void> {
|
|
138
|
+
this.controller = new AbortController();
|
|
139
|
+
this.rateLimiter.start();
|
|
140
|
+
this.selfRateLimiter.start();
|
|
141
|
+
// We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10
|
|
142
|
+
// Since it is perfectly fine to have listeners > 10
|
|
143
|
+
setMaxListeners(Infinity, this.controller.signal);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async stop(): Promise<void> {
|
|
147
|
+
this.rateLimiter.stop();
|
|
148
|
+
this.selfRateLimiter.stop();
|
|
149
|
+
this.controller.abort();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Helper to reduce code duplication
|
|
153
|
+
async *sendRequest(
|
|
154
|
+
peerId: PeerId,
|
|
155
|
+
method: string,
|
|
156
|
+
versions: number[],
|
|
157
|
+
encoding: Encoding,
|
|
158
|
+
body: Uint8Array
|
|
159
|
+
): AsyncIterable<ResponseIncoming> {
|
|
160
|
+
const peerIdStr = peerId.toString();
|
|
161
|
+
const peerClient = this.opts.getPeerLogMetadata?.(peerIdStr);
|
|
162
|
+
this.metrics?.outgoingRequests.inc({method});
|
|
163
|
+
const timer = this.metrics?.outgoingRequestRoundtripTime.startTimer({method});
|
|
164
|
+
|
|
165
|
+
const protocols: (MixedProtocol | DialOnlyProtocol)[] = [];
|
|
166
|
+
const protocolIDs: string[] = [];
|
|
167
|
+
// don't increase this.reqCount until we know request will be sent
|
|
168
|
+
const requestId = this.reqCount + 1;
|
|
169
|
+
|
|
170
|
+
for (const version of versions) {
|
|
171
|
+
const protocolID = this.formatProtocolID({method, version, encoding});
|
|
172
|
+
const protocol = this.registeredProtocols.get(protocolID);
|
|
173
|
+
if (!protocol) {
|
|
174
|
+
throw Error(`Request to send to protocol ${protocolID} but it has not been declared`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!this.selfRateLimiter.allows(peerIdStr, protocolID, requestId)) {
|
|
178
|
+
// we technically don't send request in this case but would be nice just to track this in the same `outgoingErrorReasons` metric
|
|
179
|
+
this.metrics?.outgoingErrorReasons.inc({reason: RequestErrorCode.REQUEST_SELF_RATE_LIMITED});
|
|
180
|
+
throw new RequestError({code: RequestErrorCode.REQUEST_SELF_RATE_LIMITED});
|
|
181
|
+
// don't call this.onOutgoingRequestError() to penalize peer
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
protocols.push(protocol);
|
|
185
|
+
protocolIDs.push(protocolID);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// requestId is now the same to reqCount
|
|
189
|
+
this.reqCount++;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
yield* sendRequest(
|
|
193
|
+
{logger: this.logger, libp2p: this.libp2p, metrics: this.metrics, peerClient},
|
|
194
|
+
peerId,
|
|
195
|
+
protocols,
|
|
196
|
+
protocolIDs,
|
|
197
|
+
body,
|
|
198
|
+
this.controller.signal,
|
|
199
|
+
this.opts,
|
|
200
|
+
requestId
|
|
201
|
+
);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
this.metrics?.outgoingErrors.inc({method});
|
|
204
|
+
|
|
205
|
+
if (e instanceof RequestError) {
|
|
206
|
+
if (e.type.code === RequestErrorCode.DIAL_ERROR || e.type.code === RequestErrorCode.DIAL_TIMEOUT) {
|
|
207
|
+
this.metrics?.dialErrors.inc();
|
|
208
|
+
}
|
|
209
|
+
this.metrics?.outgoingErrorReasons.inc({reason: e.type.code});
|
|
210
|
+
|
|
211
|
+
this.onOutgoingRequestError(peerId, method, e);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw e;
|
|
215
|
+
} finally {
|
|
216
|
+
for (const protocolID of protocolIDs) {
|
|
217
|
+
this.selfRateLimiter.requestCompleted(peerIdStr, protocolID, requestId);
|
|
218
|
+
}
|
|
219
|
+
timer?.();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private getRequestHandler(protocol: MixedProtocol, protocolID: string) {
|
|
224
|
+
return async ({connection, stream}: {connection: Connection; stream: Stream}) => {
|
|
225
|
+
if (this.dialOnlyProtocols.get(protocolID)) {
|
|
226
|
+
throw new Error(`Received request on dial only protocol '${protocolID}'`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const peerId = connection.remotePeer;
|
|
230
|
+
const peerClient = this.opts.getPeerLogMetadata?.(peerId.toString());
|
|
231
|
+
const {method} = protocol;
|
|
232
|
+
|
|
233
|
+
this.metrics?.incomingRequests.inc({method});
|
|
234
|
+
const timer = this.metrics?.incomingRequestHandlerTime.startTimer({method});
|
|
235
|
+
|
|
236
|
+
this.onIncomingRequest?.(peerId, protocol);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
await handleRequest({
|
|
240
|
+
logger: this.logger,
|
|
241
|
+
metrics: this.metrics,
|
|
242
|
+
stream,
|
|
243
|
+
peerId,
|
|
244
|
+
protocol: protocol as Protocol,
|
|
245
|
+
protocolID,
|
|
246
|
+
rateLimiter: this.rateLimiter,
|
|
247
|
+
signal: this.controller.signal,
|
|
248
|
+
requestId: this.reqCount++,
|
|
249
|
+
peerClient,
|
|
250
|
+
requestTimeoutMs: this.opts.requestTimeoutMs,
|
|
251
|
+
});
|
|
252
|
+
// TODO: Do success peer scoring here
|
|
253
|
+
} catch (err) {
|
|
254
|
+
this.metrics?.incomingErrors.inc({method});
|
|
255
|
+
|
|
256
|
+
if (err instanceof RequestError) {
|
|
257
|
+
this.onIncomingRequestError(protocol as ProtocolDescriptor, err);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// TODO: Do error peer scoring here
|
|
261
|
+
// Must not throw since this is an event handler
|
|
262
|
+
} finally {
|
|
263
|
+
timer?.();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
protected onIncomingRequest(_peerId: PeerId, _protocol: ProtocolDescriptor): void {
|
|
269
|
+
// Override
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
protected onIncomingRequestError(_protocol: ProtocolDescriptor, _error: RequestError): void {
|
|
273
|
+
// Override
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
protected onOutgoingRequestError(_peerId: PeerId, _method: string, _error: RequestError): void {
|
|
277
|
+
// Override
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* ```
|
|
282
|
+
* /ProtocolPrefix/MessageName/SchemaVersion/Encoding
|
|
283
|
+
* ```
|
|
284
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/phase0/p2p-interface.md#protocol-identification
|
|
285
|
+
*/
|
|
286
|
+
protected formatProtocolID(protocol: Pick<MixedProtocol, "method" | "version" | "encoding">): string {
|
|
287
|
+
return formatProtocolID(this.protocolPrefix, protocol.method, protocol.version, protocol.encoding);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {Sink} from "it-stream-types";
|
|
2
|
+
import {Uint8ArrayList} from "uint8arraylist";
|
|
3
|
+
import {readEncodedPayload} from "../encodingStrategies/index.js";
|
|
4
|
+
import {MixedProtocol} from "../types.js";
|
|
5
|
+
import {BufferedSource} from "../utils/index.js";
|
|
6
|
+
|
|
7
|
+
const EMPTY_DATA = new Uint8Array();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Consumes a stream source to read a `<request>`
|
|
11
|
+
* ```bnf
|
|
12
|
+
* request ::= <encoding-dependent-header> | <encoded-payload>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function requestDecode(
|
|
16
|
+
protocol: MixedProtocol
|
|
17
|
+
): Sink<AsyncIterable<Uint8Array | Uint8ArrayList>, Promise<Uint8Array>> {
|
|
18
|
+
return async function requestDecodeSink(source) {
|
|
19
|
+
const type = protocol.requestSizes;
|
|
20
|
+
if (type === null) {
|
|
21
|
+
// method has no body
|
|
22
|
+
return EMPTY_DATA;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Request has a single payload, so return immediately
|
|
26
|
+
const bufferedSource = new BufferedSource(source as AsyncGenerator<Uint8ArrayList>);
|
|
27
|
+
return readEncodedPayload(bufferedSource, protocol.encoding, type);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {writeEncodedPayload} from "../encodingStrategies/index.js";
|
|
2
|
+
import {MixedProtocol} from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Yields byte chunks for a `<request>`
|
|
6
|
+
* ```bnf
|
|
7
|
+
* request ::= <encoding-dependent-header> | <encoded-payload>
|
|
8
|
+
* ```
|
|
9
|
+
* Requests may contain no payload (e.g. /eth2/beacon_chain/req/metadata/1/)
|
|
10
|
+
* if so, it would yield no byte chunks
|
|
11
|
+
*/
|
|
12
|
+
export async function* requestEncode(protocol: MixedProtocol, requestBody: Uint8Array): AsyncGenerator<Buffer> {
|
|
13
|
+
const type = protocol.requestSizes;
|
|
14
|
+
|
|
15
|
+
if (type && requestBody !== null) {
|
|
16
|
+
yield* writeEncodedPayload(requestBody, protocol.encoding);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {Uint8ArrayList} from "uint8arraylist";
|
|
2
|
+
import {ForkName} from "@lodestar/params";
|
|
3
|
+
import {readEncodedPayload} from "../encodingStrategies/index.js";
|
|
4
|
+
import {RespStatus} from "../interface.js";
|
|
5
|
+
import {ResponseError} from "../response/index.js";
|
|
6
|
+
import {
|
|
7
|
+
CONTEXT_BYTES_FORK_DIGEST_LENGTH,
|
|
8
|
+
ContextBytesFactory,
|
|
9
|
+
ContextBytesType,
|
|
10
|
+
MixedProtocol,
|
|
11
|
+
ResponseIncoming,
|
|
12
|
+
} from "../types.js";
|
|
13
|
+
import {BufferedSource, decodeErrorMessage} from "../utils/index.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Internal helper type to signal stream ended early
|
|
17
|
+
*/
|
|
18
|
+
enum StreamStatus {
|
|
19
|
+
Ended = "STREAM_ENDED",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Consumes a stream source to read a `<response>`
|
|
24
|
+
* ```bnf
|
|
25
|
+
* response ::= <response_chunk>*
|
|
26
|
+
* response_chunk ::= <result> | <context-bytes> | <encoding-dependent-header> | <encoded-payload>
|
|
27
|
+
* result ::= "0" | "1" | "2" | ["128" ... "255"]
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function responseDecode(
|
|
31
|
+
protocol: MixedProtocol,
|
|
32
|
+
cbs: {
|
|
33
|
+
onFirstHeader: () => void;
|
|
34
|
+
onFirstResponseChunk: () => void;
|
|
35
|
+
}
|
|
36
|
+
): (source: AsyncIterable<Uint8Array | Uint8ArrayList>) => AsyncIterable<ResponseIncoming> {
|
|
37
|
+
return async function* responseDecodeSink(source) {
|
|
38
|
+
const bufferedSource = new BufferedSource(source as AsyncGenerator<Uint8ArrayList>);
|
|
39
|
+
|
|
40
|
+
let readFirstHeader = false;
|
|
41
|
+
let readFirstResponseChunk = false;
|
|
42
|
+
|
|
43
|
+
// Consumers of `responseDecode()` may limit the number of <response_chunk> and break out of the while loop
|
|
44
|
+
while (!bufferedSource.isDone) {
|
|
45
|
+
const status = await readResultHeader(bufferedSource);
|
|
46
|
+
|
|
47
|
+
// Stream is only allowed to end at the start of a <response_chunk> block
|
|
48
|
+
// The happens when source ends before readResultHeader() can fetch 1 byte
|
|
49
|
+
if (status === StreamStatus.Ended) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!readFirstHeader) {
|
|
54
|
+
cbs.onFirstHeader();
|
|
55
|
+
readFirstHeader = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// For multiple chunks, only the last chunk is allowed to have a non-zero error
|
|
59
|
+
// code (i.e. The chunk stream is terminated once an error occurs
|
|
60
|
+
if (status !== RespStatus.SUCCESS) {
|
|
61
|
+
const errorMessage = await readErrorMessage(bufferedSource);
|
|
62
|
+
throw new ResponseError(status, errorMessage);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const forkName = await readContextBytes(protocol.contextBytes, bufferedSource);
|
|
66
|
+
const typeSizes = protocol.responseSizes(forkName);
|
|
67
|
+
const chunkData = await readEncodedPayload(bufferedSource, protocol.encoding, typeSizes);
|
|
68
|
+
|
|
69
|
+
yield {
|
|
70
|
+
data: chunkData,
|
|
71
|
+
fork: forkName,
|
|
72
|
+
protocolVersion: protocol.version,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!readFirstResponseChunk) {
|
|
76
|
+
cbs.onFirstResponseChunk();
|
|
77
|
+
readFirstResponseChunk = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Consumes a stream source to read a `<result>`
|
|
85
|
+
* ```bnf
|
|
86
|
+
* result ::= "0" | "1" | "2" | ["128" ... "255"]
|
|
87
|
+
* ```
|
|
88
|
+
* `<response_chunk>` starts with a single-byte response code which determines the contents of the response_chunk
|
|
89
|
+
*/
|
|
90
|
+
export async function readResultHeader(bufferedSource: BufferedSource): Promise<RespStatus | StreamStatus> {
|
|
91
|
+
for await (const buffer of bufferedSource) {
|
|
92
|
+
const status = buffer.get(0);
|
|
93
|
+
buffer.consume(1);
|
|
94
|
+
|
|
95
|
+
// If first chunk had zero bytes status === null, get next
|
|
96
|
+
if (status !== null) {
|
|
97
|
+
return status;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return StreamStatus.Ended;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Consumes a stream source to read an optional `<error_response>?`
|
|
106
|
+
* ```bnf
|
|
107
|
+
* error_response ::= <result> | <error_message>?
|
|
108
|
+
* result ::= "1" | "2" | ["128" ... "255"]
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export async function readErrorMessage(bufferedSource: BufferedSource): Promise<string> {
|
|
112
|
+
// Read at least 256 or wait for the stream to end
|
|
113
|
+
let length: number | undefined;
|
|
114
|
+
for await (const buffer of bufferedSource) {
|
|
115
|
+
// Wait for next chunk with bytes or for the stream to end
|
|
116
|
+
// Note: The entire <error_message> is expected to be in the same chunk
|
|
117
|
+
if (buffer.length >= 256) {
|
|
118
|
+
length = 256;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
length = buffer.length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// biome-ignore lint/complexity/useLiteralKeys: It is a private attribute
|
|
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
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Consumes a stream source to read a variable length `<context-bytes>` depending on the method.
|
|
137
|
+
* While `<context-bytes>` has a single type of `ForkDigest`, this function only parses the `ForkName`
|
|
138
|
+
* of the `ForkDigest` or defaults to `phase0`
|
|
139
|
+
*/
|
|
140
|
+
export async function readContextBytes(
|
|
141
|
+
contextBytes: ContextBytesFactory,
|
|
142
|
+
bufferedSource: BufferedSource
|
|
143
|
+
): Promise<ForkName> {
|
|
144
|
+
switch (contextBytes.type) {
|
|
145
|
+
case ContextBytesType.Empty:
|
|
146
|
+
return ForkName.phase0;
|
|
147
|
+
|
|
148
|
+
case ContextBytesType.ForkDigest: {
|
|
149
|
+
const forkDigest = await readContextBytesForkDigest(bufferedSource);
|
|
150
|
+
return contextBytes.config.forkDigest2ForkBoundary(forkDigest).fork;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Consumes a stream source to read `<context-bytes>`, where it's a fixed-width 4 byte
|
|
157
|
+
*/
|
|
158
|
+
export async function readContextBytesForkDigest(bufferedSource: BufferedSource): Promise<Uint8Array> {
|
|
159
|
+
for await (const buffer of bufferedSource) {
|
|
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");
|
|
169
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {writeEncodedPayload} from "../encodingStrategies/index.js";
|
|
2
|
+
import {RespStatus, RpcResponseStatusError} from "../interface.js";
|
|
3
|
+
import {ContextBytesFactory, ContextBytesType, MixedProtocol, Protocol, ResponseOutgoing} from "../types.js";
|
|
4
|
+
import {encodeErrorMessage} from "../utils/index.js";
|
|
5
|
+
|
|
6
|
+
const SUCCESS_BUFFER = Buffer.from([RespStatus.SUCCESS]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Yields byte chunks for a `<response>` with a zero response code `<result>`
|
|
10
|
+
* ```bnf
|
|
11
|
+
* response ::= <response_chunk>*
|
|
12
|
+
* response_chunk ::= <result> | <context-bytes> | <encoding-dependent-header> | <encoded-payload>
|
|
13
|
+
* result ::= "0"
|
|
14
|
+
* ```
|
|
15
|
+
* Note: `response` has zero or more chunks (denoted by `<>*`)
|
|
16
|
+
*/
|
|
17
|
+
export function responseEncodeSuccess(
|
|
18
|
+
protocol: Protocol,
|
|
19
|
+
cbs: {onChunk: (chunkIndex: number) => void}
|
|
20
|
+
): (source: AsyncIterable<ResponseOutgoing>) => AsyncIterable<Buffer> {
|
|
21
|
+
return async function* responseEncodeSuccessTransform(source) {
|
|
22
|
+
let chunkIndex = 0;
|
|
23
|
+
|
|
24
|
+
for await (const chunk of source) {
|
|
25
|
+
// Postfix increment, return 0 as first chunk
|
|
26
|
+
cbs.onChunk(chunkIndex++);
|
|
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);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Yields byte chunks for a `<response_chunk>` with a non-zero response code `<result>`
|
|
45
|
+
* denoted as `<error_response>`
|
|
46
|
+
* ```bnf
|
|
47
|
+
* error_response ::= <result> | <error_message>?
|
|
48
|
+
* result ::= "1" | "2" | ["128" ... "255"]
|
|
49
|
+
* ```
|
|
50
|
+
* Only the last `<response_chunk>` is allowed to have a non-zero error code, so this
|
|
51
|
+
* fn yields exactly one `<error_response>` and afterwards the stream must be terminated
|
|
52
|
+
*/
|
|
53
|
+
export async function* responseEncodeError(
|
|
54
|
+
protocol: Pick<MixedProtocol, "encoding">,
|
|
55
|
+
status: RpcResponseStatusError,
|
|
56
|
+
errorMessage: string
|
|
57
|
+
): AsyncGenerator<Buffer> {
|
|
58
|
+
// <result>
|
|
59
|
+
yield Buffer.from([status]);
|
|
60
|
+
|
|
61
|
+
// <error_message>? is optional
|
|
62
|
+
if (errorMessage) {
|
|
63
|
+
yield* encodeErrorMessage(errorMessage, protocol.encoding);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Yields byte chunks for a `<context-bytes>`. See `ContextBytesType` for possible types.
|
|
69
|
+
* This item is mandatory but may be empty.
|
|
70
|
+
*/
|
|
71
|
+
function getContextBytes(contextBytes: ContextBytesFactory, chunk: ResponseOutgoing): Uint8Array | null {
|
|
72
|
+
switch (contextBytes.type) {
|
|
73
|
+
// Yield nothing
|
|
74
|
+
case ContextBytesType.Empty:
|
|
75
|
+
return null;
|
|
76
|
+
|
|
77
|
+
// Yield a fixed-width 4 byte chunk, set to the `ForkDigest`
|
|
78
|
+
case ContextBytesType.ForkDigest:
|
|
79
|
+
return contextBytes.config.forkBoundary2ForkDigest(chunk.boundary);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {Encoding, TypeSizes} from "../types.js";
|
|
2
|
+
import {BufferedSource} from "../utils/index.js";
|
|
3
|
+
import {readSszSnappyPayload} from "./sszSnappy/decode.js";
|
|
4
|
+
import {writeSszSnappyPayload} from "./sszSnappy/encode.js";
|
|
5
|
+
|
|
6
|
+
// For more info about Ethereum Consensus request/response encoding strategies, see:
|
|
7
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#encoding-strategies
|
|
8
|
+
// Supported encoding strategies:
|
|
9
|
+
// - ssz_snappy
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Consumes a stream source to read encoded header and payload as defined in the spec:
|
|
13
|
+
* ```
|
|
14
|
+
* <encoding-dependent-header> | <encoded-payload>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export async function readEncodedPayload(
|
|
18
|
+
bufferedSource: BufferedSource,
|
|
19
|
+
encoding: Encoding,
|
|
20
|
+
type: TypeSizes
|
|
21
|
+
): Promise<Uint8Array> {
|
|
22
|
+
switch (encoding) {
|
|
23
|
+
case Encoding.SSZ_SNAPPY:
|
|
24
|
+
return readSszSnappyPayload(bufferedSource, type);
|
|
25
|
+
|
|
26
|
+
default:
|
|
27
|
+
throw Error("Unsupported encoding");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Yields byte chunks for encoded header and payload as defined in the spec:
|
|
33
|
+
* ```
|
|
34
|
+
* <encoding-dependent-header> | <encoded-payload>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export async function* writeEncodedPayload(chunkData: Uint8Array, encoding: Encoding): AsyncGenerator<Buffer> {
|
|
38
|
+
switch (encoding) {
|
|
39
|
+
case Encoding.SSZ_SNAPPY:
|
|
40
|
+
yield* writeSszSnappyPayload(chunkData);
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
default:
|
|
44
|
+
throw Error("Unsupported encoding");
|
|
45
|
+
}
|
|
46
|
+
}
|