@lodestar/beacon-node 1.50.0-dev.c04a499311 → 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 (228) hide show
  1. package/lib/api/impl/beacon/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/index.js +13 -0
  3. package/lib/api/impl/beacon/index.js.map +1 -1
  4. package/lib/api/impl/beacon/state/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/state/index.js +28 -6
  6. package/lib/api/impl/beacon/state/index.js.map +1 -1
  7. package/lib/api/impl/debug/index.d.ts.map +1 -1
  8. package/lib/api/impl/debug/index.js +26 -18
  9. package/lib/api/impl/debug/index.js.map +1 -1
  10. package/lib/api/impl/validator/index.d.ts.map +1 -1
  11. package/lib/api/impl/validator/index.js +32 -0
  12. package/lib/api/impl/validator/index.js.map +1 -1
  13. package/lib/chain/archiveStore/archiveStore.d.ts +5 -0
  14. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  15. package/lib/chain/archiveStore/archiveStore.js +13 -23
  16. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  17. package/lib/chain/archiveStore/utils/pruneHistory.d.ts +1 -1
  18. package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
  19. package/lib/chain/archiveStore/utils/pruneHistory.js +2 -1
  20. package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
  21. package/lib/chain/beaconProposerCache.d.ts +6 -0
  22. package/lib/chain/beaconProposerCache.d.ts.map +1 -1
  23. package/lib/chain/beaconProposerCache.js +16 -0
  24. package/lib/chain/beaconProposerCache.js.map +1 -1
  25. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  26. package/lib/chain/blocks/importBlock.js +14 -8
  27. package/lib/chain/blocks/importBlock.js.map +1 -1
  28. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  29. package/lib/chain/blocks/verifyBlock.js +6 -6
  30. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  31. package/lib/chain/chain.d.ts +5 -1
  32. package/lib/chain/chain.d.ts.map +1 -1
  33. package/lib/chain/chain.js +73 -20
  34. package/lib/chain/chain.js.map +1 -1
  35. package/lib/chain/interface.d.ts +3 -0
  36. package/lib/chain/interface.d.ts.map +1 -1
  37. package/lib/chain/opPools/aggregatedAttestationPool.d.ts +4 -2
  38. package/lib/chain/opPools/aggregatedAttestationPool.d.ts.map +1 -1
  39. package/lib/chain/opPools/aggregatedAttestationPool.js +30 -8
  40. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  41. package/lib/chain/opPools/opPool.d.ts.map +1 -1
  42. package/lib/chain/opPools/opPool.js +1 -23
  43. package/lib/chain/opPools/opPool.js.map +1 -1
  44. package/lib/chain/opPools/proposerPreferencesPool.d.ts +8 -4
  45. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  46. package/lib/chain/opPools/proposerPreferencesPool.js +27 -12
  47. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  48. package/lib/chain/opPools/utils.d.ts +11 -0
  49. package/lib/chain/opPools/utils.d.ts.map +1 -1
  50. package/lib/chain/opPools/utils.js +22 -0
  51. package/lib/chain/opPools/utils.js.map +1 -1
  52. package/lib/chain/options.d.ts +1 -0
  53. package/lib/chain/options.d.ts.map +1 -1
  54. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  55. package/lib/chain/prepareNextSlot.js +11 -3
  56. package/lib/chain/prepareNextSlot.js.map +1 -1
  57. package/lib/chain/produceBlock/produceBlockBody.d.ts +8 -0
  58. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  59. package/lib/chain/produceBlock/produceBlockBody.js +40 -28
  60. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  61. package/lib/chain/seenCache/seenGossipBlockInput.d.ts +7 -0
  62. package/lib/chain/seenCache/seenGossipBlockInput.d.ts.map +1 -1
  63. package/lib/chain/seenCache/seenGossipBlockInput.js +21 -0
  64. package/lib/chain/seenCache/seenGossipBlockInput.js.map +1 -1
  65. package/lib/chain/shufflingCache.d.ts +18 -8
  66. package/lib/chain/shufflingCache.d.ts.map +1 -1
  67. package/lib/chain/shufflingCache.js +72 -18
  68. package/lib/chain/shufflingCache.js.map +1 -1
  69. package/lib/db/beacon.d.ts +4 -1
  70. package/lib/db/beacon.d.ts.map +1 -1
  71. package/lib/db/beacon.js +6 -1
  72. package/lib/db/beacon.js.map +1 -1
  73. package/lib/db/buckets.d.ts +3 -1
  74. package/lib/db/buckets.d.ts.map +1 -1
  75. package/lib/db/buckets.js +2 -0
  76. package/lib/db/buckets.js.map +1 -1
  77. package/lib/db/interface.d.ts +4 -1
  78. package/lib/db/interface.d.ts.map +1 -1
  79. package/lib/db/repositories/blockArchiveIndex.d.ts +2 -2
  80. package/lib/db/repositories/blockArchiveIndex.d.ts.map +1 -1
  81. package/lib/db/repositories/earliestAvailableSlot.d.ts +11 -0
  82. package/lib/db/repositories/earliestAvailableSlot.d.ts.map +1 -0
  83. package/lib/db/repositories/earliestAvailableSlot.js +19 -0
  84. package/lib/db/repositories/earliestAvailableSlot.js.map +1 -0
  85. package/lib/db/repositories/index.d.ts +1 -0
  86. package/lib/db/repositories/index.d.ts.map +1 -1
  87. package/lib/db/repositories/index.js +1 -0
  88. package/lib/db/repositories/index.js.map +1 -1
  89. package/lib/db/repositories/proposerPreferences.d.ts +14 -0
  90. package/lib/db/repositories/proposerPreferences.d.ts.map +1 -0
  91. package/lib/db/repositories/proposerPreferences.js +21 -0
  92. package/lib/db/repositories/proposerPreferences.js.map +1 -0
  93. package/lib/execution/engine/blobVersionedHashes.d.ts +9 -0
  94. package/lib/execution/engine/blobVersionedHashes.d.ts.map +1 -0
  95. package/lib/execution/engine/blobVersionedHashes.js +74 -0
  96. package/lib/execution/engine/blobVersionedHashes.js.map +1 -0
  97. package/lib/execution/engine/http.d.ts +45 -13
  98. package/lib/execution/engine/http.d.ts.map +1 -1
  99. package/lib/execution/engine/http.js +236 -198
  100. package/lib/execution/engine/http.js.map +1 -1
  101. package/lib/execution/engine/index.d.ts +3 -2
  102. package/lib/execution/engine/index.d.ts.map +1 -1
  103. package/lib/execution/engine/index.js +33 -8
  104. package/lib/execution/engine/index.js.map +1 -1
  105. package/lib/execution/engine/interface.d.ts +1 -1
  106. package/lib/execution/engine/interface.d.ts.map +1 -1
  107. package/lib/execution/engine/jsonRpcTransport.d.ts +25 -0
  108. package/lib/execution/engine/jsonRpcTransport.d.ts.map +1 -0
  109. package/lib/execution/engine/jsonRpcTransport.js +158 -0
  110. package/lib/execution/engine/jsonRpcTransport.js.map +1 -0
  111. package/lib/execution/engine/restHttpClient.d.ts +71 -0
  112. package/lib/execution/engine/restHttpClient.d.ts.map +1 -0
  113. package/lib/execution/engine/restHttpClient.js +201 -0
  114. package/lib/execution/engine/restHttpClient.js.map +1 -0
  115. package/lib/execution/engine/restTransport.d.ts +46 -0
  116. package/lib/execution/engine/restTransport.d.ts.map +1 -0
  117. package/lib/execution/engine/restTransport.js +399 -0
  118. package/lib/execution/engine/restTransport.js.map +1 -0
  119. package/lib/execution/engine/sszTypes.d.ts +1261 -0
  120. package/lib/execution/engine/sszTypes.d.ts.map +1 -0
  121. package/lib/execution/engine/sszTypes.js +216 -0
  122. package/lib/execution/engine/sszTypes.js.map +1 -0
  123. package/lib/execution/engine/transport.d.ts +39 -0
  124. package/lib/execution/engine/transport.d.ts.map +1 -0
  125. package/lib/execution/engine/transport.js +2 -0
  126. package/lib/execution/engine/transport.js.map +1 -0
  127. package/lib/execution/engine/types.d.ts +2 -0
  128. package/lib/execution/engine/types.d.ts.map +1 -1
  129. package/lib/execution/engine/types.js +24 -57
  130. package/lib/execution/engine/types.js.map +1 -1
  131. package/lib/execution/engine/utils.d.ts.map +1 -1
  132. package/lib/execution/engine/utils.js +9 -2
  133. package/lib/execution/engine/utils.js.map +1 -1
  134. package/lib/metrics/metrics/lodestar.d.ts +10 -0
  135. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  136. package/lib/metrics/metrics/lodestar.js +22 -0
  137. package/lib/metrics/metrics/lodestar.js.map +1 -1
  138. package/lib/network/core/events.d.ts +5 -1
  139. package/lib/network/core/events.d.ts.map +1 -1
  140. package/lib/network/core/events.js +8 -0
  141. package/lib/network/core/events.js.map +1 -1
  142. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  143. package/lib/network/processor/gossipHandlers.js +60 -64
  144. package/lib/network/processor/gossipHandlers.js.map +1 -1
  145. package/lib/node/nodejs.d.ts +3 -1
  146. package/lib/node/nodejs.d.ts.map +1 -1
  147. package/lib/node/nodejs.js +2 -1
  148. package/lib/node/nodejs.js.map +1 -1
  149. package/lib/node/options.d.ts +3 -2
  150. package/lib/node/options.d.ts.map +1 -1
  151. package/lib/node/options.js +2 -2
  152. package/lib/node/options.js.map +1 -1
  153. package/lib/sync/range/batch.d.ts +5 -5
  154. package/lib/sync/range/batch.d.ts.map +1 -1
  155. package/lib/sync/range/batch.js +11 -17
  156. package/lib/sync/range/batch.js.map +1 -1
  157. package/lib/sync/range/chain.d.ts +4 -4
  158. package/lib/sync/range/chain.d.ts.map +1 -1
  159. package/lib/sync/range/chain.js +9 -5
  160. package/lib/sync/range/chain.js.map +1 -1
  161. package/lib/sync/range/range.d.ts.map +1 -1
  162. package/lib/sync/range/range.js +25 -11
  163. package/lib/sync/range/range.js.map +1 -1
  164. package/lib/sync/unknownBlock.d.ts +5 -2
  165. package/lib/sync/unknownBlock.d.ts.map +1 -1
  166. package/lib/sync/unknownBlock.js +44 -20
  167. package/lib/sync/unknownBlock.js.map +1 -1
  168. package/lib/sync/utils/downloadByRange.d.ts +38 -14
  169. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  170. package/lib/sync/utils/downloadByRange.js +84 -39
  171. package/lib/sync/utils/downloadByRange.js.map +1 -1
  172. package/lib/util/asyncIterableToEvents.d.ts +10 -2
  173. package/lib/util/asyncIterableToEvents.d.ts.map +1 -1
  174. package/lib/util/asyncIterableToEvents.js +29 -3
  175. package/lib/util/asyncIterableToEvents.js.map +1 -1
  176. package/lib/util/metadata.d.ts.map +1 -1
  177. package/lib/util/metadata.js +2 -1
  178. package/lib/util/metadata.js.map +1 -1
  179. package/package.json +17 -16
  180. package/src/api/impl/beacon/index.ts +15 -0
  181. package/src/api/impl/beacon/state/index.ts +43 -6
  182. package/src/api/impl/debug/index.ts +28 -19
  183. package/src/api/impl/validator/index.ts +45 -0
  184. package/src/chain/archiveStore/archiveStore.ts +18 -24
  185. package/src/chain/archiveStore/utils/pruneHistory.ts +4 -1
  186. package/src/chain/beaconProposerCache.ts +19 -1
  187. package/src/chain/blocks/importBlock.ts +17 -7
  188. package/src/chain/blocks/verifyBlock.ts +7 -6
  189. package/src/chain/chain.ts +97 -19
  190. package/src/chain/interface.ts +3 -0
  191. package/src/chain/opPools/aggregatedAttestationPool.ts +34 -10
  192. package/src/chain/opPools/opPool.ts +1 -31
  193. package/src/chain/opPools/proposerPreferencesPool.ts +34 -11
  194. package/src/chain/opPools/utils.ts +31 -0
  195. package/src/chain/options.ts +1 -1
  196. package/src/chain/prepareNextSlot.ts +15 -3
  197. package/src/chain/produceBlock/produceBlockBody.ts +68 -34
  198. package/src/chain/seenCache/seenGossipBlockInput.ts +23 -0
  199. package/src/chain/shufflingCache.ts +84 -22
  200. package/src/db/beacon.ts +6 -0
  201. package/src/db/buckets.ts +2 -0
  202. package/src/db/interface.ts +4 -0
  203. package/src/db/repositories/earliestAvailableSlot.ts +19 -0
  204. package/src/db/repositories/index.ts +1 -0
  205. package/src/db/repositories/proposerPreferences.ts +23 -0
  206. package/src/execution/engine/blobVersionedHashes.ts +72 -0
  207. package/src/execution/engine/http.ts +304 -271
  208. package/src/execution/engine/index.ts +41 -7
  209. package/src/execution/engine/interface.ts +1 -1
  210. package/src/execution/engine/jsonRpcTransport.ts +247 -0
  211. package/src/execution/engine/restHttpClient.ts +269 -0
  212. package/src/execution/engine/restTransport.ts +544 -0
  213. package/src/execution/engine/sszTypes.ts +354 -0
  214. package/src/execution/engine/transport.ts +61 -0
  215. package/src/execution/engine/types.ts +34 -74
  216. package/src/execution/engine/utils.ts +12 -2
  217. package/src/metrics/metrics/lodestar.ts +23 -0
  218. package/src/network/core/events.ts +16 -1
  219. package/src/network/processor/gossipHandlers.ts +91 -77
  220. package/src/node/nodejs.ts +4 -0
  221. package/src/node/options.ts +10 -1
  222. package/src/sync/range/batch.ts +14 -19
  223. package/src/sync/range/chain.ts +12 -7
  224. package/src/sync/range/range.ts +26 -12
  225. package/src/sync/unknownBlock.ts +57 -21
  226. package/src/sync/utils/downloadByRange.ts +123 -58
  227. package/src/util/asyncIterableToEvents.ts +44 -5
  228. package/src/util/metadata.ts +2 -1
@@ -1,17 +1,24 @@
1
- import {fromHex, toPrintableUrl} from "@lodestar/utils";
1
+ import {LodestarError, fromHex, toPrintableUrl} from "@lodestar/utils";
2
+ import {getLodestarClientVersion} from "../../util/metadata.js";
2
3
  import {ExecutionEngineDisabled} from "./disabled.js";
3
4
  import {
5
+ EngineApiMode,
4
6
  ExecutionEngineHttp,
5
7
  ExecutionEngineHttpOpts,
6
8
  ExecutionEngineModules,
7
9
  defaultExecutionEngineHttpOpts,
10
+ engineApiModes,
8
11
  } from "./http.js";
9
12
  import {IExecutionEngine} from "./interface.js";
10
13
  import {JsonRpcHttpClient} from "./jsonRpcHttpClient.js";
14
+ import {JsonRpcEngineTransport} from "./jsonRpcTransport.js";
11
15
  import {ExecutionEngineMockBackend, ExecutionEngineMockOpts} from "./mock.js";
16
+ import {EngineRestHttpClient} from "./restHttpClient.js";
17
+ import {RestEngineTransport, formatClientVersionHeader} from "./restTransport.js";
12
18
  import {ExecutionEngineMockJsonRpcClient, JsonRpcBackend} from "./utils.js";
13
19
 
14
- export {ExecutionEngineHttp, ExecutionEngineDisabled, defaultExecutionEngineHttpOpts};
20
+ export {ExecutionEngineHttp, ExecutionEngineDisabled, defaultExecutionEngineHttpOpts, engineApiModes};
21
+ export type {EngineApiMode};
15
22
 
16
23
  export type ExecutionEngineOpts =
17
24
  | ({mode?: "http"} & ExecutionEngineHttpOpts)
@@ -24,23 +31,50 @@ export function getExecutionEngineFromBackend(
24
31
  modules: ExecutionEngineModules
25
32
  ): IExecutionEngine {
26
33
  const rpc = new ExecutionEngineMockJsonRpcClient(backend);
27
- return new ExecutionEngineHttp(rpc, modules);
34
+ return new ExecutionEngineHttp({jsonRpc: new JsonRpcEngineTransport(rpc)}, modules);
28
35
  }
29
36
 
30
37
  export function getExecutionEngineHttp(
31
38
  opts: ExecutionEngineHttpOpts,
32
39
  modules: ExecutionEngineModules
33
40
  ): IExecutionEngine {
41
+ const jwtSecret = opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined;
42
+ const metrics = modules.metrics?.executionEnginerHttpClient;
43
+ const engineApi = opts.engineApi ?? "auto";
44
+ if (engineApi === "ssz" && opts.urls.length !== 1) {
45
+ throw new LodestarError({code: "ENGINE_REST_REQUIRES_SINGLE_URL", count: opts.urls.length});
46
+ }
47
+ if (engineApi === "auto" && opts.urls.length > 1) {
48
+ modules.logger.debug("Using JSON-RPC for multiple execution URLs", {count: opts.urls.length});
49
+ }
50
+
34
51
  const rpc = new JsonRpcHttpClient(opts.urls, {
35
52
  ...opts,
36
53
  signal: modules.signal,
37
- metrics: modules.metrics?.executionEnginerHttpClient,
38
- jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined,
54
+ metrics,
55
+ jwtSecret,
39
56
  jwtId: opts.jwtId,
40
57
  jwtVersion: opts.jwtVersion,
41
58
  });
42
- modules.logger.info("Execution client", {urls: opts.urls.map(toPrintableUrl).toString()});
43
- return new ExecutionEngineHttp(rpc, modules, opts);
59
+
60
+ const rest =
61
+ engineApi === "json-rpc" || opts.urls.length !== 1
62
+ ? undefined
63
+ : new RestEngineTransport(
64
+ new EngineRestHttpClient(opts.urls, {
65
+ signal: modules.signal,
66
+ timeout: opts.timeout,
67
+ retries: opts.retries,
68
+ retryDelay: opts.retryDelay,
69
+ jwtSecret,
70
+ jwtId: opts.jwtId,
71
+ clientVersion: formatClientVersionHeader(getLodestarClientVersion(opts)),
72
+ metrics,
73
+ })
74
+ );
75
+
76
+ modules.logger.info("Execution client", {urls: opts.urls.map(toPrintableUrl).toString(), engineApi});
77
+ return new ExecutionEngineHttp({jsonRpc: new JsonRpcEngineTransport(rpc), rest}, modules, opts);
44
78
  }
45
79
 
46
80
  export function initializeExecutionEngine(
@@ -170,7 +170,7 @@ export interface IExecutionEngine {
170
170
  shouldOverrideBuilder?: boolean;
171
171
  }>;
172
172
 
173
- /** Amsterdam: payload bodies including the block access list (null if the EL no longer has it) */
173
+ /** Amsterdam: includes the block access list; REST returns null for bodies outside the Amsterdam era. */
174
174
  getPayloadBodiesByHashV2(blockHash: DATA[]): Promise<(ExecutionPayloadBodyV2 | null)[]>;
175
175
 
176
176
  getBlobs(
@@ -0,0 +1,247 @@
1
+ import {ForkName, ForkSeq} from "@lodestar/params";
2
+ import {ExecutionPayload, ExecutionRequests, Root, RootHex} from "@lodestar/types";
3
+ import {BlobAndProof} from "@lodestar/types/deneb";
4
+ import {BlobAndProofV2} from "@lodestar/types/fulu";
5
+ import {strip0xPrefix} from "@lodestar/utils";
6
+ import {ClientCode, ClientVersion, PayloadAttributes, PayloadId, VersionedHashes} from "./interface.js";
7
+ import {IJsonRpcHttpClient, JsonRpcHttpClientEventEmitter, ReqOpts} from "./jsonRpcHttpClient.js";
8
+ import {ForkchoiceUpdatedResult, GetPayloadResult, IEngineTransport, PayloadStatusResult} from "./transport.js";
9
+ import {
10
+ BLOB_AND_PROOF_V2_RPC_BYTES,
11
+ EngineApiRpcParamTypes,
12
+ EngineApiRpcReturnTypes,
13
+ ExecutionPayloadBodyV2,
14
+ assertReqSizeLimit,
15
+ deserializeBlobAndProofs,
16
+ deserializeBlobAndProofsV2,
17
+ deserializeBlobAndProofsV2IntoBytes,
18
+ deserializeExecutionPayloadBodyV2,
19
+ parseExecutionPayload,
20
+ serializeBeaconBlockRoot,
21
+ serializeExecutionPayload,
22
+ serializeExecutionRequests,
23
+ serializePayloadAttributes,
24
+ serializeVersionedHashes,
25
+ } from "./types.js";
26
+ import {bytesToData} from "./utils.js";
27
+
28
+ /**
29
+ * Maximum number of version hashes that can be sent in a getBlobs request
30
+ * Clients must support at least 128 versionedHashes, so we avoid sending more
31
+ * https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#specification-3
32
+ */
33
+ const MAX_VERSIONED_HASHES = 128;
34
+
35
+ // Define static options once to prevent extra allocations
36
+ const notifyNewPayloadOpts: ReqOpts = {routeId: "notifyNewPayload"};
37
+ const forkchoiceUpdatedOpts: ReqOpts = {routeId: "forkchoiceUpdated"};
38
+ const getPayloadOpts: ReqOpts = {routeId: "getPayload"};
39
+ const getPayloadBodiesByHashOpts: ReqOpts = {routeId: "getPayloadBodiesByHash"};
40
+ const getBlobsV1Opts: ReqOpts = {routeId: "getBlobsV1"};
41
+ const getBlobsV2Opts: ReqOpts = {routeId: "getBlobsV2"};
42
+ const getClientVersionOpts: ReqOpts = {routeId: "getClientVersion"};
43
+
44
+ /**
45
+ * Legacy `engine_*` JSON-RPC methods
46
+ * https://github.com/ethereum/execution-apis/tree/main/src/engine
47
+ */
48
+ export class JsonRpcEngineTransport implements IEngineTransport {
49
+ constructor(private readonly rpc: IJsonRpcHttpClient) {}
50
+
51
+ get emitter(): JsonRpcHttpClientEventEmitter {
52
+ return this.rpc.emitter;
53
+ }
54
+
55
+ async newPayload(
56
+ fork: ForkName,
57
+ executionPayload: ExecutionPayload,
58
+ versionedHashes?: VersionedHashes,
59
+ parentBeaconBlockRoot?: Root,
60
+ executionRequests?: ExecutionRequests
61
+ ): Promise<PayloadStatusResult> {
62
+ const serializedExecutionPayload = serializeExecutionPayload(fork, executionPayload);
63
+
64
+ if (ForkSeq[fork] >= ForkSeq.deneb) {
65
+ if (versionedHashes === undefined) {
66
+ throw Error(`versionedHashes required in notifyNewPayload for fork=${fork}`);
67
+ }
68
+ if (parentBeaconBlockRoot === undefined) {
69
+ throw Error(`parentBlockRoot required in notifyNewPayload for fork=${fork}`);
70
+ }
71
+
72
+ const serializedVersionedHashes = serializeVersionedHashes(versionedHashes);
73
+ const serializedParentBeaconBlockRoot = serializeBeaconBlockRoot(parentBeaconBlockRoot);
74
+
75
+ if (ForkSeq[fork] >= ForkSeq.electra) {
76
+ if (executionRequests === undefined) {
77
+ throw Error(`executionRequests required in notifyNewPayload for fork=${fork}`);
78
+ }
79
+ const method = ForkSeq[fork] >= ForkSeq.gloas ? "engine_newPayloadV5" : "engine_newPayloadV4";
80
+ return this.rpc.fetchWithRetries<EngineApiRpcReturnTypes[typeof method], EngineApiRpcParamTypes[typeof method]>(
81
+ {
82
+ method,
83
+ params: [
84
+ serializedExecutionPayload,
85
+ serializedVersionedHashes,
86
+ serializedParentBeaconBlockRoot,
87
+ serializeExecutionRequests(fork, executionRequests),
88
+ ],
89
+ },
90
+ notifyNewPayloadOpts
91
+ );
92
+ }
93
+
94
+ const method = "engine_newPayloadV3";
95
+ return this.rpc.fetchWithRetries<EngineApiRpcReturnTypes[typeof method], EngineApiRpcParamTypes[typeof method]>(
96
+ {method, params: [serializedExecutionPayload, serializedVersionedHashes, serializedParentBeaconBlockRoot]},
97
+ notifyNewPayloadOpts
98
+ );
99
+ }
100
+
101
+ const method = ForkSeq[fork] >= ForkSeq.capella ? "engine_newPayloadV2" : "engine_newPayloadV1";
102
+ return this.rpc.fetchWithRetries<EngineApiRpcReturnTypes[typeof method], EngineApiRpcParamTypes[typeof method]>(
103
+ {method, params: [serializedExecutionPayload]},
104
+ notifyNewPayloadOpts
105
+ );
106
+ }
107
+
108
+ async forkchoiceUpdated(
109
+ fork: ForkName,
110
+ headBlockHash: RootHex,
111
+ safeBlockHash: RootHex,
112
+ finalizedBlockHash: RootHex,
113
+ payloadAttributes?: PayloadAttributes
114
+ ): Promise<ForkchoiceUpdatedResult> {
115
+ const method =
116
+ ForkSeq[fork] >= ForkSeq.gloas
117
+ ? "engine_forkchoiceUpdatedV4"
118
+ : ForkSeq[fork] >= ForkSeq.deneb
119
+ ? "engine_forkchoiceUpdatedV3"
120
+ : ForkSeq[fork] >= ForkSeq.capella
121
+ ? "engine_forkchoiceUpdatedV2"
122
+ : "engine_forkchoiceUpdatedV1";
123
+ const payloadAttributesRpc = payloadAttributes ? serializePayloadAttributes(payloadAttributes) : undefined;
124
+ // If we are just fcUing and not asking execution for payload, retry is not required
125
+ // and we can move on, as the next fcU will be issued soon on the new slot
126
+ const fcUReqOpts = payloadAttributes !== undefined ? forkchoiceUpdatedOpts : {...forkchoiceUpdatedOpts, retries: 0};
127
+
128
+ const {payloadStatus, payloadId} = await this.rpc.fetchWithRetries<
129
+ EngineApiRpcReturnTypes[typeof method],
130
+ EngineApiRpcParamTypes[typeof method]
131
+ >({method, params: [{headBlockHash, safeBlockHash, finalizedBlockHash}, payloadAttributesRpc]}, fcUReqOpts);
132
+
133
+ return {payloadStatus, payloadId: payloadId && payloadId !== "0x" ? payloadId : null};
134
+ }
135
+
136
+ async getPayload(fork: ForkName, payloadId: PayloadId): Promise<GetPayloadResult> {
137
+ let method: keyof EngineApiRpcReturnTypes;
138
+ switch (fork) {
139
+ case ForkName.phase0:
140
+ case ForkName.altair:
141
+ case ForkName.bellatrix:
142
+ method = "engine_getPayloadV1";
143
+ break;
144
+ case ForkName.capella:
145
+ method = "engine_getPayloadV2";
146
+ break;
147
+ case ForkName.deneb:
148
+ method = "engine_getPayloadV3";
149
+ break;
150
+ case ForkName.electra:
151
+ method = "engine_getPayloadV4";
152
+ break;
153
+ case ForkName.fulu:
154
+ method = "engine_getPayloadV5";
155
+ break;
156
+ default:
157
+ method = "engine_getPayloadV6";
158
+ break;
159
+ }
160
+ const payloadResponse = await this.rpc.fetchWithRetries<
161
+ EngineApiRpcReturnTypes[typeof method],
162
+ EngineApiRpcParamTypes[typeof method]
163
+ >({method, params: [payloadId]}, getPayloadOpts);
164
+ return parseExecutionPayload(fork, payloadResponse);
165
+ }
166
+
167
+ async getPayloadBodiesByHashV2(blockHashes: RootHex[]): Promise<(ExecutionPayloadBodyV2 | null)[]> {
168
+ const method = "engine_getPayloadBodiesByHashV2";
169
+ assertReqSizeLimit(blockHashes.length, 32);
170
+ const response = await this.rpc.fetchWithRetries<
171
+ EngineApiRpcReturnTypes[typeof method],
172
+ EngineApiRpcParamTypes[typeof method]
173
+ >({method, params: [blockHashes]}, getPayloadBodiesByHashOpts);
174
+ return response.map(deserializeExecutionPayloadBodyV2);
175
+ }
176
+
177
+ async getBlobsV1(versionedHashes: VersionedHashes): Promise<(BlobAndProof | null)[]> {
178
+ assertReqSizeLimit(versionedHashes.length, MAX_VERSIONED_HASHES);
179
+ const versionedHashesHex = versionedHashes.map(bytesToData);
180
+ const response = await this.rpc.fetchWithRetries<
181
+ EngineApiRpcReturnTypes["engine_getBlobsV1"],
182
+ EngineApiRpcParamTypes["engine_getBlobsV1"]
183
+ >({method: "engine_getBlobsV1", params: [versionedHashesHex]}, getBlobsV1Opts);
184
+
185
+ if (response.length !== versionedHashesHex.length) {
186
+ throw Error(
187
+ `Invalid engine_getBlobsV1 response length=${response.length} versionedHashes=${versionedHashesHex.length}`
188
+ );
189
+ }
190
+
191
+ return response.map(deserializeBlobAndProofs);
192
+ }
193
+
194
+ async getBlobsV2(versionedHashes: VersionedHashes, buffers?: Uint8Array[]): Promise<BlobAndProofV2[] | null> {
195
+ assertReqSizeLimit(versionedHashes.length, MAX_VERSIONED_HASHES);
196
+ const versionedHashesHex = versionedHashes.map(bytesToData);
197
+ if (buffers) {
198
+ // Callers preallocate one buffer per max blobs of the epoch, only the first entries are used
199
+ if (buffers.length < versionedHashesHex.length) {
200
+ throw Error(`Not enough buffers length=${buffers.length} versionedHashes=${versionedHashesHex.length}`);
201
+ }
202
+
203
+ for (const [i, buffer] of buffers.entries()) {
204
+ if (buffer.length !== BLOB_AND_PROOF_V2_RPC_BYTES) {
205
+ throw Error(`Invalid buffer[${i}] length=${buffer.length} expected=${BLOB_AND_PROOF_V2_RPC_BYTES}`);
206
+ }
207
+ }
208
+ }
209
+
210
+ const response = await this.rpc.fetchWithRetries<
211
+ EngineApiRpcReturnTypes["engine_getBlobsV2"],
212
+ EngineApiRpcParamTypes["engine_getBlobsV2"]
213
+ >({method: "engine_getBlobsV2", params: [versionedHashesHex]}, getBlobsV2Opts);
214
+
215
+ // engine_getBlobsV2 does not return partial responses. It returns null if any blob is not found
216
+ if (response == null) {
217
+ return null;
218
+ }
219
+
220
+ if (response.length !== versionedHashesHex.length) {
221
+ throw Error(
222
+ `Invalid engine_getBlobsV2 response length=${response.length} versionedHashes=${versionedHashesHex.length}`
223
+ );
224
+ }
225
+
226
+ if (buffers) {
227
+ // getBlobsV2() is designed to called once per slot so we expect to have buffers
228
+ return response.map((data, i) => deserializeBlobAndProofsV2IntoBytes(data, buffers[i]));
229
+ }
230
+
231
+ return response.map(deserializeBlobAndProofsV2);
232
+ }
233
+
234
+ async getClientVersion(clientVersion: ClientVersion): Promise<ClientVersion[]> {
235
+ const method = "engine_getClientVersionV1";
236
+
237
+ const response = await this.rpc.fetchWithRetries<
238
+ EngineApiRpcReturnTypes[typeof method],
239
+ EngineApiRpcParamTypes[typeof method]
240
+ >({method, params: [{...clientVersion, commit: `0x${clientVersion.commit}`}]}, getClientVersionOpts);
241
+
242
+ return response.map((cv) => {
243
+ const code = cv.code in ClientCode ? ClientCode[cv.code as keyof typeof ClientCode] : ClientCode.XX;
244
+ return {code, name: cv.name, version: cv.version, commit: strip0xPrefix(cv.commit)};
245
+ });
246
+ }
247
+ }
@@ -0,0 +1,269 @@
1
+ import {ErrorAborted, FetchError, LodestarError, TimeoutError, fetch, isValidHttpUrl, retry} from "@lodestar/utils";
2
+ import {
3
+ JsonRpcHttpClientEvent,
4
+ JsonRpcHttpClientEventEmitter,
5
+ JsonRpcHttpClientMetrics,
6
+ ReqOpts,
7
+ } from "./jsonRpcHttpClient.js";
8
+ import {JwtClaim, encodeJwtToken} from "./jwt.js";
9
+ import {ExecutionForkName} from "./sszTypes.js";
10
+
11
+ export const ENGINE_REST_BASE_PATH = "/engine/v1";
12
+ export const EXECUTION_VERSION_HEADER = "Eth-Execution-Version";
13
+ export const CLIENT_VERSION_HEADER = "X-Engine-Client-Version";
14
+ export const MEDIA_TYPE_SSZ = "application/octet-stream";
15
+ export const MEDIA_TYPE_JSON = "application/json";
16
+
17
+ const REQUEST_TIMEOUT = 30 * 1000;
18
+ /** Limits the amount of error detail printed with REST errors */
19
+ const MAX_ERROR_DETAIL_LENGTH = 500;
20
+
21
+ export type EngineRestRequest = {
22
+ method: "GET" | "POST";
23
+ /** Path relative to the `/engine/v1` base path */
24
+ path: string;
25
+ query?: Record<string, string | number>;
26
+ /** Sets the `Eth-Execution-Version` header on fork-scoped endpoints */
27
+ executionFork?: ExecutionForkName;
28
+ /** SSZ encoded request body */
29
+ body?: Uint8Array;
30
+ responseType: "ssz" | "json";
31
+ };
32
+
33
+ export type EngineRestResponse = {
34
+ status: number;
35
+ /** Empty on `204 No Content` */
36
+ body: Uint8Array;
37
+ };
38
+
39
+ /** Non-2xx response, `type` and `detail` are taken from the problem+json body when present */
40
+ export class EngineRestError extends Error {
41
+ constructor(
42
+ readonly status: number,
43
+ readonly type: string | null,
44
+ readonly detail: string | null,
45
+ routeId: string
46
+ ) {
47
+ super(
48
+ `Engine REST error: status=${status} type=${type ?? "unknown"}${detail ? ` detail=${detail}` : ""}, ${routeId}`
49
+ );
50
+ }
51
+ }
52
+
53
+ export class EngineRestResponseError extends LodestarError<{
54
+ code: "ENGINE_REST_INVALID_RESPONSE";
55
+ routeId: string;
56
+ reason: string;
57
+ }> {
58
+ constructor(routeId: string, reason: string) {
59
+ super(
60
+ {code: "ENGINE_REST_INVALID_RESPONSE", routeId, reason},
61
+ `Invalid engine REST response: ${reason}, ${routeId}`
62
+ );
63
+ }
64
+ }
65
+
66
+ export type EngineRestHttpClientOpts = {
67
+ signal?: AbortSignal;
68
+ timeout?: number;
69
+ retries?: number;
70
+ retryDelay?: number;
71
+ /** HS256 secret, a fresh token is generated for every request as ELs check `iat` freshness */
72
+ jwtSecret?: Uint8Array;
73
+ /** Included as `id` claim if `jwtSecret` is provided */
74
+ jwtId?: string;
75
+ /** Value of the `X-Engine-Client-Version` header, the `clv` JWT claim is removed in the REST API */
76
+ clientVersion?: string;
77
+ metrics?: JsonRpcHttpClientMetrics | null;
78
+ };
79
+
80
+ /**
81
+ * HTTP/1.1 client for the REST engine API. Only transport concerns live here, encoding of
82
+ * request and response bodies is up to the caller.
83
+ */
84
+ export class EngineRestHttpClient {
85
+ readonly emitter = new JsonRpcHttpClientEventEmitter();
86
+ private readonly metrics: JsonRpcHttpClientMetrics | null;
87
+
88
+ constructor(
89
+ private readonly urls: string[],
90
+ private readonly opts: EngineRestHttpClientOpts = {}
91
+ ) {
92
+ if (urls.length === 0) {
93
+ throw Error("No urls provided to EngineRestHttpClient");
94
+ }
95
+ for (const [i, url] of urls.entries()) {
96
+ if (!isValidHttpUrl(url)) {
97
+ throw Error(`EngineRestHttpClient.urls[${i}] must be a valid URL: ${url}`);
98
+ }
99
+ }
100
+ this.metrics = opts.metrics ?? null;
101
+ }
102
+
103
+ /** Perform request with retries, emitting response and error events consumed by the engine state tracking */
104
+ async request(req: EngineRestRequest, opts?: ReqOpts): Promise<EngineRestResponse> {
105
+ try {
106
+ const response = await this.requestWithRetries(req, opts);
107
+ this.emitter.emit(JsonRpcHttpClientEvent.RESPONSE, {payload: req, response});
108
+ return response;
109
+ } catch (error) {
110
+ this.emitter.emit(JsonRpcHttpClientEvent.ERROR, {payload: req, error: error as Error});
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ /** Perform request with retries without emitting events, used to probe support for the REST API */
116
+ async requestWithRetries(req: EngineRestRequest, opts?: ReqOpts): Promise<EngineRestResponse> {
117
+ const routeId = opts?.routeId ?? "unknown";
118
+ return retry(() => this.requestAnyUrl(req, opts), {
119
+ retries: opts?.retries ?? this.opts.retries ?? 0,
120
+ retryDelay: opts?.retryDelay ?? this.opts.retryDelay,
121
+ shouldRetry: opts?.shouldRetry ?? isRetryableEngineRestError,
122
+ signal: this.opts.signal,
123
+ onRetry: () => {
124
+ this.metrics?.retryCount.inc({routeId});
125
+ },
126
+ });
127
+ }
128
+
129
+ private async requestAnyUrl(req: EngineRestRequest, opts?: ReqOpts): Promise<EngineRestResponse> {
130
+ const routeId = opts?.routeId ?? "unknown";
131
+ let lastError: Error | null = null;
132
+
133
+ for (let i = 0; i < this.urls.length; i++) {
134
+ if (i > 0) {
135
+ this.metrics?.requestUsedFallbackUrl.inc({routeId});
136
+ }
137
+
138
+ try {
139
+ return await this.requestOneUrl(this.urls[i], req, opts);
140
+ } catch (e) {
141
+ if (!isRetryableEngineRestError(e as Error)) {
142
+ throw e;
143
+ }
144
+ lastError = e as Error;
145
+ }
146
+ }
147
+ throw lastError ?? Error("Unknown error");
148
+ }
149
+
150
+ private async requestOneUrl(baseUrl: string, req: EngineRestRequest, opts?: ReqOpts): Promise<EngineRestResponse> {
151
+ const controller = new AbortController();
152
+ const timeout = setTimeout(() => controller.abort(), opts?.timeout ?? this.opts.timeout ?? REQUEST_TIMEOUT);
153
+
154
+ // Not AbortSignal.any(), Node retains every composite until the long-lived parent signal aborts
155
+ const onParentSignalAbort = (): void => controller.abort();
156
+ this.opts.signal?.addEventListener("abort", onParentSignalAbort, {once: true});
157
+ if (this.opts.signal?.aborted) {
158
+ controller.abort();
159
+ }
160
+
161
+ const routeId = opts?.routeId ?? "unknown";
162
+ const timer = this.metrics?.requestTime.startTimer({routeId});
163
+ this.metrics?.activeRequests.inc({routeId}, 1);
164
+
165
+ try {
166
+ const url = new URL(`${ENGINE_REST_BASE_PATH}${req.path}`, baseUrl);
167
+ for (const [key, value] of Object.entries(req.query ?? {})) {
168
+ url.searchParams.set(key, String(value));
169
+ }
170
+
171
+ const headers: Record<string, string> = {
172
+ Accept: req.responseType === "ssz" ? MEDIA_TYPE_SSZ : MEDIA_TYPE_JSON,
173
+ };
174
+ if (req.body !== undefined) {
175
+ headers["Content-Type"] = MEDIA_TYPE_SSZ;
176
+ }
177
+ if (req.executionFork !== undefined) {
178
+ headers[EXECUTION_VERSION_HEADER] = req.executionFork;
179
+ }
180
+ if (this.opts.clientVersion !== undefined) {
181
+ headers[CLIENT_VERSION_HEADER] = this.opts.clientVersion;
182
+ }
183
+ if (this.opts.jwtSecret) {
184
+ const jwtClaim: JwtClaim = {iat: Math.floor(Date.now() / 1000), id: this.opts.jwtId};
185
+ headers.Authorization = `Bearer ${encodeJwtToken(jwtClaim, this.opts.jwtSecret)}`;
186
+ }
187
+
188
+ this.metrics?.requestBytes.inc({routeId}, req.body?.length ?? 0);
189
+
190
+ const res = await fetch(url, {
191
+ method: req.method,
192
+ body: req.body as BodyInit | undefined,
193
+ headers,
194
+ signal: controller.signal,
195
+ });
196
+
197
+ const streamTimer = this.metrics?.streamTime.startTimer({routeId});
198
+ let body: Uint8Array;
199
+ try {
200
+ body = new Uint8Array(await res.arrayBuffer());
201
+ } catch (e) {
202
+ throw new FetchError(url, e);
203
+ } finally {
204
+ streamTimer?.();
205
+ }
206
+ this.metrics?.responseBytes.inc({routeId}, body.length);
207
+
208
+ if (!res.ok) {
209
+ const {type, detail} = parseProblemBody(body);
210
+ throw new EngineRestError(res.status, type, detail, routeId);
211
+ }
212
+
213
+ if (res.status !== 200 && res.status !== 204) {
214
+ throw new EngineRestResponseError(routeId, `Unexpected status ${res.status}`);
215
+ }
216
+ const contentType = res.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
217
+ const expectedContentType = req.responseType === "ssz" ? MEDIA_TYPE_SSZ : MEDIA_TYPE_JSON;
218
+ if (res.status !== 204 && contentType !== expectedContentType) {
219
+ throw new EngineRestResponseError(routeId, `Unexpected content type ${contentType}`);
220
+ }
221
+
222
+ return {status: res.status, body};
223
+ } catch (e) {
224
+ this.metrics?.requestErrors.inc({routeId});
225
+ if (controller.signal.aborted) {
226
+ if (this.opts.signal?.aborted) {
227
+ throw new ErrorAborted("request");
228
+ }
229
+ throw new TimeoutError("request");
230
+ }
231
+ throw e;
232
+ } finally {
233
+ timer?.();
234
+ this.metrics?.activeRequests.dec({routeId}, 1);
235
+
236
+ clearTimeout(timeout);
237
+ this.opts.signal?.removeEventListener("abort", onParentSignalAbort);
238
+ }
239
+ }
240
+ }
241
+
242
+ /** Client errors are deterministic apart from timeouts and rate limits, transport failures and server errors are retried */
243
+ export function isRetryableEngineRestError(e: Error): boolean {
244
+ return (
245
+ (e instanceof EngineRestError && (e.status >= 500 || e.status === 408 || e.status === 429)) ||
246
+ e instanceof TimeoutError ||
247
+ (e instanceof FetchError && e.type !== "input" && e.type !== "aborted")
248
+ );
249
+ }
250
+
251
+ function parseProblemBody(body: Uint8Array): {type: string | null; detail: string | null} {
252
+ const text = new TextDecoder().decode(body);
253
+ try {
254
+ const problem: unknown = JSON.parse(text);
255
+ if (typeof problem === "object" && problem !== null) {
256
+ return {
257
+ type: "type" in problem && typeof problem.type === "string" ? problem.type : null,
258
+ detail:
259
+ ("detail" in problem && typeof problem.detail === "string" ? problem.detail : text).slice(
260
+ 0,
261
+ MAX_ERROR_DETAIL_LENGTH
262
+ ) || null,
263
+ };
264
+ }
265
+ } catch {
266
+ // Legacy endpoints and proxies may return plain text instead of a problem document.
267
+ }
268
+ return {type: null, detail: text.slice(0, MAX_ERROR_DETAIL_LENGTH) || null};
269
+ }