@lodestar/beacon-node 1.50.0-dev.fda8006fc3 → 1.50.0-dev.fe4e7408b4

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 (244) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +7 -2
  3. package/lib/api/impl/beacon/blocks/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 +26 -5
  6. package/lib/api/impl/beacon/state/index.js.map +1 -1
  7. package/lib/api/impl/validator/index.d.ts.map +1 -1
  8. package/lib/api/impl/validator/index.js +42 -3
  9. package/lib/api/impl/validator/index.js.map +1 -1
  10. package/lib/chain/archiveStore/archiveStore.d.ts +5 -0
  11. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  12. package/lib/chain/archiveStore/archiveStore.js +13 -23
  13. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  14. package/lib/chain/archiveStore/utils/pruneHistory.d.ts +1 -1
  15. package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
  16. package/lib/chain/archiveStore/utils/pruneHistory.js +2 -1
  17. package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
  18. package/lib/chain/beaconProposerCache.d.ts +6 -0
  19. package/lib/chain/beaconProposerCache.d.ts.map +1 -1
  20. package/lib/chain/beaconProposerCache.js +16 -0
  21. package/lib/chain/beaconProposerCache.js.map +1 -1
  22. package/lib/chain/blocks/blockInput/blockInput.d.ts +1 -1
  23. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  24. package/lib/chain/blocks/blockInput/blockInput.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 +3 -1
  32. package/lib/chain/chain.d.ts.map +1 -1
  33. package/lib/chain/chain.js +55 -19
  34. package/lib/chain/chain.js.map +1 -1
  35. package/lib/chain/interface.d.ts +3 -1
  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 +4 -0
  45. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  46. package/lib/chain/opPools/proposerPreferencesPool.js +13 -1
  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/prepareNextSlot.d.ts.map +1 -1
  53. package/lib/chain/prepareNextSlot.js +11 -3
  54. package/lib/chain/prepareNextSlot.js.map +1 -1
  55. package/lib/chain/produceBlock/computeNewStateRoot.d.ts.map +1 -1
  56. package/lib/chain/produceBlock/computeNewStateRoot.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 +48 -34
  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 +22 -1
  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/blockArchive.js +2 -2
  80. package/lib/db/repositories/blockArchive.js.map +1 -1
  81. package/lib/db/repositories/blockArchiveIndex.d.ts +3 -3
  82. package/lib/db/repositories/blockArchiveIndex.d.ts.map +1 -1
  83. package/lib/db/repositories/blockArchiveIndex.js +1 -2
  84. package/lib/db/repositories/blockArchiveIndex.js.map +1 -1
  85. package/lib/db/repositories/earliestAvailableSlot.d.ts +11 -0
  86. package/lib/db/repositories/earliestAvailableSlot.d.ts.map +1 -0
  87. package/lib/db/repositories/earliestAvailableSlot.js +19 -0
  88. package/lib/db/repositories/earliestAvailableSlot.js.map +1 -0
  89. package/lib/db/repositories/index.d.ts +1 -0
  90. package/lib/db/repositories/index.d.ts.map +1 -1
  91. package/lib/db/repositories/index.js +1 -0
  92. package/lib/db/repositories/index.js.map +1 -1
  93. package/lib/db/repositories/proposerPreferences.d.ts +14 -0
  94. package/lib/db/repositories/proposerPreferences.d.ts.map +1 -0
  95. package/lib/db/repositories/proposerPreferences.js +21 -0
  96. package/lib/db/repositories/proposerPreferences.js.map +1 -0
  97. package/lib/execution/engine/blobVersionedHashes.d.ts +9 -0
  98. package/lib/execution/engine/blobVersionedHashes.d.ts.map +1 -0
  99. package/lib/execution/engine/blobVersionedHashes.js +74 -0
  100. package/lib/execution/engine/blobVersionedHashes.js.map +1 -0
  101. package/lib/execution/engine/http.d.ts +45 -13
  102. package/lib/execution/engine/http.d.ts.map +1 -1
  103. package/lib/execution/engine/http.js +236 -198
  104. package/lib/execution/engine/http.js.map +1 -1
  105. package/lib/execution/engine/index.d.ts +3 -2
  106. package/lib/execution/engine/index.d.ts.map +1 -1
  107. package/lib/execution/engine/index.js +33 -8
  108. package/lib/execution/engine/index.js.map +1 -1
  109. package/lib/execution/engine/interface.d.ts +1 -1
  110. package/lib/execution/engine/interface.d.ts.map +1 -1
  111. package/lib/execution/engine/jsonRpcTransport.d.ts +25 -0
  112. package/lib/execution/engine/jsonRpcTransport.d.ts.map +1 -0
  113. package/lib/execution/engine/jsonRpcTransport.js +158 -0
  114. package/lib/execution/engine/jsonRpcTransport.js.map +1 -0
  115. package/lib/execution/engine/restHttpClient.d.ts +71 -0
  116. package/lib/execution/engine/restHttpClient.d.ts.map +1 -0
  117. package/lib/execution/engine/restHttpClient.js +201 -0
  118. package/lib/execution/engine/restHttpClient.js.map +1 -0
  119. package/lib/execution/engine/restTransport.d.ts +46 -0
  120. package/lib/execution/engine/restTransport.d.ts.map +1 -0
  121. package/lib/execution/engine/restTransport.js +399 -0
  122. package/lib/execution/engine/restTransport.js.map +1 -0
  123. package/lib/execution/engine/sszTypes.d.ts +1261 -0
  124. package/lib/execution/engine/sszTypes.d.ts.map +1 -0
  125. package/lib/execution/engine/sszTypes.js +216 -0
  126. package/lib/execution/engine/sszTypes.js.map +1 -0
  127. package/lib/execution/engine/transport.d.ts +39 -0
  128. package/lib/execution/engine/transport.d.ts.map +1 -0
  129. package/lib/execution/engine/transport.js +2 -0
  130. package/lib/execution/engine/transport.js.map +1 -0
  131. package/lib/execution/engine/types.d.ts +2 -0
  132. package/lib/execution/engine/types.d.ts.map +1 -1
  133. package/lib/execution/engine/types.js +24 -57
  134. package/lib/execution/engine/types.js.map +1 -1
  135. package/lib/execution/engine/utils.d.ts.map +1 -1
  136. package/lib/execution/engine/utils.js +9 -2
  137. package/lib/execution/engine/utils.js.map +1 -1
  138. package/lib/metrics/metrics/lodestar.d.ts +10 -0
  139. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  140. package/lib/metrics/metrics/lodestar.js +22 -0
  141. package/lib/metrics/metrics/lodestar.js.map +1 -1
  142. package/lib/network/core/events.d.ts +5 -1
  143. package/lib/network/core/events.d.ts.map +1 -1
  144. package/lib/network/core/events.js +8 -0
  145. package/lib/network/core/events.js.map +1 -1
  146. package/lib/network/gossip/topic.d.ts +0 -14
  147. package/lib/network/gossip/topic.d.ts.map +1 -1
  148. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  149. package/lib/network/processor/gossipHandlers.js +60 -64
  150. package/lib/network/processor/gossipHandlers.js.map +1 -1
  151. package/lib/network/processor/index.js +3 -3
  152. package/lib/network/processor/index.js.map +1 -1
  153. package/lib/node/nodejs.d.ts +3 -1
  154. package/lib/node/nodejs.d.ts.map +1 -1
  155. package/lib/node/nodejs.js +2 -1
  156. package/lib/node/nodejs.js.map +1 -1
  157. package/lib/node/options.d.ts +3 -2
  158. package/lib/node/options.d.ts.map +1 -1
  159. package/lib/node/options.js +2 -2
  160. package/lib/node/options.js.map +1 -1
  161. package/lib/sync/range/batch.d.ts +5 -5
  162. package/lib/sync/range/batch.d.ts.map +1 -1
  163. package/lib/sync/range/batch.js +11 -17
  164. package/lib/sync/range/batch.js.map +1 -1
  165. package/lib/sync/range/chain.d.ts +4 -4
  166. package/lib/sync/range/chain.d.ts.map +1 -1
  167. package/lib/sync/range/chain.js +7 -5
  168. package/lib/sync/range/chain.js.map +1 -1
  169. package/lib/sync/range/range.d.ts.map +1 -1
  170. package/lib/sync/range/range.js +25 -11
  171. package/lib/sync/range/range.js.map +1 -1
  172. package/lib/sync/unknownBlock.d.ts +5 -2
  173. package/lib/sync/unknownBlock.d.ts.map +1 -1
  174. package/lib/sync/unknownBlock.js +44 -20
  175. package/lib/sync/unknownBlock.js.map +1 -1
  176. package/lib/sync/utils/downloadByRange.d.ts +38 -14
  177. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  178. package/lib/sync/utils/downloadByRange.js +84 -39
  179. package/lib/sync/utils/downloadByRange.js.map +1 -1
  180. package/lib/util/asyncIterableToEvents.d.ts +10 -2
  181. package/lib/util/asyncIterableToEvents.d.ts.map +1 -1
  182. package/lib/util/asyncIterableToEvents.js +29 -3
  183. package/lib/util/asyncIterableToEvents.js.map +1 -1
  184. package/lib/util/metadata.d.ts.map +1 -1
  185. package/lib/util/metadata.js +2 -1
  186. package/lib/util/metadata.js.map +1 -1
  187. package/lib/util/sszBytes.d.ts +2 -2
  188. package/lib/util/sszBytes.d.ts.map +1 -1
  189. package/lib/util/sszBytes.js +21 -14
  190. package/lib/util/sszBytes.js.map +1 -1
  191. package/package.json +17 -16
  192. package/src/api/impl/beacon/blocks/index.ts +10 -3
  193. package/src/api/impl/beacon/state/index.ts +41 -5
  194. package/src/api/impl/validator/index.ts +57 -5
  195. package/src/chain/archiveStore/archiveStore.ts +18 -24
  196. package/src/chain/archiveStore/utils/pruneHistory.ts +4 -1
  197. package/src/chain/beaconProposerCache.ts +19 -1
  198. package/src/chain/blocks/blockInput/blockInput.ts +1 -1
  199. package/src/chain/blocks/importBlock.ts +17 -7
  200. package/src/chain/blocks/verifyBlock.ts +7 -6
  201. package/src/chain/chain.ts +68 -18
  202. package/src/chain/interface.ts +4 -1
  203. package/src/chain/opPools/aggregatedAttestationPool.ts +34 -10
  204. package/src/chain/opPools/opPool.ts +1 -31
  205. package/src/chain/opPools/proposerPreferencesPool.ts +20 -1
  206. package/src/chain/opPools/utils.ts +31 -0
  207. package/src/chain/prepareNextSlot.ts +15 -3
  208. package/src/chain/produceBlock/computeNewStateRoot.ts +9 -2
  209. package/src/chain/produceBlock/produceBlockBody.ts +78 -39
  210. package/src/chain/seenCache/seenGossipBlockInput.ts +25 -1
  211. package/src/chain/shufflingCache.ts +84 -22
  212. package/src/db/beacon.ts +6 -0
  213. package/src/db/buckets.ts +2 -0
  214. package/src/db/interface.ts +4 -0
  215. package/src/db/repositories/blockArchive.ts +2 -2
  216. package/src/db/repositories/blockArchiveIndex.ts +2 -3
  217. package/src/db/repositories/earliestAvailableSlot.ts +19 -0
  218. package/src/db/repositories/index.ts +1 -0
  219. package/src/db/repositories/proposerPreferences.ts +23 -0
  220. package/src/execution/engine/blobVersionedHashes.ts +72 -0
  221. package/src/execution/engine/http.ts +304 -271
  222. package/src/execution/engine/index.ts +41 -7
  223. package/src/execution/engine/interface.ts +1 -1
  224. package/src/execution/engine/jsonRpcTransport.ts +247 -0
  225. package/src/execution/engine/restHttpClient.ts +269 -0
  226. package/src/execution/engine/restTransport.ts +544 -0
  227. package/src/execution/engine/sszTypes.ts +354 -0
  228. package/src/execution/engine/transport.ts +61 -0
  229. package/src/execution/engine/types.ts +34 -74
  230. package/src/execution/engine/utils.ts +12 -2
  231. package/src/metrics/metrics/lodestar.ts +23 -0
  232. package/src/network/core/events.ts +16 -1
  233. package/src/network/processor/gossipHandlers.ts +91 -77
  234. package/src/network/processor/index.ts +3 -3
  235. package/src/node/nodejs.ts +4 -0
  236. package/src/node/options.ts +10 -1
  237. package/src/sync/range/batch.ts +14 -19
  238. package/src/sync/range/chain.ts +9 -7
  239. package/src/sync/range/range.ts +26 -12
  240. package/src/sync/unknownBlock.ts +57 -21
  241. package/src/sync/utils/downloadByRange.ts +123 -58
  242. package/src/util/asyncIterableToEvents.ts +44 -5
  243. package/src/util/metadata.ts +2 -1
  244. package/src/util/sszBytes.ts +25 -12
@@ -1,15 +1,23 @@
1
1
  import {Logger} from "@lodestar/logger";
2
- import {ForkName, ForkPostFulu, ForkPreFulu, ForkSeq, SLOTS_PER_EPOCH, isForkPostFulu} from "@lodestar/params";
2
+ import {
3
+ ForkName,
4
+ ForkPostFulu,
5
+ ForkPreFulu,
6
+ ForkSeq,
7
+ SLOTS_PER_EPOCH,
8
+ isForkPostBellatrix,
9
+ isForkPostFulu,
10
+ } from "@lodestar/params";
3
11
  import {BlobsBundle, ExecutionPayload, ExecutionRequests, Root, RootHex, Wei} from "@lodestar/types";
4
12
  import {BlobAndProof} from "@lodestar/types/deneb";
5
13
  import {BlobAndProofV2} from "@lodestar/types/fulu";
6
- import {strip0xPrefix} from "@lodestar/utils";
14
+ import {isErrorAborted} from "@lodestar/utils";
7
15
  import {Metrics} from "../../metrics/index.js";
8
16
  import {EPOCHS_PER_BATCH} from "../../sync/constants.js";
9
17
  import {getLodestarClientVersion} from "../../util/metadata.js";
18
+ import {isQueueErrorAborted} from "../../util/queue/errors.js";
10
19
  import {JobItemQueue} from "../../util/queue/index.js";
11
20
  import {
12
- ClientCode,
13
21
  ClientVersion,
14
22
  ExecutePayloadResponse,
15
23
  ExecutionEngineState,
@@ -19,32 +27,14 @@ import {
19
27
  PayloadId,
20
28
  VersionedHashes,
21
29
  } from "./interface.js";
22
- import {
23
- ErrorJsonRpcResponse,
24
- HttpRpcError,
25
- IJsonRpcHttpClient,
26
- JsonRpcHttpClientEvent,
27
- ReqOpts,
28
- } from "./jsonRpcHttpClient.js";
30
+ import {ErrorJsonRpcResponse, HttpRpcError, JsonRpcHttpClientEvent} from "./jsonRpcHttpClient.js";
29
31
  import {PayloadIdCache} from "./payloadIdCache.js";
30
- import {
31
- BLOB_AND_PROOF_V2_RPC_BYTES,
32
- EngineApiRpcParamTypes,
33
- EngineApiRpcReturnTypes,
34
- ExecutionPayloadBodyV2,
35
- assertReqSizeLimit,
36
- deserializeBlobAndProofs,
37
- deserializeBlobAndProofsV2,
38
- deserializeBlobAndProofsV2IntoBytes,
39
- deserializeExecutionPayloadBodyV2,
40
- parseExecutionPayload,
41
- serializeBeaconBlockRoot,
42
- serializeExecutionPayload,
43
- serializeExecutionRequests,
44
- serializePayloadAttributes,
45
- serializeVersionedHashes,
46
- } from "./types.js";
47
- import {bytesToData, getExecutionEngineState} from "./utils.js";
32
+ import {EngineRestError, EngineRestResponseError, isRetryableEngineRestError} from "./restHttpClient.js";
33
+ import {EngineCapabilities, RestEngineTransport} from "./restTransport.js";
34
+ import {executionForkName} from "./sszTypes.js";
35
+ import {IEngineTransport, PayloadStatusResult} from "./transport.js";
36
+ import {ExecutionPayloadBodyV2, serializePayloadAttributes} from "./types.js";
37
+ import {getExecutionEngineState} from "./utils.js";
48
38
 
49
39
  export type ExecutionEngineModules = {
50
40
  signal: AbortSignal;
@@ -52,11 +42,21 @@ export type ExecutionEngineModules = {
52
42
  logger: Logger;
53
43
  };
54
44
 
45
+ /**
46
+ * Engine API transport selection
47
+ * - `auto`: negotiate REST with one execution URL; use JSON-RPC for multiple URLs or unadvertised features
48
+ * - `ssz`: always use the REST API with SSZ encoding
49
+ * - `json-rpc`: always use the legacy JSON-RPC API
50
+ */
51
+ export type EngineApiMode = "auto" | "ssz" | "json-rpc";
52
+ export const engineApiModes: EngineApiMode[] = ["auto", "ssz", "json-rpc"];
53
+
55
54
  export type ExecutionEngineHttpOpts = {
56
55
  urls: string[];
57
56
  retries: number;
58
57
  retryDelay: number;
59
58
  timeout?: number;
59
+ engineApi?: EngineApiMode;
60
60
  /**
61
61
  * 256 bit jwt secret in hex format without the leading 0x. If provided, the execution engine
62
62
  * rpc requests will be bundled by an authorization header having a fresh jwt token on each
@@ -92,38 +92,37 @@ export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = {
92
92
  retries: 2,
93
93
  retryDelay: 2000,
94
94
  timeout: 12000,
95
+ engineApi: "auto",
95
96
  };
96
97
 
98
+ export type ExecutionEngineTransports = {
99
+ jsonRpc: IEngineTransport;
100
+ rest?: RestEngineTransport;
101
+ };
102
+
103
+ type RestSupport =
104
+ | {state: "pending"; error?: Error}
105
+ | {state: "unsupported"}
106
+ | {state: "supported"; capabilities: EngineCapabilities};
107
+
97
108
  /**
98
109
  * Size for the serializing queue for fcUs and new payloads, the max length could be equal to
99
110
  * EPOCHS_PER_BATCH * 2 in case new payloads are also not awaited serially
100
111
  */
101
112
  const QUEUE_MAX_LENGTH = EPOCHS_PER_BATCH * SLOTS_PER_EPOCH * 2;
102
113
 
103
- /**
104
- * Maximum number of version hashes that can be sent in a getBlobs request
105
- * Clients must support at least 128 versionedHashes, so we avoid sending more
106
- * https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#specification-3
107
- */
108
- const MAX_VERSIONED_HASHES = 128;
109
-
110
- // Define static options once to prevent extra allocations
111
- const notifyNewPayloadOpts: ReqOpts = {routeId: "notifyNewPayload"};
112
- const forkchoiceUpdatedV1Opts: ReqOpts = {routeId: "forkchoiceUpdated"};
113
- const getPayloadOpts: ReqOpts = {routeId: "getPayload"};
114
- const getPayloadBodiesByHashOpts: ReqOpts = {routeId: "getPayloadBodiesByHash"};
115
- const getBlobsV1Opts: ReqOpts = {routeId: "getBlobsV1"};
116
- const getBlobsV2Opts: ReqOpts = {routeId: "getBlobsV2"};
117
- const getClientVersionOpts: ReqOpts = {routeId: "getClientVersion"};
114
+ /** Minimum delay between capabilities probes while the execution client is unreachable */
115
+ const REST_PROBE_RETRY_MS = 12_000;
118
116
 
119
117
  /**
120
- * based on Ethereum JSON-RPC API and inherits the following properties of this standard:
121
- * - Supported communication protocols (HTTP and WebSocket)
122
- * - Message format and encoding notation
123
- * - Error codes improvement proposal
118
+ * Drives the execution client over the engine API, either the legacy `engine_*` JSON-RPC methods
119
+ * or the REST API with SSZ encoded bodies.
120
+ * https://github.com/ethereum/execution-apis/tree/main/src/engine
124
121
  *
125
- * Client software MUST expose Engine API at a port independent from JSON-RPC API. The default port for the Engine API is 8550 for HTTP and 8551 for WebSocket.
126
- * https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.1/src/engine/interop/specification.md
122
+ * In `auto` mode a capabilities response from a server without the REST API, a 4xx other than
123
+ * 401/403, selects JSON-RPC until the EL reconnects. Transient discovery failures use JSON-RPC while
124
+ * awaiting another probe; authentication failures fail visibly, malformed capabilities fall back to
125
+ * JSON-RPC with a warning. Forks and blob revisions the EL does not advertise also use JSON-RPC.
127
126
  */
128
127
  export class ExecutionEngineHttp implements IExecutionEngine {
129
128
  private logger: Logger;
@@ -147,42 +146,55 @@ export class ExecutionEngineHttp implements IExecutionEngine {
147
146
  * the order of new payloads and fcUs is pretty important to EL, this queue will serialize the calls in the
148
147
  * order with which we make them.
149
148
  */
150
- private readonly rpcFetchQueue: JobItemQueue<[EngineRequest], EngineResponse>;
149
+ private readonly queue: JobItemQueue<[() => Promise<unknown>], unknown>;
151
150
 
152
- private jobQueueProcessor = async ({method, params, methodOpts}: EngineRequest): Promise<EngineResponse> => {
153
- return this.rpc.fetchWithRetries<EngineApiRpcReturnTypes[typeof method], EngineApiRpcParamTypes[typeof method]>(
154
- {method, params},
155
- methodOpts
156
- );
157
- };
151
+ private readonly jsonRpc: IEngineTransport;
152
+ private readonly rest: RestEngineTransport | null;
153
+ private readonly engineApi: EngineApiMode;
154
+ private restSupport: RestSupport = {state: "pending"};
155
+ private restProbe: Promise<RestSupport> | null = null;
156
+ private lastRestProbeMs = Number.NEGATIVE_INFINITY;
157
+ private readonly loggedRestFallbacks = new Set<ForkName | "v1" | "v2">();
158
158
 
159
159
  constructor(
160
- private readonly rpc: IJsonRpcHttpClient,
160
+ {jsonRpc, rest}: ExecutionEngineTransports,
161
161
  {metrics, signal, logger}: ExecutionEngineModules,
162
162
  private readonly opts?: ExecutionEngineHttpOpts
163
163
  ) {
164
- this.rpcFetchQueue = new JobItemQueue<[EngineRequest], EngineResponse>(
165
- this.jobQueueProcessor,
164
+ this.queue = new JobItemQueue<[() => Promise<unknown>], unknown>(
165
+ (job) => job(),
166
166
  {maxLength: QUEUE_MAX_LENGTH, maxConcurrency: 1, noYieldIfOneItem: true, signal},
167
167
  metrics?.engineHttpProcessorQueue
168
168
  );
169
169
  this.logger = logger;
170
170
  this.metrics = metrics ?? null;
171
+ this.jsonRpc = jsonRpc;
172
+ this.rest = rest ?? null;
173
+ this.engineApi = opts?.engineApi ?? "auto";
171
174
 
172
- this.rpc.emitter.on(JsonRpcHttpClientEvent.ERROR, ({error}) => {
173
- this.updateEngineState(getExecutionEngineState({payloadError: error, oldState: this.state}));
174
- });
175
+ if (this.engineApi === "ssz" && this.rest === null) {
176
+ throw Error("REST transport is required for engineApi=ssz");
177
+ }
175
178
 
176
- this.rpc.emitter.on(JsonRpcHttpClientEvent.RESPONSE, () => {
177
- if (this.clientVersion === undefined) {
178
- this.clientVersion = null;
179
- // This statement should only be called first time receiving response after startup
180
- this.getClientVersion(getLodestarClientVersion(this.opts)).catch((e) => {
181
- this.logger.debug("Unable to get execution client version", {}, e);
182
- });
183
- }
184
- this.updateEngineState(getExecutionEngineState({targetState: ExecutionEngineState.ONLINE, oldState: this.state}));
179
+ // REST errors are handled in withTransport after compatibility fallback.
180
+ this.jsonRpc.emitter.on(JsonRpcHttpClientEvent.ERROR, ({error}) => {
181
+ this.updateEngineState(getExecutionEngineState({payloadError: error, oldState: this.state}), error);
185
182
  });
183
+
184
+ for (const transport of [this.jsonRpc, this.rest]) {
185
+ transport?.emitter.on(JsonRpcHttpClientEvent.RESPONSE, () => {
186
+ if (this.clientVersion === undefined) {
187
+ this.clientVersion = null;
188
+ // This statement should only be called first time receiving response after startup
189
+ this.getClientVersion(getLodestarClientVersion(this.opts)).catch((e) => {
190
+ this.logger.debug("Unable to get execution client version", {}, e);
191
+ });
192
+ }
193
+ this.updateEngineState(
194
+ getExecutionEngineState({targetState: ExecutionEngineState.ONLINE, oldState: this.state})
195
+ );
196
+ });
197
+ }
186
198
  }
187
199
 
188
200
  /**
@@ -214,20 +226,7 @@ export class ExecutionEngineHttp implements IExecutionEngine {
214
226
  parentBlockRoot?: Root,
215
227
  executionRequests?: ExecutionRequests
216
228
  ): Promise<ExecutePayloadResponse> {
217
- const method =
218
- ForkSeq[fork] >= ForkSeq.gloas
219
- ? "engine_newPayloadV5"
220
- : ForkSeq[fork] >= ForkSeq.electra
221
- ? "engine_newPayloadV4"
222
- : ForkSeq[fork] >= ForkSeq.deneb
223
- ? "engine_newPayloadV3"
224
- : ForkSeq[fork] >= ForkSeq.capella
225
- ? "engine_newPayloadV2"
226
- : "engine_newPayloadV1";
227
-
228
- const serializedExecutionPayload = serializeExecutionPayload(fork, executionPayload);
229
-
230
- let engineRequest: EngineRequest;
229
+ // Validate before queueing, argument errors must not be reported as an unavailable execution client
231
230
  if (ForkSeq[fork] >= ForkSeq.deneb) {
232
231
  if (versionedHashes === undefined) {
233
232
  throw Error(`versionedHashes required in notifyNewPayload for fork=${fork}`);
@@ -235,49 +234,31 @@ export class ExecutionEngineHttp implements IExecutionEngine {
235
234
  if (parentBlockRoot === undefined) {
236
235
  throw Error(`parentBlockRoot required in notifyNewPayload for fork=${fork}`);
237
236
  }
238
-
239
- const serializedVersionedHashes = serializeVersionedHashes(versionedHashes);
240
- const parentBeaconBlockRoot = serializeBeaconBlockRoot(parentBlockRoot);
241
-
242
- if (ForkSeq[fork] >= ForkSeq.electra) {
243
- if (executionRequests === undefined) {
244
- throw Error(`executionRequests required in notifyNewPayload for fork=${fork}`);
245
- }
246
- const serializedExecutionRequests = serializeExecutionRequests(fork, executionRequests);
247
- engineRequest = {
248
- method: ForkSeq[fork] >= ForkSeq.gloas ? "engine_newPayloadV5" : "engine_newPayloadV4",
249
- params: [
250
- serializedExecutionPayload,
251
- serializedVersionedHashes,
252
- parentBeaconBlockRoot,
253
- serializedExecutionRequests,
254
- ],
255
- methodOpts: notifyNewPayloadOpts,
256
- };
257
- } else {
258
- engineRequest = {
259
- method: "engine_newPayloadV3",
260
- params: [serializedExecutionPayload, serializedVersionedHashes, parentBeaconBlockRoot],
261
- methodOpts: notifyNewPayloadOpts,
262
- };
237
+ if (ForkSeq[fork] >= ForkSeq.electra && executionRequests === undefined) {
238
+ throw Error(`executionRequests required in notifyNewPayload for fork=${fork}`);
263
239
  }
264
- } else {
265
- const method = ForkSeq[fork] >= ForkSeq.capella ? "engine_newPayloadV2" : "engine_newPayloadV1";
266
- engineRequest = {
267
- method,
268
- params: [serializedExecutionPayload],
269
- methodOpts: notifyNewPayloadOpts,
270
- };
271
240
  }
272
241
 
273
- const {status, latestValidHash, validationError} = await (
274
- this.rpcFetchQueue.push(engineRequest) as Promise<EngineApiRpcReturnTypes[typeof method]>
275
- ).catch((e: Error) => {
276
- if (e instanceof HttpRpcError || e instanceof ErrorJsonRpcResponse) {
277
- return {status: ExecutionPayloadStatus.ELERROR, latestValidHash: null, validationError: e.message};
278
- }
279
- return {status: ExecutionPayloadStatus.UNAVAILABLE, latestValidHash: null, validationError: e.message};
280
- });
242
+ let result: PayloadStatusResult;
243
+ try {
244
+ result = await this.enqueue(() =>
245
+ this.withTransport(fork, undefined, (transport) =>
246
+ transport.newPayload(fork, executionPayload, versionedHashes, parentBlockRoot, executionRequests)
247
+ )
248
+ );
249
+ } catch (e) {
250
+ const status = isEngineResponseError(e as Error)
251
+ ? ExecutionPayloadStatus.ELERROR
252
+ : ExecutionPayloadStatus.UNAVAILABLE;
253
+ // Only newPayload treats an unreachable execution client, including a timeout, as offline
254
+ const newState =
255
+ status === ExecutionPayloadStatus.UNAVAILABLE && !isErrorAborted(e) && !isQueueErrorAborted(e)
256
+ ? getExecutionEngineState({payloadStatus: status, oldState: this.state})
257
+ : getExecutionEngineState({payloadError: e, oldState: this.state});
258
+ this.updateEngineState(newState, e as Error);
259
+ return {status, latestValidHash: null, validationError: (e as Error).message};
260
+ }
261
+ const {status, latestValidHash, validationError} = result;
281
262
 
282
263
  this.updateEngineState(getExecutionEngineState({payloadStatus: status, oldState: this.state}));
283
264
 
@@ -346,32 +327,14 @@ export class ExecutionEngineHttp implements IExecutionEngine {
346
327
  finalizedBlockHash: RootHex,
347
328
  payloadAttributes?: PayloadAttributes
348
329
  ): Promise<PayloadId | null> {
349
- // Once on capella, should this need to be permanently switched to v2 when payload attrs
350
- // not provided
351
- const method =
352
- ForkSeq[fork] >= ForkSeq.gloas
353
- ? "engine_forkchoiceUpdatedV4"
354
- : ForkSeq[fork] >= ForkSeq.deneb
355
- ? "engine_forkchoiceUpdatedV3"
356
- : ForkSeq[fork] >= ForkSeq.capella
357
- ? "engine_forkchoiceUpdatedV2"
358
- : "engine_forkchoiceUpdatedV1";
359
- const payloadAttributesRpc = payloadAttributes ? serializePayloadAttributes(payloadAttributes) : undefined;
360
- // If we are just fcUing and not asking execution for payload, retry is not required
361
- // and we can move on, as the next fcU will be issued soon on the new slot
362
- const fcUReqOpts =
363
- payloadAttributes !== undefined ? forkchoiceUpdatedV1Opts : {...forkchoiceUpdatedV1Opts, retries: 0};
364
-
365
- const request = this.rpcFetchQueue.push({
366
- method,
367
- params: [{headBlockHash, safeBlockHash, finalizedBlockHash}, payloadAttributesRpc],
368
- methodOpts: fcUReqOpts,
369
- }) as Promise<EngineApiRpcReturnTypes[typeof method]>;
370
-
371
330
  const {
372
- payloadStatus: {status, latestValidHash: _latestValidHash, validationError},
331
+ payloadStatus: {status, validationError},
373
332
  payloadId,
374
- } = await request;
333
+ } = await this.enqueue(() =>
334
+ this.withTransport(fork, undefined, (transport) =>
335
+ transport.forkchoiceUpdated(fork, headBlockHash, safeBlockHash, finalizedBlockHash, payloadAttributes)
336
+ )
337
+ );
375
338
 
376
339
  this.updateEngineState(getExecutionEngineState({payloadStatus: status, oldState: this.state}));
377
340
  this.metrics?.engineNotifyForkchoiceUpdateResult.inc({result: status});
@@ -379,15 +342,16 @@ export class ExecutionEngineHttp implements IExecutionEngine {
379
342
  switch (status) {
380
343
  case ExecutionPayloadStatus.VALID:
381
344
  // if payloadAttributes are provided, a valid payloadId is expected
382
- if (payloadAttributesRpc) {
383
- if (!payloadId || payloadId === "0x") {
345
+ if (payloadAttributes) {
346
+ if (payloadId === null) {
384
347
  throw Error(`Received invalid payloadId=${payloadId}`);
385
348
  }
386
349
 
350
+ const payloadAttributesRpc = serializePayloadAttributes(payloadAttributes);
387
351
  this.payloadIdCache.add({headBlockHash, finalizedBlockHash, ...payloadAttributesRpc}, payloadId);
388
352
  void this.prunePayloadIdCache();
389
353
  }
390
- return payloadId !== "0x" ? payloadId : null;
354
+ return payloadId;
391
355
 
392
356
  case ExecutionPayloadStatus.SYNCING:
393
357
  // Throw error on syncing if requested to produce a block, else silently ignore
@@ -425,40 +389,7 @@ export class ExecutionEngineHttp implements IExecutionEngine {
425
389
  executionRequests?: ExecutionRequests;
426
390
  shouldOverrideBuilder?: boolean;
427
391
  }> {
428
- let method: keyof EngineApiRpcReturnTypes;
429
- switch (fork) {
430
- case ForkName.phase0:
431
- case ForkName.altair:
432
- case ForkName.bellatrix:
433
- method = "engine_getPayloadV1";
434
- break;
435
- case ForkName.capella:
436
- method = "engine_getPayloadV2";
437
- break;
438
- case ForkName.deneb:
439
- method = "engine_getPayloadV3";
440
- break;
441
- case ForkName.electra:
442
- method = "engine_getPayloadV4";
443
- break;
444
- case ForkName.fulu:
445
- method = "engine_getPayloadV5";
446
- break;
447
- default:
448
- method = "engine_getPayloadV6";
449
- break;
450
- }
451
- const payloadResponse = await this.rpc.fetchWithRetries<
452
- EngineApiRpcReturnTypes[typeof method],
453
- EngineApiRpcParamTypes[typeof method]
454
- >(
455
- {
456
- method,
457
- params: [payloadId],
458
- },
459
- getPayloadOpts
460
- );
461
- return parseExecutionPayload(fork, payloadResponse);
392
+ return this.withTransport(fork, undefined, (transport) => transport.getPayload(fork, payloadId));
462
393
  }
463
394
 
464
395
  async prunePayloadIdCache(): Promise<void> {
@@ -466,13 +397,9 @@ export class ExecutionEngineHttp implements IExecutionEngine {
466
397
  }
467
398
 
468
399
  async getPayloadBodiesByHashV2(blockHashes: RootHex[]): Promise<(ExecutionPayloadBodyV2 | null)[]> {
469
- const method = "engine_getPayloadBodiesByHashV2";
470
- assertReqSizeLimit(blockHashes.length, 32);
471
- const response = await this.rpc.fetchWithRetries<
472
- EngineApiRpcReturnTypes[typeof method],
473
- EngineApiRpcParamTypes[typeof method]
474
- >({method, params: [blockHashes]}, getPayloadBodiesByHashOpts);
475
- return response.map(deserializeExecutionPayloadBodyV2);
400
+ return this.withTransport(ForkName.gloas, undefined, (transport) =>
401
+ transport.getPayloadBodiesByHashV2(blockHashes)
402
+ );
476
403
  }
477
404
 
478
405
  async getBlobs(
@@ -490,95 +417,168 @@ export class ExecutionEngineHttp implements IExecutionEngine {
490
417
  versionedHashes: VersionedHashes,
491
418
  buffers?: Uint8Array[]
492
419
  ): Promise<BlobAndProofV2[] | (BlobAndProof | null)[] | null> {
493
- assertReqSizeLimit(versionedHashes.length, MAX_VERSIONED_HASHES);
494
- const versionedHashesHex = versionedHashes.map(bytesToData);
495
420
  if (isForkPostFulu(fork)) {
496
- return await this.getBlobsV2(versionedHashesHex, buffers);
421
+ return this.withTransport(undefined, "v2", (transport) => transport.getBlobsV2(versionedHashes, buffers));
497
422
  }
498
- return await this.getBlobsV1(versionedHashesHex);
423
+ return this.withTransport(undefined, "v1", (transport) => transport.getBlobsV1(versionedHashes));
499
424
  }
500
425
 
501
- private async getBlobsV1(versionedHashesHex: string[]) {
502
- const response = await this.rpc.fetchWithRetries<
503
- EngineApiRpcReturnTypes["engine_getBlobsV1"],
504
- EngineApiRpcParamTypes["engine_getBlobsV1"]
505
- >(
506
- {
507
- method: "engine_getBlobsV1",
508
- params: [versionedHashesHex],
509
- },
510
- getBlobsV1Opts
511
- );
512
-
513
- const invalidLength = response.length !== versionedHashesHex.length;
426
+ private enqueue<T>(job: () => Promise<T>): Promise<T> {
427
+ return this.queue.push(job) as Promise<T>;
428
+ }
514
429
 
515
- if (invalidLength) {
516
- const error = `Invalid engine_getBlobsV1 response length=${response.length} versionedHashes=${versionedHashesHex.length}`;
517
- this.logger.error(error);
518
- throw Error(error);
430
+ /**
431
+ * Run a call on the selected transport. An execution client that advertises a fork in its
432
+ * capabilities but rejects it with `unsupported-fork` is not spec compliant, keep the node
433
+ * functional by serving that fork over JSON-RPC from then on.
434
+ */
435
+ private async withTransport<T>(
436
+ fork: ForkName | undefined,
437
+ blobsRevision: "v1" | "v2" | undefined,
438
+ fn: (transport: IEngineTransport) => Promise<T>
439
+ ): Promise<T> {
440
+ const transport = await this.getTransport(fork, blobsRevision);
441
+ this.metrics?.engineApiRequests.inc({transport: transport === this.rest ? "ssz" : "json-rpc"});
442
+ try {
443
+ return await fn(transport);
444
+ } catch (e) {
445
+ if (fork !== undefined && transport === this.rest && this.engineApi === "auto" && isUnsupportedForkError(e)) {
446
+ this.disableRestForFork(fork, e);
447
+ this.metrics?.engineApiRequests.inc({transport: "json-rpc"});
448
+ return fn(this.jsonRpc);
449
+ }
450
+ if (transport === this.rest) {
451
+ this.updateEngineState(getExecutionEngineState({payloadError: e, oldState: this.state}), e as Error);
452
+ }
453
+ throw e;
519
454
  }
455
+ }
520
456
 
521
- return response.map(deserializeBlobAndProofs);
457
+ private disableRestForFork(fork: ForkName, e: EngineRestError): void {
458
+ const executionFork = isForkPostBellatrix(fork) ? executionForkName[fork] : null;
459
+ if (this.restSupport.state === "supported" && executionFork !== null) {
460
+ this.restSupport.capabilities.supportedForks.delete(executionFork);
461
+ }
462
+ if (!this.loggedRestFallbacks.has(fork)) {
463
+ this.loggedRestFallbacks.add(fork);
464
+ this.logger.debug("REST engine API rejected an advertised fork, using JSON-RPC until reconnect", {
465
+ fork,
466
+ executionFork,
467
+ status: e.status,
468
+ type: e.type,
469
+ detail: e.detail,
470
+ });
471
+ }
522
472
  }
523
473
 
524
- private async getBlobsV2(versionedHashesHex: string[], buffers?: Uint8Array[]) {
525
- if (buffers) {
526
- // Callers preallocate one buffer per max blobs of the epoch, only the first entries are used
527
- if (buffers.length < versionedHashesHex.length) {
528
- throw Error(`Not enough buffers length=${buffers.length} versionedHashes=${versionedHashesHex.length}`);
529
- }
474
+ /**
475
+ * Pick the transport for a call. Fork-scoped calls only go over REST if the execution client
476
+ * advertises that fork, blob requests only if it serves the needed `/blobs/vN` revision.
477
+ */
478
+ private async getTransport(fork?: ForkName, blobsRevision?: "v1" | "v2"): Promise<IEngineTransport> {
479
+ if (this.rest === null || this.engineApi === "json-rpc") {
480
+ return this.jsonRpc;
481
+ }
482
+ const restSupport = await this.probeRestSupport(this.rest);
483
+ if (restSupport.state === "pending" && restSupport.error) {
484
+ this.updateEngineState(
485
+ getExecutionEngineState({payloadError: restSupport.error, oldState: this.state}),
486
+ restSupport.error
487
+ );
488
+ throw restSupport.error;
489
+ }
490
+ if (this.engineApi === "ssz") {
491
+ return this.rest;
492
+ }
493
+ if (restSupport.state !== "supported") {
494
+ return this.jsonRpc;
495
+ }
530
496
 
531
- for (const [i, buffer] of buffers.entries()) {
532
- if (buffer.length !== BLOB_AND_PROOF_V2_RPC_BYTES) {
533
- throw Error(`Invalid buffer[${i}] length=${buffer.length} expected=${BLOB_AND_PROOF_V2_RPC_BYTES}`);
497
+ if (fork !== undefined) {
498
+ const executionFork = isForkPostBellatrix(fork) ? executionForkName[fork] : null;
499
+ if (executionFork === null || !restSupport.capabilities.supportedForks.has(executionFork)) {
500
+ if (!this.loggedRestFallbacks.has(fork)) {
501
+ this.loggedRestFallbacks.add(fork);
502
+ this.logger.debug("Using JSON-RPC for a fork not advertised by the REST engine API", {fork, executionFork});
534
503
  }
504
+ return this.jsonRpc;
535
505
  }
536
506
  }
507
+ if (blobsRevision !== undefined && !restSupport.capabilities.blobsRevisions.has(blobsRevision)) {
508
+ if (!this.loggedRestFallbacks.has(blobsRevision)) {
509
+ this.loggedRestFallbacks.add(blobsRevision);
510
+ this.logger.debug("Using JSON-RPC for a blob revision not advertised by the REST engine API", {blobsRevision});
511
+ }
512
+ return this.jsonRpc;
513
+ }
537
514
 
538
- const response = await this.rpc.fetchWithRetries<
539
- EngineApiRpcReturnTypes["engine_getBlobsV2"],
540
- EngineApiRpcParamTypes["engine_getBlobsV2"]
541
- >(
542
- {
543
- method: "engine_getBlobsV2",
544
- params: [versionedHashesHex],
545
- },
546
- getBlobsV2Opts
547
- );
548
-
549
- // engine_getBlobsV2 does not return partial responses. It returns null if any blob is not found
550
- const invalidLength = !!response && response.length !== versionedHashesHex.length;
515
+ return this.rest;
516
+ }
551
517
 
552
- if (invalidLength) {
553
- const error = `Invalid engine_getBlobsV2 response length=${response?.length ?? "null"} versionedHashes=${versionedHashesHex.length}`;
554
- this.logger.error(error);
555
- throw Error(error);
518
+ private probeRestSupport(rest: RestEngineTransport): Promise<RestSupport> {
519
+ if (this.restSupport.state !== "pending") {
520
+ return Promise.resolve(this.restSupport);
556
521
  }
557
-
558
- if (response == null) {
559
- return null;
522
+ if (this.restProbe !== null) {
523
+ return this.restProbe;
560
524
  }
561
-
562
- if (buffers) {
563
- // getBlobsV2() is designed to called once per slot so we expect to have buffers
564
- return response.map((data, i) => deserializeBlobAndProofsV2IntoBytes(data, buffers[i]));
525
+ // Transport errors keep the probe pending, do not retry on every call while the EL is down
526
+ if (Date.now() - this.lastRestProbeMs < REST_PROBE_RETRY_MS) {
527
+ return Promise.resolve(this.restSupport);
565
528
  }
566
529
 
567
- return response.map(deserializeBlobAndProofsV2);
530
+ this.restProbe = rest
531
+ .getCapabilities()
532
+ .then(
533
+ (capabilities): RestSupport => {
534
+ this.restSupport = {state: "supported", capabilities};
535
+ this.loggedRestFallbacks.clear();
536
+ this.logger.debug("Discovered REST engine API capabilities", {
537
+ supportedForks: Array.from(capabilities.supportedForks).join(","),
538
+ blobsRevisions: Array.from(capabilities.blobsRevisions).join(","),
539
+ ...capabilities.limits,
540
+ });
541
+ return this.restSupport;
542
+ },
543
+ (e: Error): RestSupport => {
544
+ if (this.engineApi === "auto" && isRestApiAbsent(e)) {
545
+ this.restSupport = {state: "unsupported"};
546
+ this.logger.debug("Execution client does not support engine API over REST, using JSON-RPC", {
547
+ status: e.status,
548
+ type: e.type ?? "unknown",
549
+ });
550
+ } else if (this.engineApi === "auto" && e instanceof EngineRestResponseError) {
551
+ // The REST API answered but cannot be trusted, auto mode must keep the node on a working transport
552
+ this.restSupport = {state: "unsupported"};
553
+ this.logger.warn("Invalid engine API capabilities, using JSON-RPC until reconnect", {}, e);
554
+ } else {
555
+ const transient = isRetryableEngineRestError(e);
556
+ this.restSupport = {state: "pending", error: this.engineApi === "auto" && transient ? undefined : e};
557
+ this.logger.debug(
558
+ "Unable to probe engine API capabilities",
559
+ {
560
+ engineApi: this.engineApi,
561
+ fallback: this.restSupport.error ? "none" : "json-rpc",
562
+ retryAfterMs: REST_PROBE_RETRY_MS,
563
+ },
564
+ e
565
+ );
566
+ }
567
+ return this.restSupport;
568
+ }
569
+ )
570
+ .finally(() => {
571
+ this.restProbe = null;
572
+ this.lastRestProbeMs = Date.now();
573
+ });
574
+
575
+ return this.restProbe;
568
576
  }
569
577
 
570
578
  private async getClientVersion(clientVersion: ClientVersion): Promise<ClientVersion[]> {
571
- const method = "engine_getClientVersionV1";
572
-
573
- const response = await this.rpc.fetchWithRetries<
574
- EngineApiRpcReturnTypes[typeof method],
575
- EngineApiRpcParamTypes[typeof method]
576
- >({method, params: [{...clientVersion, commit: `0x${clientVersion.commit}`}]}, getClientVersionOpts);
577
-
578
- const clientVersions = response.map((cv) => {
579
- const code = cv.code in ClientCode ? ClientCode[cv.code as keyof typeof ClientCode] : ClientCode.XX;
580
- return {code, name: cv.name, version: cv.version, commit: strip0xPrefix(cv.commit)};
581
- });
579
+ const clientVersions = await this.withTransport(undefined, undefined, (transport) =>
580
+ transport.getClientVersion(clientVersion)
581
+ );
582
582
 
583
583
  if (clientVersions.length === 0) {
584
584
  throw Error("Received empty client versions array");
@@ -590,7 +590,7 @@ export class ExecutionEngineHttp implements IExecutionEngine {
590
590
  return clientVersions;
591
591
  }
592
592
 
593
- private updateEngineState(newState: ExecutionEngineState): void {
593
+ private updateEngineState(newState: ExecutionEngineState, error?: Error): void {
594
594
  const oldState = this.state;
595
595
 
596
596
  if (oldState === newState) return;
@@ -598,22 +598,33 @@ export class ExecutionEngineHttp implements IExecutionEngine {
598
598
  switch (newState) {
599
599
  case ExecutionEngineState.ONLINE:
600
600
  this.logger.info("Execution client became online", {oldState, newState});
601
+ if (oldState === ExecutionEngineState.AUTH_FAILED) {
602
+ this.restSupport = {state: "pending"};
603
+ this.lastRestProbeMs = Number.NEGATIVE_INFINITY;
604
+ }
601
605
  this.getClientVersion(getLodestarClientVersion(this.opts)).catch((e) => {
602
606
  this.logger.debug("Unable to get execution client version", {}, e);
603
607
  this.clientVersion = null;
604
608
  });
605
609
  break;
606
610
  case ExecutionEngineState.OFFLINE:
607
- this.logger.error("Execution client went offline", {oldState, newState});
611
+ // Reprobe before choosing a transport for the next call after a disconnect.
612
+ this.restSupport = {state: "pending"};
613
+ this.lastRestProbeMs = Number.NEGATIVE_INFINITY;
614
+ this.logger.error("Execution client went offline", {oldState, newState}, error);
608
615
  break;
609
616
  case ExecutionEngineState.SYNCED:
610
617
  this.logger.info("Execution client is synced", {oldState, newState});
611
618
  break;
612
619
  case ExecutionEngineState.SYNCING:
613
- this.logger.warn("Execution client is syncing", {oldState, newState});
620
+ this.logger.warn(
621
+ error ? "Execution client request failed" : "Execution client is syncing",
622
+ {oldState, newState},
623
+ error
624
+ );
614
625
  break;
615
626
  case ExecutionEngineState.AUTH_FAILED:
616
- this.logger.error("Execution client authentication failed", {oldState, newState});
627
+ this.logger.error("Execution client authentication failed", {oldState, newState}, error);
617
628
  break;
618
629
  }
619
630
 
@@ -621,10 +632,32 @@ export class ExecutionEngineHttp implements IExecutionEngine {
621
632
  }
622
633
  }
623
634
 
624
- type EngineRequestKey = keyof EngineApiRpcParamTypes;
625
- type EngineRequestByKey = {
626
- [K in EngineRequestKey]: {method: K; params: EngineApiRpcParamTypes[K]; methodOpts: ReqOpts};
627
- };
628
- type EngineRequest = EngineRequestByKey[EngineRequestKey];
629
- type EngineResponseByKey = {[K in EngineRequestKey]: EngineApiRpcReturnTypes[K]};
630
- type EngineResponse = EngineResponseByKey[EngineRequestKey];
635
+ /** The execution client answered, as opposed to being unreachable */
636
+ function isEngineResponseError(e: Error): boolean {
637
+ return (
638
+ e instanceof HttpRpcError ||
639
+ e instanceof ErrorJsonRpcResponse ||
640
+ e instanceof EngineRestError ||
641
+ e instanceof EngineRestResponseError
642
+ );
643
+ }
644
+
645
+ /**
646
+ * Legacy JSON-RPC servers and proxies reject the capabilities probe in different ways, a 404 or
647
+ * another client error such as 405. Neither serves the REST API. Authentication failures and
648
+ * transient client errors say nothing about REST support.
649
+ */
650
+ function isRestApiAbsent(e: Error): e is EngineRestError {
651
+ return (
652
+ e instanceof EngineRestError &&
653
+ e.status >= 400 &&
654
+ e.status < 500 &&
655
+ e.status !== 401 &&
656
+ e.status !== 403 &&
657
+ !isRetryableEngineRestError(e)
658
+ );
659
+ }
660
+
661
+ function isUnsupportedForkError(e: unknown): e is EngineRestError {
662
+ return e instanceof EngineRestError && e.status === 400 && e.type === "/engine-api/errors/unsupported-fork";
663
+ }