mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-01-09 15:48:08 -05:00
**Motivation** All networks are post-electra now and transition period is completed, which means due to [EIP-6110](https://eips.ethereum.org/EIPS/eip-6110) we no longer need to process deposits via eth1 bridge as those are now processed by the execution layer. This code is effectively tech debt, no longer exercised and just gets in the way when doing refactors. **Description** Removes all code related to eth1 bridge mechanism to include new deposits - removed all eth1 related code, we can no longer produce blocks with deposits pre-electra (syncing blocks still works) - building a genesis state from eth1 is no longer supported (only for testing) - removed various db repositories related to deposits/eth1 data - removed various `lodestar_eth1_*` metrics and dashboard panels - deprecated all `--eth1.*` flags (but kept for backward compatibility) - moved shared utility functions from eth1 to execution engine module Closes https://github.com/ChainSafe/lodestar/issues/7682 Closes https://github.com/ChainSafe/lodestar/issues/8654
636 lines
24 KiB
TypeScript
636 lines
24 KiB
TypeScript
import {Logger} from "@lodestar/logger";
|
|
import {ForkName, ForkPostFulu, ForkPreFulu, ForkSeq, SLOTS_PER_EPOCH, isForkPostFulu} from "@lodestar/params";
|
|
import {BlobsBundle, ExecutionPayload, ExecutionRequests, Root, RootHex, Wei} from "@lodestar/types";
|
|
import {BlobAndProof} from "@lodestar/types/deneb";
|
|
import {BlobAndProofV2} from "@lodestar/types/fulu";
|
|
import {strip0xPrefix} from "@lodestar/utils";
|
|
import {Metrics} from "../../metrics/index.js";
|
|
import {EPOCHS_PER_BATCH} from "../../sync/constants.js";
|
|
import {getLodestarClientVersion} from "../../util/metadata.js";
|
|
import {JobItemQueue} from "../../util/queue/index.js";
|
|
import {
|
|
ClientCode,
|
|
ClientVersion,
|
|
ExecutePayloadResponse,
|
|
ExecutionEngineState,
|
|
ExecutionPayloadStatus,
|
|
IExecutionEngine,
|
|
PayloadAttributes,
|
|
PayloadId,
|
|
VersionedHashes,
|
|
} from "./interface.js";
|
|
import {
|
|
ErrorJsonRpcResponse,
|
|
HttpRpcError,
|
|
IJsonRpcHttpClient,
|
|
JsonRpcHttpClientEvent,
|
|
ReqOpts,
|
|
} from "./jsonRpcHttpClient.js";
|
|
import {PayloadIdCache} from "./payloadIdCache.js";
|
|
import {
|
|
BLOB_AND_PROOF_V2_RPC_BYTES,
|
|
EngineApiRpcParamTypes,
|
|
EngineApiRpcReturnTypes,
|
|
ExecutionPayloadBody,
|
|
assertReqSizeLimit,
|
|
deserializeBlobAndProofs,
|
|
deserializeBlobAndProofsV2,
|
|
deserializeBlobAndProofsV2IntoBytes,
|
|
deserializeExecutionPayloadBody,
|
|
parseExecutionPayload,
|
|
serializeBeaconBlockRoot,
|
|
serializeExecutionPayload,
|
|
serializeExecutionRequests,
|
|
serializePayloadAttributes,
|
|
serializeVersionedHashes,
|
|
} from "./types.js";
|
|
import {bytesToData, getExecutionEngineState, numToQuantity} from "./utils.js";
|
|
|
|
export type ExecutionEngineModules = {
|
|
signal: AbortSignal;
|
|
metrics?: Metrics | null;
|
|
logger: Logger;
|
|
};
|
|
|
|
export type ExecutionEngineHttpOpts = {
|
|
urls: string[];
|
|
retries: number;
|
|
retryDelay: number;
|
|
timeout?: number;
|
|
/**
|
|
* 256 bit jwt secret in hex format without the leading 0x. If provided, the execution engine
|
|
* rpc requests will be bundled by an authorization header having a fresh jwt token on each
|
|
* request, as the EL auth specs mandate the fresh of the token (iat) to be checked within
|
|
* +-5 seconds interval.
|
|
*/
|
|
jwtSecretHex?: string;
|
|
/**
|
|
* An identifier string passed as CLI arg that will be set in `id` field of jwt claims
|
|
*/
|
|
jwtId?: string;
|
|
/**
|
|
* A version string that will be set in `clv` field of jwt claims
|
|
*/
|
|
jwtVersion?: string;
|
|
/**
|
|
* Lodestar version to be used for `ClientVersion`
|
|
*/
|
|
version?: string;
|
|
/**
|
|
* Lodestar commit to be used for `ClientVersion`
|
|
*/
|
|
commit?: string;
|
|
};
|
|
|
|
export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = {
|
|
/**
|
|
* By default ELs host engine api on an auth protected 8551 port, would need a jwt secret to be
|
|
* specified to bundle jwt tokens if that is the case. In case one has access to an open
|
|
* port/url, one can override this and skip providing a jwt secret.
|
|
*/
|
|
urls: ["http://localhost:8551"],
|
|
retries: 2,
|
|
retryDelay: 2000,
|
|
timeout: 12000,
|
|
};
|
|
|
|
/**
|
|
* Size for the serializing queue for fcUs and new payloads, the max length could be equal to
|
|
* EPOCHS_PER_BATCH * 2 in case new payloads are also not awaited serially
|
|
*/
|
|
const QUEUE_MAX_LENGTH = EPOCHS_PER_BATCH * SLOTS_PER_EPOCH * 2;
|
|
|
|
/**
|
|
* Maximum number of version hashes that can be sent in a getBlobs request
|
|
* Clients must support at least 128 versionedHashes, so we avoid sending more
|
|
* https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#specification-3
|
|
*/
|
|
const MAX_VERSIONED_HASHES = 128;
|
|
|
|
// Define static options once to prevent extra allocations
|
|
const notifyNewPayloadOpts: ReqOpts = {routeId: "notifyNewPayload"};
|
|
const forkchoiceUpdatedV1Opts: ReqOpts = {routeId: "forkchoiceUpdated"};
|
|
const getPayloadOpts: ReqOpts = {routeId: "getPayload"};
|
|
const getPayloadBodiesByHashOpts: ReqOpts = {routeId: "getPayloadBodiesByHash"};
|
|
const getPayloadBodiesByRangeOpts: ReqOpts = {routeId: "getPayloadBodiesByRange"};
|
|
const getBlobsV1Opts: ReqOpts = {routeId: "getBlobsV1"};
|
|
const getBlobsV2Opts: ReqOpts = {routeId: "getBlobsV2"};
|
|
const getClientVersionOpts: ReqOpts = {routeId: "getClientVersion"};
|
|
|
|
/**
|
|
* based on Ethereum JSON-RPC API and inherits the following properties of this standard:
|
|
* - Supported communication protocols (HTTP and WebSocket)
|
|
* - Message format and encoding notation
|
|
* - Error codes improvement proposal
|
|
*
|
|
* 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.
|
|
* https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.1/src/engine/interop/specification.md
|
|
*/
|
|
export class ExecutionEngineHttp implements IExecutionEngine {
|
|
private logger: Logger;
|
|
|
|
// The default state is ONLINE, it will be updated to SYNCING once we receive the first payload
|
|
// This assumption is better than the OFFLINE state, since we can't be sure if the EL is offline and being offline may trigger some notifications
|
|
// It's safer to to avoid false positives and assume that the EL is syncing until we receive the first payload
|
|
state: ExecutionEngineState = ExecutionEngineState.ONLINE;
|
|
|
|
/** Cached EL client version from the latest getClientVersion call */
|
|
clientVersion?: ClientVersion | null;
|
|
|
|
readonly payloadIdCache = new PayloadIdCache();
|
|
/**
|
|
* A queue to serialize the fcUs and newPayloads calls:
|
|
*
|
|
* While syncing, lodestar has a batch processing module which calls new payloads in batch followed by fcUs.
|
|
* Even though we await for responses to new payloads serially, we just trigger fcUs consecutively. This
|
|
* may lead to the EL receiving the fcUs out of the order and may break the EL's backfill/beacon sync. Since
|
|
* the order of new payloads and fcUs is pretty important to EL, this queue will serialize the calls in the
|
|
* order with which we make them.
|
|
*/
|
|
private readonly rpcFetchQueue: JobItemQueue<[EngineRequest], EngineResponse>;
|
|
|
|
private jobQueueProcessor = async ({method, params, methodOpts}: EngineRequest): Promise<EngineResponse> => {
|
|
return this.rpc.fetchWithRetries<EngineApiRpcReturnTypes[typeof method], EngineApiRpcParamTypes[typeof method]>(
|
|
{method, params},
|
|
methodOpts
|
|
);
|
|
};
|
|
|
|
constructor(
|
|
private readonly rpc: IJsonRpcHttpClient,
|
|
{metrics, signal, logger}: ExecutionEngineModules,
|
|
private readonly opts?: ExecutionEngineHttpOpts
|
|
) {
|
|
this.rpcFetchQueue = new JobItemQueue<[EngineRequest], EngineResponse>(
|
|
this.jobQueueProcessor,
|
|
{maxLength: QUEUE_MAX_LENGTH, maxConcurrency: 1, noYieldIfOneItem: true, signal},
|
|
metrics?.engineHttpProcessorQueue
|
|
);
|
|
this.logger = logger;
|
|
|
|
this.rpc.emitter.on(JsonRpcHttpClientEvent.ERROR, ({error}) => {
|
|
this.updateEngineState(getExecutionEngineState({payloadError: error, oldState: this.state}));
|
|
});
|
|
|
|
this.rpc.emitter.on(JsonRpcHttpClientEvent.RESPONSE, () => {
|
|
if (this.clientVersion === undefined) {
|
|
this.clientVersion = null;
|
|
// This statement should only be called first time receiving response after startup
|
|
this.getClientVersion(getLodestarClientVersion(this.opts)).catch((e) => {
|
|
this.logger.debug("Unable to get execution client version", {}, e);
|
|
});
|
|
}
|
|
this.updateEngineState(getExecutionEngineState({targetState: ExecutionEngineState.ONLINE, oldState: this.state}));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* `engine_newPayloadV1`
|
|
* From: https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.6/src/engine/specification.md#engine_newpayloadv1
|
|
*
|
|
* Client software MUST respond to this method call in the following way:
|
|
*
|
|
* 1. {status: INVALID_BLOCK_HASH, latestValidHash: null, validationError:
|
|
* errorMessage | null} if the blockHash validation has failed
|
|
*
|
|
* 2. {status: SYNCING, latestValidHash: null, validationError: null} if the payload
|
|
* extends the canonical chain and requisite data for its validation is missing
|
|
* with the payload status obtained from the Payload validation process if the payload
|
|
* has been fully validated while processing the call
|
|
*
|
|
* 3. {status: ACCEPTED, latestValidHash: null, validationError: null} if the
|
|
* following conditions are met:
|
|
* i) the blockHash of the payload is valid
|
|
* ii) the payload doesn't extend the canonical chain
|
|
* iii) the payload hasn't been fully validated.
|
|
*
|
|
* If any of the above fails due to errors unrelated to the normal processing flow of the method, client software MUST respond with an error object.
|
|
*/
|
|
async notifyNewPayload(
|
|
fork: ForkName,
|
|
executionPayload: ExecutionPayload,
|
|
versionedHashes?: VersionedHashes,
|
|
parentBlockRoot?: Root,
|
|
executionRequests?: ExecutionRequests
|
|
): Promise<ExecutePayloadResponse> {
|
|
const method =
|
|
ForkSeq[fork] >= ForkSeq.electra
|
|
? "engine_newPayloadV4"
|
|
: ForkSeq[fork] >= ForkSeq.deneb
|
|
? "engine_newPayloadV3"
|
|
: ForkSeq[fork] >= ForkSeq.capella
|
|
? "engine_newPayloadV2"
|
|
: "engine_newPayloadV1";
|
|
|
|
const serializedExecutionPayload = serializeExecutionPayload(fork, executionPayload);
|
|
|
|
let engineRequest: EngineRequest;
|
|
if (ForkSeq[fork] >= ForkSeq.deneb) {
|
|
if (versionedHashes === undefined) {
|
|
throw Error(`versionedHashes required in notifyNewPayload for fork=${fork}`);
|
|
}
|
|
if (parentBlockRoot === undefined) {
|
|
throw Error(`parentBlockRoot required in notifyNewPayload for fork=${fork}`);
|
|
}
|
|
|
|
const serializedVersionedHashes = serializeVersionedHashes(versionedHashes);
|
|
const parentBeaconBlockRoot = serializeBeaconBlockRoot(parentBlockRoot);
|
|
|
|
if (ForkSeq[fork] >= ForkSeq.electra) {
|
|
if (executionRequests === undefined) {
|
|
throw Error(`executionRequests required in notifyNewPayload for fork=${fork}`);
|
|
}
|
|
const serializedExecutionRequests = serializeExecutionRequests(executionRequests);
|
|
engineRequest = {
|
|
method: "engine_newPayloadV4",
|
|
params: [
|
|
serializedExecutionPayload,
|
|
serializedVersionedHashes,
|
|
parentBeaconBlockRoot,
|
|
serializedExecutionRequests,
|
|
],
|
|
methodOpts: notifyNewPayloadOpts,
|
|
};
|
|
} else {
|
|
engineRequest = {
|
|
method: "engine_newPayloadV3",
|
|
params: [serializedExecutionPayload, serializedVersionedHashes, parentBeaconBlockRoot],
|
|
methodOpts: notifyNewPayloadOpts,
|
|
};
|
|
}
|
|
} else {
|
|
const method = ForkSeq[fork] >= ForkSeq.capella ? "engine_newPayloadV2" : "engine_newPayloadV1";
|
|
engineRequest = {
|
|
method,
|
|
params: [serializedExecutionPayload],
|
|
methodOpts: notifyNewPayloadOpts,
|
|
};
|
|
}
|
|
|
|
const {status, latestValidHash, validationError} = await (
|
|
this.rpcFetchQueue.push(engineRequest) as Promise<EngineApiRpcReturnTypes[typeof method]>
|
|
).catch((e: Error) => {
|
|
if (e instanceof HttpRpcError || e instanceof ErrorJsonRpcResponse) {
|
|
return {status: ExecutionPayloadStatus.ELERROR, latestValidHash: null, validationError: e.message};
|
|
}
|
|
return {status: ExecutionPayloadStatus.UNAVAILABLE, latestValidHash: null, validationError: e.message};
|
|
});
|
|
|
|
this.updateEngineState(getExecutionEngineState({payloadStatus: status, oldState: this.state}));
|
|
|
|
switch (status) {
|
|
case ExecutionPayloadStatus.VALID:
|
|
return {status, latestValidHash: latestValidHash ?? "0x0", validationError: null};
|
|
|
|
case ExecutionPayloadStatus.INVALID:
|
|
// As per latest specs if latestValidHash can be null and it would mean only
|
|
// invalidate this block
|
|
return {status, latestValidHash, validationError};
|
|
|
|
case ExecutionPayloadStatus.SYNCING:
|
|
case ExecutionPayloadStatus.ACCEPTED:
|
|
return {status, latestValidHash: null, validationError: null};
|
|
|
|
case ExecutionPayloadStatus.INVALID_BLOCK_HASH:
|
|
return {status, latestValidHash: null, validationError: validationError ?? "Malformed block"};
|
|
|
|
case ExecutionPayloadStatus.UNAVAILABLE:
|
|
case ExecutionPayloadStatus.ELERROR:
|
|
return {
|
|
status,
|
|
latestValidHash: null,
|
|
validationError: validationError ?? "Unknown ELERROR",
|
|
};
|
|
|
|
default:
|
|
return {
|
|
status: ExecutionPayloadStatus.ELERROR,
|
|
latestValidHash: null,
|
|
validationError: `Invalid EL status on executePayload: ${status}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `engine_forkchoiceUpdatedV1`
|
|
* From: https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.6/src/engine/specification.md#engine_forkchoiceupdatedv1
|
|
*
|
|
* Client software MUST respond to this method call in the following way:
|
|
*
|
|
* 1. {payloadStatus: {status: SYNCING, latestValidHash: null, validationError: null}
|
|
* , payloadId: null}
|
|
* if forkchoiceState.headBlockHash references an unknown payload or a payload that
|
|
* can't be validated because requisite data for the validation is missing
|
|
*
|
|
* 2. {payloadStatus: {status: INVALID, latestValidHash: null, validationError:
|
|
* errorMessage | null}, payloadId: null}
|
|
* obtained from the Payload validation process if the payload is deemed INVALID
|
|
*
|
|
* 3. {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash,
|
|
* validationError: null}, payloadId: null}
|
|
* if the payload is deemed VALID and a build process hasn't been started
|
|
*
|
|
* 4. {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash,
|
|
* validationError: null}, payloadId: buildProcessId}
|
|
* if the payload is deemed VALID and the build process has begun.
|
|
*
|
|
* If any of the above fails due to errors unrelated to the normal processing flow of the method, client software MUST respond with an error object.
|
|
*/
|
|
async notifyForkchoiceUpdate(
|
|
fork: ForkName,
|
|
headBlockHash: RootHex,
|
|
safeBlockHash: RootHex,
|
|
finalizedBlockHash: RootHex,
|
|
payloadAttributes?: PayloadAttributes
|
|
): Promise<PayloadId | null> {
|
|
// Once on capella, should this need to be permanently switched to v2 when payload attrs
|
|
// not provided
|
|
const method =
|
|
ForkSeq[fork] >= ForkSeq.deneb
|
|
? "engine_forkchoiceUpdatedV3"
|
|
: ForkSeq[fork] >= ForkSeq.capella
|
|
? "engine_forkchoiceUpdatedV2"
|
|
: "engine_forkchoiceUpdatedV1";
|
|
const payloadAttributesRpc = payloadAttributes ? serializePayloadAttributes(payloadAttributes) : undefined;
|
|
// If we are just fcUing and not asking execution for payload, retry is not required
|
|
// and we can move on, as the next fcU will be issued soon on the new slot
|
|
const fcUReqOpts =
|
|
payloadAttributes !== undefined ? forkchoiceUpdatedV1Opts : {...forkchoiceUpdatedV1Opts, retries: 0};
|
|
|
|
const request = this.rpcFetchQueue.push({
|
|
method,
|
|
params: [{headBlockHash, safeBlockHash, finalizedBlockHash}, payloadAttributesRpc],
|
|
methodOpts: fcUReqOpts,
|
|
}) as Promise<EngineApiRpcReturnTypes[typeof method]>;
|
|
|
|
const {
|
|
payloadStatus: {status, latestValidHash: _latestValidHash, validationError},
|
|
payloadId,
|
|
} = await request;
|
|
|
|
this.updateEngineState(getExecutionEngineState({payloadStatus: status, oldState: this.state}));
|
|
|
|
switch (status) {
|
|
case ExecutionPayloadStatus.VALID:
|
|
// if payloadAttributes are provided, a valid payloadId is expected
|
|
if (payloadAttributesRpc) {
|
|
if (!payloadId || payloadId === "0x") {
|
|
throw Error(`Received invalid payloadId=${payloadId}`);
|
|
}
|
|
|
|
this.payloadIdCache.add({headBlockHash, finalizedBlockHash, ...payloadAttributesRpc}, payloadId);
|
|
void this.prunePayloadIdCache();
|
|
}
|
|
return payloadId !== "0x" ? payloadId : null;
|
|
|
|
case ExecutionPayloadStatus.SYNCING:
|
|
// Throw error on syncing if requested to produce a block, else silently ignore
|
|
if (payloadAttributes) {
|
|
throw Error("Execution Layer Syncing");
|
|
}
|
|
return null;
|
|
|
|
case ExecutionPayloadStatus.INVALID:
|
|
throw Error(
|
|
`Invalid ${payloadAttributes ? "prepare payload" : "forkchoice request"}, validationError=${
|
|
validationError ?? ""
|
|
}`
|
|
);
|
|
|
|
default:
|
|
throw Error(`Unknown status ${status}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `engine_getPayloadV1`
|
|
*
|
|
* 1. Given the payloadId client software MUST respond with the most recent version of the payload that is available in the corresponding building process at the time of receiving the call.
|
|
* 2. The call MUST be responded with 5: Unavailable payload error if the building process identified by the payloadId doesn't exist.
|
|
* 3. Client software MAY stop the corresponding building process after serving this call.
|
|
*/
|
|
async getPayload(
|
|
fork: ForkName,
|
|
payloadId: PayloadId
|
|
): Promise<{
|
|
executionPayload: ExecutionPayload;
|
|
executionPayloadValue: Wei;
|
|
blobsBundle?: BlobsBundle;
|
|
executionRequests?: ExecutionRequests;
|
|
shouldOverrideBuilder?: boolean;
|
|
}> {
|
|
let method: keyof EngineApiRpcReturnTypes;
|
|
switch (fork) {
|
|
case ForkName.phase0:
|
|
case ForkName.altair:
|
|
case ForkName.bellatrix:
|
|
method = "engine_getPayloadV1";
|
|
break;
|
|
case ForkName.capella:
|
|
method = "engine_getPayloadV2";
|
|
break;
|
|
case ForkName.deneb:
|
|
method = "engine_getPayloadV3";
|
|
break;
|
|
case ForkName.electra:
|
|
method = "engine_getPayloadV4";
|
|
break;
|
|
default:
|
|
method = "engine_getPayloadV5";
|
|
break;
|
|
}
|
|
const payloadResponse = await this.rpc.fetchWithRetries<
|
|
EngineApiRpcReturnTypes[typeof method],
|
|
EngineApiRpcParamTypes[typeof method]
|
|
>(
|
|
{
|
|
method,
|
|
params: [payloadId],
|
|
},
|
|
getPayloadOpts
|
|
);
|
|
return parseExecutionPayload(fork, payloadResponse);
|
|
}
|
|
|
|
async prunePayloadIdCache(): Promise<void> {
|
|
this.payloadIdCache.prune();
|
|
}
|
|
|
|
async getPayloadBodiesByHash(_fork: ForkName, blockHashes: RootHex[]): Promise<(ExecutionPayloadBody | null)[]> {
|
|
const method = "engine_getPayloadBodiesByHashV1";
|
|
assertReqSizeLimit(blockHashes.length, 32);
|
|
const response = await this.rpc.fetchWithRetries<
|
|
EngineApiRpcReturnTypes[typeof method],
|
|
EngineApiRpcParamTypes[typeof method]
|
|
>({method, params: [blockHashes]}, getPayloadBodiesByHashOpts);
|
|
return response.map(deserializeExecutionPayloadBody);
|
|
}
|
|
|
|
async getPayloadBodiesByRange(
|
|
_fork: ForkName,
|
|
startBlockNumber: number,
|
|
blockCount: number
|
|
): Promise<(ExecutionPayloadBody | null)[]> {
|
|
const method = "engine_getPayloadBodiesByRangeV1";
|
|
assertReqSizeLimit(blockCount, 32);
|
|
const start = numToQuantity(startBlockNumber);
|
|
const count = numToQuantity(blockCount);
|
|
const response = await this.rpc.fetchWithRetries<
|
|
EngineApiRpcReturnTypes[typeof method],
|
|
EngineApiRpcParamTypes[typeof method]
|
|
>({method, params: [start, count]}, getPayloadBodiesByRangeOpts);
|
|
return response.map(deserializeExecutionPayloadBody);
|
|
}
|
|
|
|
async getBlobs(
|
|
fork: ForkPostFulu,
|
|
versionedHashes: VersionedHashes,
|
|
buffers?: Uint8Array[]
|
|
): Promise<BlobAndProofV2[] | null>;
|
|
async getBlobs(
|
|
fork: ForkPreFulu,
|
|
versionedHashes: VersionedHashes,
|
|
buffers?: Uint8Array[]
|
|
): Promise<(BlobAndProof | null)[]>;
|
|
async getBlobs(
|
|
fork: ForkName,
|
|
versionedHashes: VersionedHashes
|
|
): Promise<BlobAndProofV2[] | (BlobAndProof | null)[] | null> {
|
|
assertReqSizeLimit(versionedHashes.length, MAX_VERSIONED_HASHES);
|
|
const versionedHashesHex = versionedHashes.map(bytesToData);
|
|
if (isForkPostFulu(fork)) {
|
|
return await this.getBlobsV2(versionedHashesHex);
|
|
}
|
|
return await this.getBlobsV1(versionedHashesHex);
|
|
}
|
|
|
|
private async getBlobsV1(versionedHashesHex: string[]) {
|
|
const response = await this.rpc.fetchWithRetries<
|
|
EngineApiRpcReturnTypes["engine_getBlobsV1"],
|
|
EngineApiRpcParamTypes["engine_getBlobsV1"]
|
|
>(
|
|
{
|
|
method: "engine_getBlobsV1",
|
|
params: [versionedHashesHex],
|
|
},
|
|
getBlobsV1Opts
|
|
);
|
|
|
|
const invalidLength = response.length !== versionedHashesHex.length;
|
|
|
|
if (invalidLength) {
|
|
const error = `Invalid engine_getBlobsV1 response length=${response.length} versionedHashes=${versionedHashesHex.length}`;
|
|
this.logger.error(error);
|
|
throw Error(error);
|
|
}
|
|
|
|
return response.map(deserializeBlobAndProofs);
|
|
}
|
|
|
|
private async getBlobsV2(versionedHashesHex: string[], buffers?: Uint8Array[]) {
|
|
if (buffers) {
|
|
if (buffers.length !== versionedHashesHex.length) {
|
|
throw Error(`Invalid buffers length=${buffers.length} versionedHashes=${versionedHashesHex.length}`);
|
|
}
|
|
|
|
for (const [i, buffer] of buffers.entries()) {
|
|
if (buffer.length !== BLOB_AND_PROOF_V2_RPC_BYTES) {
|
|
throw Error(`Invalid buffer[${i}] length=${buffer.length} expected=${BLOB_AND_PROOF_V2_RPC_BYTES}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const response = await this.rpc.fetchWithRetries<
|
|
EngineApiRpcReturnTypes["engine_getBlobsV2"],
|
|
EngineApiRpcParamTypes["engine_getBlobsV2"]
|
|
>(
|
|
{
|
|
method: "engine_getBlobsV2",
|
|
params: [versionedHashesHex],
|
|
},
|
|
getBlobsV2Opts
|
|
);
|
|
|
|
// engine_getBlobsV2 does not return partial responses. It returns null if any blob is not found
|
|
const invalidLength = !!response && response.length !== versionedHashesHex.length;
|
|
|
|
if (invalidLength) {
|
|
const error = `Invalid engine_getBlobsV2 response length=${response?.length ?? "null"} versionedHashes=${versionedHashesHex.length}`;
|
|
this.logger.error(error);
|
|
throw Error(error);
|
|
}
|
|
|
|
if (response == null) {
|
|
return null;
|
|
}
|
|
|
|
if (buffers) {
|
|
// getBlobsV2() is designed to called once per slot so we expect to have buffers
|
|
return response.map((data, i) => deserializeBlobAndProofsV2IntoBytes(data, buffers[i]));
|
|
}
|
|
|
|
return response.map(deserializeBlobAndProofsV2);
|
|
}
|
|
|
|
private async getClientVersion(clientVersion: ClientVersion): Promise<ClientVersion[]> {
|
|
const method = "engine_getClientVersionV1";
|
|
|
|
const response = await this.rpc.fetchWithRetries<
|
|
EngineApiRpcReturnTypes[typeof method],
|
|
EngineApiRpcParamTypes[typeof method]
|
|
>({method, params: [{...clientVersion, commit: `0x${clientVersion.commit}`}]}, getClientVersionOpts);
|
|
|
|
const clientVersions = response.map((cv) => {
|
|
const code = cv.code in ClientCode ? ClientCode[cv.code as keyof typeof ClientCode] : ClientCode.XX;
|
|
return {code, name: cv.name, version: cv.version, commit: strip0xPrefix(cv.commit)};
|
|
});
|
|
|
|
if (clientVersions.length === 0) {
|
|
throw Error("Received empty client versions array");
|
|
}
|
|
|
|
this.clientVersion = clientVersions[0];
|
|
this.logger.debug("Execution client version updated", this.clientVersion);
|
|
|
|
return clientVersions;
|
|
}
|
|
|
|
private updateEngineState(newState: ExecutionEngineState): void {
|
|
const oldState = this.state;
|
|
|
|
if (oldState === newState) return;
|
|
|
|
switch (newState) {
|
|
case ExecutionEngineState.ONLINE:
|
|
this.logger.info("Execution client became online", {oldState, newState});
|
|
this.getClientVersion(getLodestarClientVersion(this.opts)).catch((e) => {
|
|
this.logger.debug("Unable to get execution client version", {}, e);
|
|
this.clientVersion = null;
|
|
});
|
|
break;
|
|
case ExecutionEngineState.OFFLINE:
|
|
this.logger.error("Execution client went offline", {oldState, newState});
|
|
break;
|
|
case ExecutionEngineState.SYNCED:
|
|
this.logger.info("Execution client is synced", {oldState, newState});
|
|
break;
|
|
case ExecutionEngineState.SYNCING:
|
|
this.logger.warn("Execution client is syncing", {oldState, newState});
|
|
break;
|
|
case ExecutionEngineState.AUTH_FAILED:
|
|
this.logger.error("Execution client authentication failed", {oldState, newState});
|
|
break;
|
|
}
|
|
|
|
this.state = newState;
|
|
}
|
|
}
|
|
|
|
type EngineRequestKey = keyof EngineApiRpcParamTypes;
|
|
type EngineRequestByKey = {
|
|
[K in EngineRequestKey]: {method: K; params: EngineApiRpcParamTypes[K]; methodOpts: ReqOpts};
|
|
};
|
|
type EngineRequest = EngineRequestByKey[EngineRequestKey];
|
|
type EngineResponseByKey = {[K in EngineRequestKey]: EngineApiRpcReturnTypes[K]};
|
|
type EngineResponse = EngineResponseByKey[EngineRequestKey];
|