@lodestar/beacon-node 1.50.0-dev.c02b971f5b → 1.50.0-dev.c24762bc60

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 (162) hide show
  1. package/lib/api/impl/beacon/state/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/state/index.js +6 -4
  3. package/lib/api/impl/beacon/state/index.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +26 -0
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/beaconProposerCache.d.ts +6 -0
  8. package/lib/chain/beaconProposerCache.d.ts.map +1 -1
  9. package/lib/chain/beaconProposerCache.js +16 -0
  10. package/lib/chain/beaconProposerCache.js.map +1 -1
  11. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  12. package/lib/chain/blocks/importBlock.js +7 -5
  13. package/lib/chain/blocks/importBlock.js.map +1 -1
  14. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  15. package/lib/chain/blocks/verifyBlock.js +6 -6
  16. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  17. package/lib/chain/chain.d.ts +1 -0
  18. package/lib/chain/chain.d.ts.map +1 -1
  19. package/lib/chain/chain.js +49 -15
  20. package/lib/chain/chain.js.map +1 -1
  21. package/lib/chain/interface.d.ts +2 -0
  22. package/lib/chain/interface.d.ts.map +1 -1
  23. package/lib/chain/opPools/opPool.d.ts.map +1 -1
  24. package/lib/chain/opPools/opPool.js +1 -23
  25. package/lib/chain/opPools/opPool.js.map +1 -1
  26. package/lib/chain/opPools/proposerPreferencesPool.d.ts +4 -0
  27. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  28. package/lib/chain/opPools/proposerPreferencesPool.js +13 -1
  29. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  30. package/lib/chain/opPools/utils.d.ts +11 -0
  31. package/lib/chain/opPools/utils.d.ts.map +1 -1
  32. package/lib/chain/opPools/utils.js +22 -0
  33. package/lib/chain/opPools/utils.js.map +1 -1
  34. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  35. package/lib/chain/prepareNextSlot.js +11 -3
  36. package/lib/chain/prepareNextSlot.js.map +1 -1
  37. package/lib/chain/produceBlock/produceBlockBody.d.ts +8 -0
  38. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  39. package/lib/chain/produceBlock/produceBlockBody.js +40 -28
  40. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  41. package/lib/chain/shufflingCache.d.ts +17 -8
  42. package/lib/chain/shufflingCache.d.ts.map +1 -1
  43. package/lib/chain/shufflingCache.js +68 -18
  44. package/lib/chain/shufflingCache.js.map +1 -1
  45. package/lib/db/beacon.d.ts +2 -1
  46. package/lib/db/beacon.d.ts.map +1 -1
  47. package/lib/db/beacon.js +3 -1
  48. package/lib/db/beacon.js.map +1 -1
  49. package/lib/db/buckets.d.ts +2 -1
  50. package/lib/db/buckets.d.ts.map +1 -1
  51. package/lib/db/buckets.js +1 -0
  52. package/lib/db/buckets.js.map +1 -1
  53. package/lib/db/interface.d.ts +2 -1
  54. package/lib/db/interface.d.ts.map +1 -1
  55. package/lib/db/repositories/blockArchiveIndex.d.ts +2 -2
  56. package/lib/db/repositories/blockArchiveIndex.d.ts.map +1 -1
  57. package/lib/db/repositories/index.d.ts +1 -0
  58. package/lib/db/repositories/index.d.ts.map +1 -1
  59. package/lib/db/repositories/index.js +1 -0
  60. package/lib/db/repositories/index.js.map +1 -1
  61. package/lib/db/repositories/proposerPreferences.d.ts +14 -0
  62. package/lib/db/repositories/proposerPreferences.d.ts.map +1 -0
  63. package/lib/db/repositories/proposerPreferences.js +21 -0
  64. package/lib/db/repositories/proposerPreferences.js.map +1 -0
  65. package/lib/execution/engine/blobVersionedHashes.d.ts +9 -0
  66. package/lib/execution/engine/blobVersionedHashes.d.ts.map +1 -0
  67. package/lib/execution/engine/blobVersionedHashes.js +74 -0
  68. package/lib/execution/engine/blobVersionedHashes.js.map +1 -0
  69. package/lib/execution/engine/http.d.ts +45 -13
  70. package/lib/execution/engine/http.d.ts.map +1 -1
  71. package/lib/execution/engine/http.js +236 -198
  72. package/lib/execution/engine/http.js.map +1 -1
  73. package/lib/execution/engine/index.d.ts +3 -2
  74. package/lib/execution/engine/index.d.ts.map +1 -1
  75. package/lib/execution/engine/index.js +33 -8
  76. package/lib/execution/engine/index.js.map +1 -1
  77. package/lib/execution/engine/interface.d.ts +1 -1
  78. package/lib/execution/engine/interface.d.ts.map +1 -1
  79. package/lib/execution/engine/jsonRpcTransport.d.ts +25 -0
  80. package/lib/execution/engine/jsonRpcTransport.d.ts.map +1 -0
  81. package/lib/execution/engine/jsonRpcTransport.js +158 -0
  82. package/lib/execution/engine/jsonRpcTransport.js.map +1 -0
  83. package/lib/execution/engine/restHttpClient.d.ts +71 -0
  84. package/lib/execution/engine/restHttpClient.d.ts.map +1 -0
  85. package/lib/execution/engine/restHttpClient.js +201 -0
  86. package/lib/execution/engine/restHttpClient.js.map +1 -0
  87. package/lib/execution/engine/restTransport.d.ts +46 -0
  88. package/lib/execution/engine/restTransport.d.ts.map +1 -0
  89. package/lib/execution/engine/restTransport.js +399 -0
  90. package/lib/execution/engine/restTransport.js.map +1 -0
  91. package/lib/execution/engine/sszTypes.d.ts +1261 -0
  92. package/lib/execution/engine/sszTypes.d.ts.map +1 -0
  93. package/lib/execution/engine/sszTypes.js +216 -0
  94. package/lib/execution/engine/sszTypes.js.map +1 -0
  95. package/lib/execution/engine/transport.d.ts +39 -0
  96. package/lib/execution/engine/transport.d.ts.map +1 -0
  97. package/lib/execution/engine/transport.js +2 -0
  98. package/lib/execution/engine/transport.js.map +1 -0
  99. package/lib/execution/engine/types.d.ts +2 -0
  100. package/lib/execution/engine/types.d.ts.map +1 -1
  101. package/lib/execution/engine/types.js +24 -57
  102. package/lib/execution/engine/types.js.map +1 -1
  103. package/lib/execution/engine/utils.d.ts.map +1 -1
  104. package/lib/execution/engine/utils.js +9 -2
  105. package/lib/execution/engine/utils.js.map +1 -1
  106. package/lib/metrics/metrics/lodestar.d.ts +9 -0
  107. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  108. package/lib/metrics/metrics/lodestar.js +18 -0
  109. package/lib/metrics/metrics/lodestar.js.map +1 -1
  110. package/lib/network/core/events.d.ts +5 -1
  111. package/lib/network/core/events.d.ts.map +1 -1
  112. package/lib/network/core/events.js +8 -0
  113. package/lib/network/core/events.js.map +1 -1
  114. package/lib/network/processor/gossipHandlers.js +1 -1
  115. package/lib/network/processor/gossipHandlers.js.map +1 -1
  116. package/lib/node/options.d.ts +3 -2
  117. package/lib/node/options.d.ts.map +1 -1
  118. package/lib/node/options.js +2 -2
  119. package/lib/node/options.js.map +1 -1
  120. package/lib/util/asyncIterableToEvents.d.ts +10 -2
  121. package/lib/util/asyncIterableToEvents.d.ts.map +1 -1
  122. package/lib/util/asyncIterableToEvents.js +29 -3
  123. package/lib/util/asyncIterableToEvents.js.map +1 -1
  124. package/lib/util/metadata.d.ts.map +1 -1
  125. package/lib/util/metadata.js +2 -1
  126. package/lib/util/metadata.js.map +1 -1
  127. package/package.json +16 -15
  128. package/src/api/impl/beacon/state/index.ts +13 -4
  129. package/src/api/impl/validator/index.ts +37 -0
  130. package/src/chain/beaconProposerCache.ts +19 -1
  131. package/src/chain/blocks/importBlock.ts +8 -4
  132. package/src/chain/blocks/verifyBlock.ts +7 -6
  133. package/src/chain/chain.ts +59 -15
  134. package/src/chain/interface.ts +2 -0
  135. package/src/chain/opPools/opPool.ts +1 -31
  136. package/src/chain/opPools/proposerPreferencesPool.ts +20 -1
  137. package/src/chain/opPools/utils.ts +31 -0
  138. package/src/chain/prepareNextSlot.ts +15 -3
  139. package/src/chain/produceBlock/produceBlockBody.ts +68 -34
  140. package/src/chain/shufflingCache.ts +79 -22
  141. package/src/db/beacon.ts +3 -0
  142. package/src/db/buckets.ts +1 -0
  143. package/src/db/interface.ts +2 -0
  144. package/src/db/repositories/index.ts +1 -0
  145. package/src/db/repositories/proposerPreferences.ts +23 -0
  146. package/src/execution/engine/blobVersionedHashes.ts +72 -0
  147. package/src/execution/engine/http.ts +304 -271
  148. package/src/execution/engine/index.ts +41 -7
  149. package/src/execution/engine/interface.ts +1 -1
  150. package/src/execution/engine/jsonRpcTransport.ts +247 -0
  151. package/src/execution/engine/restHttpClient.ts +269 -0
  152. package/src/execution/engine/restTransport.ts +544 -0
  153. package/src/execution/engine/sszTypes.ts +354 -0
  154. package/src/execution/engine/transport.ts +61 -0
  155. package/src/execution/engine/types.ts +34 -74
  156. package/src/execution/engine/utils.ts +12 -2
  157. package/src/metrics/metrics/lodestar.ts +19 -0
  158. package/src/network/core/events.ts +16 -1
  159. package/src/network/processor/gossipHandlers.ts +1 -1
  160. package/src/node/options.ts +10 -1
  161. package/src/util/asyncIterableToEvents.ts +44 -5
  162. package/src/util/metadata.ts +2 -1
@@ -0,0 +1,544 @@
1
+ import {Type} from "@chainsafe/ssz";
2
+ import {CELLS_PER_EXT_BLOB, ForkName, ForkSeq, isForkPostBellatrix} from "@lodestar/params";
3
+ import {ExecutionPayload, ExecutionRequests, Root, RootHex, capella, deneb, electra, gloas} from "@lodestar/types";
4
+ import {BlobAndProof} from "@lodestar/types/deneb";
5
+ import {BlobAndProofV2} from "@lodestar/types/fulu";
6
+ import {LodestarError, fromHex, strip0xPrefix, toHex, toRootHex} from "@lodestar/utils";
7
+ import {isValidBlobVersionedHashes} from "./blobVersionedHashes.js";
8
+ import {
9
+ ClientCode,
10
+ ClientVersion,
11
+ ExecutionPayloadStatus,
12
+ PayloadAttributes,
13
+ PayloadId,
14
+ VersionedHashes,
15
+ } from "./interface.js";
16
+ import {JsonRpcHttpClientEventEmitter, ReqOpts} from "./jsonRpcHttpClient.js";
17
+ import {EngineRestHttpClient, EngineRestResponseError} from "./restHttpClient.js";
18
+ import {
19
+ BlobsRequest,
20
+ BlobsV1Response,
21
+ BlobsV2Response,
22
+ BodiesByHashRequest,
23
+ BodiesResponseBellatrix,
24
+ BodiesResponseCapella,
25
+ BodiesResponseGloas,
26
+ BuiltPayloadBellatrix,
27
+ BuiltPayloadCapella,
28
+ BuiltPayloadDeneb,
29
+ BuiltPayloadElectra,
30
+ BuiltPayloadFulu,
31
+ BuiltPayloadGloas,
32
+ ExecutionForkName,
33
+ ExecutionPayloadEnvelopeBellatrix,
34
+ ExecutionPayloadEnvelopeCapella,
35
+ ExecutionPayloadEnvelopeDeneb,
36
+ ExecutionPayloadEnvelopeElectra,
37
+ ExecutionPayloadEnvelopeGloas,
38
+ ForkchoiceUpdateBellatrix,
39
+ ForkchoiceUpdateCapella,
40
+ ForkchoiceUpdateDeneb,
41
+ ForkchoiceUpdateGloas,
42
+ ForkchoiceUpdateResponse,
43
+ MAX_BLOBS_REQUEST,
44
+ MAX_BODIES_REQUEST,
45
+ PayloadStatus,
46
+ PayloadStatusCode,
47
+ PayloadStatusSsz,
48
+ executionForkName,
49
+ } from "./sszTypes.js";
50
+ import {ForkchoiceUpdatedResult, GetPayloadResult, IEngineTransport, PayloadStatusResult} from "./transport.js";
51
+ import {
52
+ ExecutionPayloadBodyV2,
53
+ assertReqSizeLimit,
54
+ deserializeExecutionRequestsFromBytes,
55
+ serializeExecutionRequestsToBytes,
56
+ } from "./types.js";
57
+
58
+ export type EngineCapabilities = {
59
+ supportedForks: Set<string>;
60
+ blobsRevisions: Set<string>;
61
+ limits: {bodiesMaxCount: number; blobsMaxVersionedHashes: number; payloadMaxBytes: number};
62
+ };
63
+
64
+ const DEFAULT_LIMITS: EngineCapabilities["limits"] = {
65
+ bodiesMaxCount: MAX_BODIES_REQUEST,
66
+ blobsMaxVersionedHashes: MAX_BLOBS_REQUEST,
67
+ payloadMaxBytes: 2 ** 26,
68
+ };
69
+
70
+ /** Payload ids are opaque `Bytes8` tokens, hex encoded in the `GET /payloads/{payloadId}` path */
71
+ const PAYLOAD_ID_REGEX = /^0x[0-9a-fA-F]{16}$/;
72
+
73
+ const textDecoder = new TextDecoder();
74
+
75
+ // Same route ids as the JSON-RPC transport so client metrics stay comparable across transports
76
+ const notifyNewPayloadOpts: ReqOpts = {routeId: "notifyNewPayload"};
77
+ const forkchoiceUpdatedOpts: ReqOpts = {routeId: "forkchoiceUpdated"};
78
+ const getPayloadOpts: ReqOpts = {routeId: "getPayload"};
79
+ const getPayloadBodiesByHashOpts: ReqOpts = {routeId: "getPayloadBodiesByHash"};
80
+ const getBlobsV1Opts: ReqOpts = {routeId: "getBlobsV1"};
81
+ const getBlobsV2Opts: ReqOpts = {routeId: "getBlobsV2"};
82
+ const getClientVersionOpts: ReqOpts = {routeId: "getClientVersion"};
83
+ const getCapabilitiesOpts: ReqOpts = {routeId: "getCapabilities", retries: 0};
84
+
85
+ export function toExecutionForkName(fork: ForkName): ExecutionForkName {
86
+ const name = isForkPostBellatrix(fork) ? executionForkName[fork] : null;
87
+ if (name === null) {
88
+ throw Error(`No execution fork name for fork=${fork}`);
89
+ }
90
+ return name;
91
+ }
92
+
93
+ /** The header format is not pinned by the spec, the JSON `ClientVersionV1` shape is what ELs parse today */
94
+ export function formatClientVersionHeader(clientVersion: ClientVersion): string {
95
+ return JSON.stringify({...clientVersion, commit: `0x${clientVersion.commit}`});
96
+ }
97
+
98
+ /**
99
+ * REST engine API with SSZ encoded bodies
100
+ * https://github.com/ethereum/execution-apis/blob/main/src/engine/refactor.md
101
+ */
102
+ export class RestEngineTransport implements IEngineTransport {
103
+ private limits = DEFAULT_LIMITS;
104
+
105
+ constructor(private readonly client: EngineRestHttpClient) {}
106
+
107
+ get emitter(): JsonRpcHttpClientEventEmitter {
108
+ return this.client.emitter;
109
+ }
110
+
111
+ /** Does not emit client events, a missing REST API must not be mistaken for an unhealthy engine */
112
+ async getCapabilities(): Promise<EngineCapabilities> {
113
+ const {body} = await this.client.requestWithRetries(
114
+ {method: "GET", path: "/capabilities", responseType: "json"},
115
+ getCapabilitiesOpts
116
+ );
117
+ const capabilities = parseJson(body, "getCapabilities");
118
+ if (
119
+ !isRecord(capabilities) ||
120
+ !Array.isArray(capabilities.supported_forks) ||
121
+ !capabilities.supported_forks.every(isString)
122
+ ) {
123
+ throw new EngineRestResponseError("getCapabilities", "supported_forks must be an array of strings");
124
+ }
125
+ const versioned = capabilities.independently_versioned;
126
+ if (versioned !== undefined && !isRecord(versioned)) {
127
+ throw new EngineRestResponseError("getCapabilities", "independently_versioned must be an object");
128
+ }
129
+ const blobs = versioned?.blobs ?? [];
130
+ if (!Array.isArray(blobs) || !blobs.every(isString)) {
131
+ throw new EngineRestResponseError("getCapabilities", "blobs revisions must be an array of strings");
132
+ }
133
+ const advertisedLimits = capabilities.limits;
134
+ if (advertisedLimits !== undefined && !isRecord(advertisedLimits)) {
135
+ throw new EngineRestResponseError("getCapabilities", "limits must be an object");
136
+ }
137
+ const limits = {...DEFAULT_LIMITS};
138
+ for (const [key, name] of [
139
+ ["bodies.max_count", "bodiesMaxCount"],
140
+ ["blobs.max_versioned_hashes", "blobsMaxVersionedHashes"],
141
+ ["payload.max_bytes", "payloadMaxBytes"],
142
+ ] as const) {
143
+ const value = advertisedLimits?.[key];
144
+ if (value === undefined) continue;
145
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
146
+ throw new EngineRestResponseError("getCapabilities", `${key} must be a positive integer`);
147
+ }
148
+ limits[name] = Math.min(value, DEFAULT_LIMITS[name]);
149
+ }
150
+ this.limits = limits;
151
+ return {
152
+ supportedForks: new Set(capabilities.supported_forks.map((fork) => fork.toLowerCase())),
153
+ blobsRevisions: new Set(blobs.map((rev) => rev.toLowerCase())),
154
+ limits,
155
+ };
156
+ }
157
+
158
+ async newPayload(
159
+ fork: ForkName,
160
+ executionPayload: ExecutionPayload,
161
+ versionedHashes?: VersionedHashes,
162
+ parentBeaconBlockRoot?: Root,
163
+ executionRequests?: ExecutionRequests
164
+ ): Promise<PayloadStatusResult> {
165
+ let body: Uint8Array;
166
+ if (ForkSeq[fork] >= ForkSeq.deneb) {
167
+ if (parentBeaconBlockRoot === undefined) {
168
+ throw Error(`parentBlockRoot required in notifyNewPayload for fork=${fork}`);
169
+ }
170
+ if (versionedHashes === undefined) {
171
+ throw Error(`versionedHashes required in notifyNewPayload for fork=${fork}`);
172
+ }
173
+ // The envelope omits the expected hashes, so the EL cannot run Deneb is_valid_versioned_hashes
174
+ if (!isValidBlobVersionedHashes(executionPayload.transactions, versionedHashes)) {
175
+ return {
176
+ status: ExecutionPayloadStatus.INVALID,
177
+ latestValidHash: null,
178
+ validationError: "Payload blob versioned hashes do not match the beacon commitments",
179
+ };
180
+ }
181
+
182
+ if (ForkSeq[fork] >= ForkSeq.electra) {
183
+ if (executionRequests === undefined) {
184
+ throw Error(`executionRequests required in notifyNewPayload for fork=${fork}`);
185
+ }
186
+ const requests = serializeExecutionRequestsToBytes(fork, executionRequests);
187
+ body =
188
+ ForkSeq[fork] >= ForkSeq.gloas
189
+ ? ExecutionPayloadEnvelopeGloas.serialize({
190
+ payload: executionPayload as gloas.ExecutionPayload,
191
+ parentBeaconBlockRoot,
192
+ executionRequests: requests,
193
+ })
194
+ : ExecutionPayloadEnvelopeElectra.serialize({
195
+ payload: executionPayload as electra.ExecutionPayload,
196
+ parentBeaconBlockRoot,
197
+ executionRequests: requests,
198
+ });
199
+ } else {
200
+ body = ExecutionPayloadEnvelopeDeneb.serialize({
201
+ payload: executionPayload as deneb.ExecutionPayload,
202
+ parentBeaconBlockRoot,
203
+ });
204
+ }
205
+ } else if (ForkSeq[fork] >= ForkSeq.capella) {
206
+ body = ExecutionPayloadEnvelopeCapella.serialize({payload: executionPayload as capella.ExecutionPayload});
207
+ } else {
208
+ body = ExecutionPayloadEnvelopeBellatrix.serialize({payload: executionPayload});
209
+ }
210
+
211
+ if (body.length > this.limits.payloadMaxBytes) {
212
+ throw new LodestarError({
213
+ code: "ENGINE_REST_REQUEST_TOO_LARGE",
214
+ size: body.length,
215
+ limit: this.limits.payloadMaxBytes,
216
+ });
217
+ }
218
+
219
+ const res = await this.client.request(
220
+ {method: "POST", path: "/payloads", executionFork: toExecutionForkName(fork), body, responseType: "ssz"},
221
+ notifyNewPayloadOpts
222
+ );
223
+ return toPayloadStatusResult(deserializeResponse(PayloadStatus, res.body, "notifyNewPayload"));
224
+ }
225
+
226
+ async forkchoiceUpdated(
227
+ fork: ForkName,
228
+ headBlockHash: RootHex,
229
+ safeBlockHash: RootHex,
230
+ finalizedBlockHash: RootHex,
231
+ payloadAttributes?: PayloadAttributes
232
+ ): Promise<ForkchoiceUpdatedResult> {
233
+ const forkchoiceState = {
234
+ headBlockHash: fromHex(headBlockHash),
235
+ safeBlockHash: fromHex(safeBlockHash),
236
+ finalizedBlockHash: fromHex(finalizedBlockHash),
237
+ };
238
+
239
+ let body: Uint8Array;
240
+ if (ForkSeq[fork] >= ForkSeq.gloas) {
241
+ body = ForkchoiceUpdateGloas.serialize({
242
+ forkchoiceState,
243
+ payloadAttributes: payloadAttributes ? [toPayloadAttributesGloas(fork, payloadAttributes)] : [],
244
+ custodyColumns: [],
245
+ });
246
+ } else if (ForkSeq[fork] >= ForkSeq.deneb) {
247
+ body = ForkchoiceUpdateDeneb.serialize({
248
+ forkchoiceState,
249
+ payloadAttributes: payloadAttributes ? [toPayloadAttributesDeneb(fork, payloadAttributes)] : [],
250
+ });
251
+ } else if (ForkSeq[fork] >= ForkSeq.capella) {
252
+ body = ForkchoiceUpdateCapella.serialize({
253
+ forkchoiceState,
254
+ payloadAttributes: payloadAttributes ? [toPayloadAttributesCapella(fork, payloadAttributes)] : [],
255
+ });
256
+ } else {
257
+ body = ForkchoiceUpdateBellatrix.serialize({
258
+ forkchoiceState,
259
+ payloadAttributes: payloadAttributes ? [toPayloadAttributesBellatrix(payloadAttributes)] : [],
260
+ });
261
+ }
262
+
263
+ // If we are just fcUing and not asking execution for payload, retry is not required
264
+ // and we can move on, as the next fcU will be issued soon on the new slot
265
+ const fcUReqOpts = payloadAttributes !== undefined ? forkchoiceUpdatedOpts : {...forkchoiceUpdatedOpts, retries: 0};
266
+
267
+ const res = await this.client.request(
268
+ {method: "POST", path: "/forkchoice", executionFork: toExecutionForkName(fork), body, responseType: "ssz"},
269
+ fcUReqOpts
270
+ );
271
+ const {payloadStatus, payloadId} = deserializeResponse(ForkchoiceUpdateResponse, res.body, "forkchoiceUpdated");
272
+ return {
273
+ payloadStatus: toPayloadStatusResult(payloadStatus),
274
+ payloadId: payloadId.length > 0 ? toHex(payloadId[0]) : null,
275
+ };
276
+ }
277
+
278
+ async getPayload(fork: ForkName, payloadId: PayloadId): Promise<GetPayloadResult> {
279
+ if (!PAYLOAD_ID_REGEX.test(payloadId)) {
280
+ throw Error(`Invalid payloadId=${payloadId}, expected 0x prefixed 8 bytes hex`);
281
+ }
282
+
283
+ const res = await this.client.request(
284
+ {method: "GET", path: `/payloads/${payloadId}`, executionFork: toExecutionForkName(fork), responseType: "ssz"},
285
+ getPayloadOpts
286
+ );
287
+
288
+ if (ForkSeq[fork] >= ForkSeq.gloas) {
289
+ const {payload, blockValue, blobsBundle, executionRequests, shouldOverrideBuilder} = deserializeResponse(
290
+ BuiltPayloadGloas,
291
+ res.body,
292
+ "getPayload"
293
+ );
294
+ return {
295
+ executionPayload: payload,
296
+ executionPayloadValue: blockValue,
297
+ blobsBundle,
298
+ executionRequests: deserializeExecutionRequestsFromBytes(fork, executionRequests),
299
+ shouldOverrideBuilder,
300
+ };
301
+ }
302
+ if (ForkSeq[fork] >= ForkSeq.fulu) {
303
+ const {payload, blockValue, blobsBundle, executionRequests, shouldOverrideBuilder} = deserializeResponse(
304
+ BuiltPayloadFulu,
305
+ res.body,
306
+ "getPayload"
307
+ );
308
+ return {
309
+ executionPayload: payload,
310
+ executionPayloadValue: blockValue,
311
+ blobsBundle,
312
+ executionRequests: deserializeExecutionRequestsFromBytes(fork, executionRequests),
313
+ shouldOverrideBuilder,
314
+ };
315
+ }
316
+ if (ForkSeq[fork] >= ForkSeq.electra) {
317
+ const {payload, blockValue, blobsBundle, executionRequests, shouldOverrideBuilder} = deserializeResponse(
318
+ BuiltPayloadElectra,
319
+ res.body,
320
+ "getPayload"
321
+ );
322
+ return {
323
+ executionPayload: payload,
324
+ executionPayloadValue: blockValue,
325
+ blobsBundle,
326
+ executionRequests: deserializeExecutionRequestsFromBytes(fork, executionRequests),
327
+ shouldOverrideBuilder,
328
+ };
329
+ }
330
+ if (ForkSeq[fork] >= ForkSeq.deneb) {
331
+ const {payload, blockValue, blobsBundle, shouldOverrideBuilder} = deserializeResponse(
332
+ BuiltPayloadDeneb,
333
+ res.body,
334
+ "getPayload"
335
+ );
336
+ return {executionPayload: payload, executionPayloadValue: blockValue, blobsBundle, shouldOverrideBuilder};
337
+ }
338
+ if (ForkSeq[fork] >= ForkSeq.capella) {
339
+ const {payload, blockValue} = deserializeResponse(BuiltPayloadCapella, res.body, "getPayload");
340
+ return {executionPayload: payload, executionPayloadValue: blockValue, shouldOverrideBuilder: false};
341
+ }
342
+ const {payload, blockValue} = deserializeResponse(BuiltPayloadBellatrix, res.body, "getPayload");
343
+ return {executionPayload: payload, executionPayloadValue: blockValue, shouldOverrideBuilder: false};
344
+ }
345
+
346
+ async getPayloadBodiesByHashV2(blockHashes: RootHex[]): Promise<(ExecutionPayloadBodyV2 | null)[]> {
347
+ assertReqSizeLimit(blockHashes.length, this.limits.bodiesMaxCount);
348
+ const body = BodiesByHashRequest.serialize({blockHashes: blockHashes.map((hash) => fromHex(hash))});
349
+ const res = await this.client.request(
350
+ {
351
+ method: "POST",
352
+ path: "/bodies/hash",
353
+ executionFork: toExecutionForkName(ForkName.gloas),
354
+ body,
355
+ responseType: "ssz",
356
+ },
357
+ getPayloadBodiesByHashOpts
358
+ );
359
+ const bodies = deserializeBodiesResponse(ForkName.gloas, res.body);
360
+ if (bodies.length !== blockHashes.length) {
361
+ throw Error(`Invalid bodies response length=${bodies.length} blockHashes=${blockHashes.length}`);
362
+ }
363
+ return bodies;
364
+ }
365
+
366
+ async getBlobsV1(versionedHashes: VersionedHashes): Promise<(BlobAndProof | null)[]> {
367
+ assertReqSizeLimit(versionedHashes.length, this.limits.blobsMaxVersionedHashes);
368
+ const res = await this.client.request(
369
+ {method: "POST", path: "/blobs/v1", body: BlobsRequest.serialize({versionedHashes}), responseType: "ssz"},
370
+ getBlobsV1Opts
371
+ );
372
+ // 204 means the EL cannot serve blobs at all, e.g. while syncing
373
+ if (res.status === 204) {
374
+ return versionedHashes.map(() => null);
375
+ }
376
+
377
+ const {entries} = deserializeResponse(BlobsV1Response, res.body, "getBlobsV1");
378
+ if (entries.length !== versionedHashes.length) {
379
+ throw Error(`Invalid blobs/v1 response length=${entries.length} versionedHashes=${versionedHashes.length}`);
380
+ }
381
+ return entries.map((entry) => (entry.available ? entry.contents : null));
382
+ }
383
+
384
+ /**
385
+ * Blobs and proofs are views into the response body rather than copies, the response is all-or-nothing
386
+ * so every byte of it is needed. The pooled buffers of the JSON-RPC transport are not used.
387
+ */
388
+ async getBlobsV2(versionedHashes: VersionedHashes, _buffers?: Uint8Array[]): Promise<BlobAndProofV2[] | null> {
389
+ assertReqSizeLimit(versionedHashes.length, this.limits.blobsMaxVersionedHashes);
390
+
391
+ const res = await this.client.request(
392
+ {method: "POST", path: "/blobs/v2", body: BlobsRequest.serialize({versionedHashes}), responseType: "ssz"},
393
+ getBlobsV2Opts
394
+ );
395
+ // blobs/v2 is all-or-nothing, 204 means at least one blob is missing or the EL cannot serve blobs
396
+ if (res.status === 204) {
397
+ return null;
398
+ }
399
+
400
+ const {entries} = deserializeResponse(BlobsV2Response, res.body, "getBlobsV2", {reuseBytes: true});
401
+ if (entries.length !== versionedHashes.length) {
402
+ throw Error(`Invalid blobs/v2 response length=${entries.length} versionedHashes=${versionedHashes.length}`);
403
+ }
404
+ if (entries.some((entry) => !entry.available)) {
405
+ return null;
406
+ }
407
+
408
+ return entries.map(({contents}) => {
409
+ if (contents.proofs.length !== CELLS_PER_EXT_BLOB) {
410
+ throw Error(`Invalid proofs length ${contents.proofs.length}, expected ${CELLS_PER_EXT_BLOB}`);
411
+ }
412
+ return contents;
413
+ });
414
+ }
415
+
416
+ async getClientVersion(_clientVersion: ClientVersion): Promise<ClientVersion[]> {
417
+ const {body} = await this.client.request(
418
+ {method: "GET", path: "/identity", responseType: "json"},
419
+ getClientVersionOpts
420
+ );
421
+ const versions = parseJson(body, "getClientVersion");
422
+ if (!Array.isArray(versions)) {
423
+ throw new EngineRestResponseError("getClientVersion", "Expected an array of client versions");
424
+ }
425
+ return versions.map((cv: unknown, i) => {
426
+ if (
427
+ !isRecord(cv) ||
428
+ typeof cv.code !== "string" ||
429
+ typeof cv.name !== "string" ||
430
+ typeof cv.version !== "string" ||
431
+ typeof cv.commit !== "string"
432
+ ) {
433
+ throw new EngineRestResponseError("getClientVersion", `Invalid client version at index ${i}`);
434
+ }
435
+ const code = Object.hasOwn(ClientCode, cv.code) ? ClientCode[cv.code as keyof typeof ClientCode] : ClientCode.XX;
436
+ return {
437
+ code,
438
+ name: cv.name,
439
+ version: cv.version,
440
+ commit: strip0xPrefix(cv.commit),
441
+ };
442
+ });
443
+ }
444
+ }
445
+
446
+ function toPayloadAttributesBellatrix(attributes: PayloadAttributes) {
447
+ return {
448
+ timestamp: attributes.timestamp,
449
+ prevRandao: attributes.prevRandao,
450
+ suggestedFeeRecipient: fromHex(attributes.suggestedFeeRecipient),
451
+ };
452
+ }
453
+
454
+ function toPayloadAttributesCapella(fork: ForkName, attributes: PayloadAttributes) {
455
+ if (attributes.withdrawals === undefined) {
456
+ throw Error(`withdrawals required in payload attributes for fork=${fork}`);
457
+ }
458
+ return {...toPayloadAttributesBellatrix(attributes), withdrawals: attributes.withdrawals};
459
+ }
460
+
461
+ function toPayloadAttributesDeneb(fork: ForkName, attributes: PayloadAttributes) {
462
+ if (attributes.parentBeaconBlockRoot === undefined) {
463
+ throw Error(`parentBeaconBlockRoot required in payload attributes for fork=${fork}`);
464
+ }
465
+ return {...toPayloadAttributesCapella(fork, attributes), parentBeaconBlockRoot: attributes.parentBeaconBlockRoot};
466
+ }
467
+
468
+ function toPayloadAttributesGloas(fork: ForkName, attributes: PayloadAttributes) {
469
+ if (attributes.slotNumber === undefined) {
470
+ throw Error(`slotNumber required in payload attributes for fork=${fork}`);
471
+ }
472
+ if (attributes.targetGasLimit === undefined) {
473
+ throw Error(`targetGasLimit required in payload attributes for fork=${fork}`);
474
+ }
475
+ return {
476
+ ...toPayloadAttributesDeneb(fork, attributes),
477
+ slotNumber: attributes.slotNumber,
478
+ targetGasLimit: attributes.targetGasLimit,
479
+ };
480
+ }
481
+
482
+ function toPayloadStatusResult(payloadStatus: PayloadStatusSsz): PayloadStatusResult {
483
+ const latestValidHash = payloadStatus.latestValidHash.length > 0 ? toRootHex(payloadStatus.latestValidHash[0]) : null;
484
+ const validationError =
485
+ payloadStatus.validationError.length > 0 ? textDecoder.decode(payloadStatus.validationError[0]) : null;
486
+
487
+ switch (payloadStatus.status) {
488
+ case PayloadStatusCode.VALID:
489
+ return {status: ExecutionPayloadStatus.VALID, latestValidHash, validationError};
490
+ case PayloadStatusCode.INVALID:
491
+ return {status: ExecutionPayloadStatus.INVALID, latestValidHash, validationError};
492
+ case PayloadStatusCode.SYNCING:
493
+ return {status: ExecutionPayloadStatus.SYNCING, latestValidHash, validationError};
494
+ case PayloadStatusCode.ACCEPTED:
495
+ return {status: ExecutionPayloadStatus.ACCEPTED, latestValidHash, validationError};
496
+ default:
497
+ return {
498
+ status: ExecutionPayloadStatus.ELERROR,
499
+ latestValidHash: null,
500
+ validationError: `Unknown payload status code ${payloadStatus.status}`,
501
+ };
502
+ }
503
+ }
504
+
505
+ function deserializeBodiesResponse(fork: ForkName, data: Uint8Array): (ExecutionPayloadBodyV2 | null)[] {
506
+ if (ForkSeq[fork] >= ForkSeq.gloas) {
507
+ return deserializeResponse(BodiesResponseGloas, data, "getPayloadBodiesByHash").entries.map((entry) =>
508
+ entry.available ? entry.body : null
509
+ );
510
+ }
511
+ if (ForkSeq[fork] >= ForkSeq.capella) {
512
+ return deserializeResponse(BodiesResponseCapella, data, "getPayloadBodiesByHash").entries.map((entry) =>
513
+ entry.available ? {...entry.body, blockAccessList: null} : null
514
+ );
515
+ }
516
+ return deserializeResponse(BodiesResponseBellatrix, data, "getPayloadBodiesByHash").entries.map((entry) =>
517
+ entry.available ? {transactions: entry.body.transactions, withdrawals: null, blockAccessList: null} : null
518
+ );
519
+ }
520
+
521
+ /** A 2xx response that does not decode is an execution client error, not an unreachable client */
522
+ function deserializeResponse<T>(type: Type<T>, body: Uint8Array, routeId: string, opts?: {reuseBytes?: boolean}): T {
523
+ try {
524
+ return type.deserialize(body, opts);
525
+ } catch (e) {
526
+ throw new EngineRestResponseError(routeId, `Invalid SSZ: ${(e as Error).message}`);
527
+ }
528
+ }
529
+
530
+ function parseJson(body: Uint8Array, routeId: string): unknown {
531
+ try {
532
+ return JSON.parse(textDecoder.decode(body));
533
+ } catch (e) {
534
+ throw new EngineRestResponseError(routeId, `Invalid JSON: ${(e as Error).message}`);
535
+ }
536
+ }
537
+
538
+ function isString(value: unknown): value is string {
539
+ return typeof value === "string";
540
+ }
541
+
542
+ function isRecord(value: unknown): value is Record<string, unknown> {
543
+ return typeof value === "object" && value !== null && !Array.isArray(value);
544
+ }