@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.
- package/lib/api/impl/beacon/state/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/state/index.js +6 -4
- package/lib/api/impl/beacon/state/index.js.map +1 -1
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +26 -0
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/beaconProposerCache.d.ts +6 -0
- package/lib/chain/beaconProposerCache.d.ts.map +1 -1
- package/lib/chain/beaconProposerCache.js +16 -0
- package/lib/chain/beaconProposerCache.js.map +1 -1
- package/lib/chain/blocks/importBlock.d.ts.map +1 -1
- package/lib/chain/blocks/importBlock.js +7 -5
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
- package/lib/chain/blocks/verifyBlock.js +6 -6
- package/lib/chain/blocks/verifyBlock.js.map +1 -1
- package/lib/chain/chain.d.ts +1 -0
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +49 -15
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/interface.d.ts +2 -0
- package/lib/chain/interface.d.ts.map +1 -1
- package/lib/chain/opPools/opPool.d.ts.map +1 -1
- package/lib/chain/opPools/opPool.js +1 -23
- package/lib/chain/opPools/opPool.js.map +1 -1
- package/lib/chain/opPools/proposerPreferencesPool.d.ts +4 -0
- package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
- package/lib/chain/opPools/proposerPreferencesPool.js +13 -1
- package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
- package/lib/chain/opPools/utils.d.ts +11 -0
- package/lib/chain/opPools/utils.d.ts.map +1 -1
- package/lib/chain/opPools/utils.js +22 -0
- package/lib/chain/opPools/utils.js.map +1 -1
- package/lib/chain/prepareNextSlot.d.ts.map +1 -1
- package/lib/chain/prepareNextSlot.js +11 -3
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.d.ts +8 -0
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +40 -28
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/shufflingCache.d.ts +17 -8
- package/lib/chain/shufflingCache.d.ts.map +1 -1
- package/lib/chain/shufflingCache.js +68 -18
- package/lib/chain/shufflingCache.js.map +1 -1
- package/lib/db/beacon.d.ts +2 -1
- package/lib/db/beacon.d.ts.map +1 -1
- package/lib/db/beacon.js +3 -1
- package/lib/db/beacon.js.map +1 -1
- package/lib/db/buckets.d.ts +2 -1
- package/lib/db/buckets.d.ts.map +1 -1
- package/lib/db/buckets.js +1 -0
- package/lib/db/buckets.js.map +1 -1
- package/lib/db/interface.d.ts +2 -1
- package/lib/db/interface.d.ts.map +1 -1
- package/lib/db/repositories/blockArchiveIndex.d.ts +2 -2
- package/lib/db/repositories/blockArchiveIndex.d.ts.map +1 -1
- package/lib/db/repositories/index.d.ts +1 -0
- package/lib/db/repositories/index.d.ts.map +1 -1
- package/lib/db/repositories/index.js +1 -0
- package/lib/db/repositories/index.js.map +1 -1
- package/lib/db/repositories/proposerPreferences.d.ts +14 -0
- package/lib/db/repositories/proposerPreferences.d.ts.map +1 -0
- package/lib/db/repositories/proposerPreferences.js +21 -0
- package/lib/db/repositories/proposerPreferences.js.map +1 -0
- package/lib/execution/engine/blobVersionedHashes.d.ts +9 -0
- package/lib/execution/engine/blobVersionedHashes.d.ts.map +1 -0
- package/lib/execution/engine/blobVersionedHashes.js +74 -0
- package/lib/execution/engine/blobVersionedHashes.js.map +1 -0
- package/lib/execution/engine/http.d.ts +45 -13
- package/lib/execution/engine/http.d.ts.map +1 -1
- package/lib/execution/engine/http.js +236 -198
- package/lib/execution/engine/http.js.map +1 -1
- package/lib/execution/engine/index.d.ts +3 -2
- package/lib/execution/engine/index.d.ts.map +1 -1
- package/lib/execution/engine/index.js +33 -8
- package/lib/execution/engine/index.js.map +1 -1
- package/lib/execution/engine/interface.d.ts +1 -1
- package/lib/execution/engine/interface.d.ts.map +1 -1
- package/lib/execution/engine/jsonRpcTransport.d.ts +25 -0
- package/lib/execution/engine/jsonRpcTransport.d.ts.map +1 -0
- package/lib/execution/engine/jsonRpcTransport.js +158 -0
- package/lib/execution/engine/jsonRpcTransport.js.map +1 -0
- package/lib/execution/engine/restHttpClient.d.ts +71 -0
- package/lib/execution/engine/restHttpClient.d.ts.map +1 -0
- package/lib/execution/engine/restHttpClient.js +201 -0
- package/lib/execution/engine/restHttpClient.js.map +1 -0
- package/lib/execution/engine/restTransport.d.ts +46 -0
- package/lib/execution/engine/restTransport.d.ts.map +1 -0
- package/lib/execution/engine/restTransport.js +399 -0
- package/lib/execution/engine/restTransport.js.map +1 -0
- package/lib/execution/engine/sszTypes.d.ts +1261 -0
- package/lib/execution/engine/sszTypes.d.ts.map +1 -0
- package/lib/execution/engine/sszTypes.js +216 -0
- package/lib/execution/engine/sszTypes.js.map +1 -0
- package/lib/execution/engine/transport.d.ts +39 -0
- package/lib/execution/engine/transport.d.ts.map +1 -0
- package/lib/execution/engine/transport.js +2 -0
- package/lib/execution/engine/transport.js.map +1 -0
- package/lib/execution/engine/types.d.ts +2 -0
- package/lib/execution/engine/types.d.ts.map +1 -1
- package/lib/execution/engine/types.js +24 -57
- package/lib/execution/engine/types.js.map +1 -1
- package/lib/execution/engine/utils.d.ts.map +1 -1
- package/lib/execution/engine/utils.js +9 -2
- package/lib/execution/engine/utils.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +9 -0
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +18 -0
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/core/events.d.ts +5 -1
- package/lib/network/core/events.d.ts.map +1 -1
- package/lib/network/core/events.js +8 -0
- package/lib/network/core/events.js.map +1 -1
- package/lib/network/processor/gossipHandlers.js +1 -1
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/node/options.d.ts +3 -2
- package/lib/node/options.d.ts.map +1 -1
- package/lib/node/options.js +2 -2
- package/lib/node/options.js.map +1 -1
- package/lib/util/asyncIterableToEvents.d.ts +10 -2
- package/lib/util/asyncIterableToEvents.d.ts.map +1 -1
- package/lib/util/asyncIterableToEvents.js +29 -3
- package/lib/util/asyncIterableToEvents.js.map +1 -1
- package/lib/util/metadata.d.ts.map +1 -1
- package/lib/util/metadata.js +2 -1
- package/lib/util/metadata.js.map +1 -1
- package/package.json +16 -15
- package/src/api/impl/beacon/state/index.ts +13 -4
- package/src/api/impl/validator/index.ts +37 -0
- package/src/chain/beaconProposerCache.ts +19 -1
- package/src/chain/blocks/importBlock.ts +8 -4
- package/src/chain/blocks/verifyBlock.ts +7 -6
- package/src/chain/chain.ts +59 -15
- package/src/chain/interface.ts +2 -0
- package/src/chain/opPools/opPool.ts +1 -31
- package/src/chain/opPools/proposerPreferencesPool.ts +20 -1
- package/src/chain/opPools/utils.ts +31 -0
- package/src/chain/prepareNextSlot.ts +15 -3
- package/src/chain/produceBlock/produceBlockBody.ts +68 -34
- package/src/chain/shufflingCache.ts +79 -22
- package/src/db/beacon.ts +3 -0
- package/src/db/buckets.ts +1 -0
- package/src/db/interface.ts +2 -0
- package/src/db/repositories/index.ts +1 -0
- package/src/db/repositories/proposerPreferences.ts +23 -0
- package/src/execution/engine/blobVersionedHashes.ts +72 -0
- package/src/execution/engine/http.ts +304 -271
- package/src/execution/engine/index.ts +41 -7
- package/src/execution/engine/interface.ts +1 -1
- package/src/execution/engine/jsonRpcTransport.ts +247 -0
- package/src/execution/engine/restHttpClient.ts +269 -0
- package/src/execution/engine/restTransport.ts +544 -0
- package/src/execution/engine/sszTypes.ts +354 -0
- package/src/execution/engine/transport.ts +61 -0
- package/src/execution/engine/types.ts +34 -74
- package/src/execution/engine/utils.ts +12 -2
- package/src/metrics/metrics/lodestar.ts +19 -0
- package/src/network/core/events.ts +16 -1
- package/src/network/processor/gossipHandlers.ts +1 -1
- package/src/node/options.ts +10 -1
- package/src/util/asyncIterableToEvents.ts +44 -5
- 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
|
+
}
|