Skip to content

Commit 40640c9

Browse files
hughydguenther
andauthored
feat: add engine_getBlobsV2 execution API (#7675)
**Motivation** add `engine_getBlobsV2` to the execution API in preparation for implementation of [distributed blob publishing](https://github.com/ethereum/consensus-specs/blob/dev/specs/fulu/p2p-interface.md#distributed-blob-publishing-using-blobs-retrieved-from-local-execution-layer-client) @dguenther and I wanted to get early feedback on the API change before moving forward with the rest of the implementation **Description** upcoming spec changes will add `engine_getBlobsV2` to the execution API to fetch blobs and cell proofs from the execution layer (ethereum/execution-apis#630) * add `engine_getBlobsV2` to execution API * add type definition for `BlobAndProofV2` **Not included** We'll follow up with additional PR(s) for these as we move forward with distributed blob publishing: * fetch blobs from the EL in two places: on first seen block input gossip and on unknown blocks during syncing * reconstruct blobs from cell proofs * publish data column sidecars on subscribed topics after Relates to #7638 --------- Co-authored-by: Derek Guenther <[email protected]>
1 parent 448dcd0 commit 40640c9

File tree

6 files changed

+106
-32
lines changed

6 files changed

+106
-32
lines changed

packages/beacon-node/src/execution/engine/http.ts

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {Logger} from "@lodestar/logger";
2-
import {ForkName, ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";
2+
import {ForkName, ForkPostFulu, ForkPreFulu, ForkSeq, SLOTS_PER_EPOCH, isForkPostFulu} from "@lodestar/params";
33
import {ExecutionPayload, ExecutionRequests, Root, RootHex, Wei} from "@lodestar/types";
44
import {BlobAndProof} from "@lodestar/types/deneb";
5+
import {BlobAndProofV2} from "@lodestar/types/fulu";
56
import {strip0xPrefix} from "@lodestar/utils";
67
import {
78
ErrorJsonRpcResponse,
@@ -35,6 +36,7 @@ import {
3536
ExecutionPayloadBody,
3637
assertReqSizeLimit,
3738
deserializeBlobAndProofs,
39+
deserializeBlobAndProofsV2,
3840
deserializeExecutionPayloadBody,
3941
parseExecutionPayload,
4042
serializeBeaconBlockRoot,
@@ -99,6 +101,13 @@ export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = {
99101
*/
100102
const QUEUE_MAX_LENGTH = EPOCHS_PER_BATCH * SLOTS_PER_EPOCH * 2;
101103

104+
/**
105+
* Maximum number of version hashes that can be sent in a getBlobs request
106+
* Clients must support at least 128 versionedHashes, so we avoid sending more
107+
* https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#specification-3
108+
*/
109+
const MAX_VERSIONED_HASHES = 128;
110+
102111
// Define static options once to prevent extra allocations
103112
const notifyNewPayloadOpts: ReqOpts = {routeId: "notifyNewPayload"};
104113
const forkchoiceUpdatedV1Opts: ReqOpts = {routeId: "forkchoiceUpdated"};
@@ -115,7 +124,7 @@ const getPayloadOpts: ReqOpts = {routeId: "getPayload"};
115124
*/
116125
export class ExecutionEngineHttp implements IExecutionEngine {
117126
private logger: Logger;
118-
private lastGetBlobsErrorTime = 0;
127+
private lastGetBlobsV1ErrorTime = 0;
119128

120129
// The default state is ONLINE, it will be updated to SYNCING once we receive the first payload
121130
// 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
@@ -466,55 +475,79 @@ export class ExecutionEngineHttp implements IExecutionEngine {
466475
return response.map(deserializeExecutionPayloadBody);
467476
}
468477

469-
async getBlobs(_fork: ForkName, versionedHashes: VersionedHashes): Promise<(BlobAndProof | null)[]> {
478+
async getBlobs(fork: ForkPostFulu, versionedHashes: VersionedHashes): Promise<BlobAndProofV2[] | null>;
479+
async getBlobs(fork: ForkPreFulu, versionedHashes: VersionedHashes): Promise<(BlobAndProof | null)[]>;
480+
async getBlobs(
481+
fork: ForkName,
482+
versionedHashes: VersionedHashes
483+
): Promise<BlobAndProofV2[] | (BlobAndProof | null)[] | null> {
484+
const method = isForkPostFulu(fork) ? "engine_getBlobsV2" : "engine_getBlobsV1";
485+
486+
// engine_getBlobsV2 is mandatory, but engine_getBlobsV1 is optional
487+
const timeNow = Date.now() / 1000;
470488
// retry only after a day may be
471489
const GETBLOBS_RETRY_TIMEOUT = 256 * 32 * 12;
472-
const timeNow = Date.now() / 1000;
473-
const timeSinceLastFail = timeNow - this.lastGetBlobsErrorTime;
474-
if (timeSinceLastFail < GETBLOBS_RETRY_TIMEOUT) {
475-
// do not try getblobs since it might not be available
476-
this.logger.debug(
477-
`disabled engine_getBlobsV1 api call since last failed < GETBLOBS_RETRY_TIMEOUT=${GETBLOBS_RETRY_TIMEOUT}`,
478-
timeSinceLastFail
479-
);
480-
throw Error(
481-
`engine_getBlobsV1 call recently failed timeSinceLastFail=${timeSinceLastFail} < GETBLOBS_RETRY_TIMEOUT=${GETBLOBS_RETRY_TIMEOUT}`
482-
);
490+
if (method === "engine_getBlobsV1") {
491+
const timeSinceLastFail = timeNow - this.lastGetBlobsV1ErrorTime;
492+
if (timeSinceLastFail < GETBLOBS_RETRY_TIMEOUT) {
493+
// do not try getblobs since it might not be available
494+
this.logger.debug(
495+
`disabled ${method} api call since last failed < GETBLOBS_RETRY_TIMEOUT=${GETBLOBS_RETRY_TIMEOUT}`,
496+
timeSinceLastFail
497+
);
498+
throw Error(
499+
`${method} call recently failed timeSinceLastFail=${timeSinceLastFail} < GETBLOBS_RETRY_TIMEOUT=${GETBLOBS_RETRY_TIMEOUT}`
500+
);
501+
}
483502
}
484503

485-
const method = "engine_getBlobsV1";
486-
assertReqSizeLimit(versionedHashes.length, 128);
504+
assertReqSizeLimit(versionedHashes.length, MAX_VERSIONED_HASHES);
487505
const versionedHashesHex = versionedHashes.map(bytesToData);
488-
let response = await this.rpc
506+
const response = await this.rpc
489507
.fetchWithRetries<EngineApiRpcReturnTypes[typeof method], EngineApiRpcParamTypes[typeof method]>({
490508
method,
491509
params: [versionedHashesHex],
492510
})
493511
.catch((e) => {
494-
if (e instanceof ErrorJsonRpcResponse && parseJsonRpcErrorCode(e.response.error.code) === "Method not found") {
495-
this.lastGetBlobsErrorTime = timeNow;
496-
this.logger.debug("disabling engine_getBlobsV1 api call since engine responded with method not availeble", {
512+
if (
513+
method === "engine_getBlobsV1" &&
514+
e instanceof ErrorJsonRpcResponse &&
515+
parseJsonRpcErrorCode(e.response.error.code) === "Method not found"
516+
) {
517+
if (method === "engine_getBlobsV1") {
518+
this.lastGetBlobsV1ErrorTime = timeNow;
519+
}
520+
this.logger.debug(`disabling ${method} api call since engine responded with method not available`, {
497521
retryTimeout: GETBLOBS_RETRY_TIMEOUT,
498522
});
499523
}
500524
throw e;
501525
});
502526

503-
// handle nethermind buggy response
504-
// see: https://discord.com/channels/595666850260713488/1293605631785304088/1298956894274060301
505-
if (
506-
(response as unknown as {blobsAndProofs: EngineApiRpcReturnTypes[typeof method]}).blobsAndProofs !== undefined
507-
) {
508-
response = (response as unknown as {blobsAndProofs: EngineApiRpcReturnTypes[typeof method]}).blobsAndProofs;
509-
}
527+
// engine_getBlobsV2 does not return partial responses. It returns an empty array if any blob is not found
528+
// TODO: Spec says to return null if any blob is not found, but reth and nethermind return empty arrays as of peerdas-devnet-6
529+
const invalidLength =
530+
method === "engine_getBlobsV2"
531+
? response && response.length !== 0 && response.length !== versionedHashes.length
532+
: !response || response.length !== versionedHashes.length;
510533

511-
if (response.length !== versionedHashes.length) {
512-
const error = `Invalid engine_getBlobsV1 response length=${response.length} versionedHashes=${versionedHashes.length}`;
534+
if (invalidLength) {
535+
const error = `Invalid ${method} response length=${response?.length ?? "null"} versionedHashes=${versionedHashes.length}`;
513536
this.logger.error(error);
514537
throw Error(error);
515538
}
516539

517-
return response.map(deserializeBlobAndProofs);
540+
// engine_getBlobsV2 returns a list of cell proofs per blob, whereas engine_getBlobsV1 returns one proof per blob
541+
switch (method) {
542+
case "engine_getBlobsV1":
543+
return (response as EngineApiRpcReturnTypes[typeof method]).map(deserializeBlobAndProofs);
544+
case "engine_getBlobsV2": {
545+
const castResponse = response as EngineApiRpcReturnTypes[typeof method];
546+
// TODO: Spec says to return null if any blob is not found, but reth and nethermind return empty arrays as of peerdas-devnet-6
547+
if (castResponse === null || castResponse.length === 0) return null;
548+
return castResponse.map(deserializeBlobAndProofsV2);
549+
}
550+
}
518551
}
519552

520553
private async getClientVersion(clientVersion: ClientVersion): Promise<ClientVersion[]> {

packages/beacon-node/src/execution/engine/interface.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import {CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, ForkName, WITHDRAWAL_REQUEST_TYPE} from "@lodestar/params";
1+
import {
2+
CONSOLIDATION_REQUEST_TYPE,
3+
DEPOSIT_REQUEST_TYPE,
4+
ForkName,
5+
ForkPostFulu,
6+
ForkPreFulu,
7+
WITHDRAWAL_REQUEST_TYPE,
8+
} from "@lodestar/params";
29
import {ExecutionPayload, ExecutionRequests, Root, RootHex, Wei, capella} from "@lodestar/types";
310
import {Blob, BlobAndProof, KZGCommitment, KZGProof} from "@lodestar/types/deneb";
11+
import {BlobAndProofV2} from "@lodestar/types/fulu";
412

513
import {DATA} from "../../eth1/provider/utils.js";
614
import {PayloadId, PayloadIdCache, WithdrawalV1} from "./payloadIdCache.js";
@@ -189,5 +197,6 @@ export interface IExecutionEngine {
189197

190198
getPayloadBodiesByRange(fork: ForkName, start: number, count: number): Promise<(ExecutionPayloadBody | null)[]>;
191199

192-
getBlobs(fork: ForkName, versionedHashes: VersionedHashes): Promise<(BlobAndProof | null)[]>;
200+
getBlobs(fork: ForkPostFulu, versionedHashes: VersionedHashes): Promise<BlobAndProofV2[] | null>;
201+
getBlobs(fork: ForkPreFulu, versionedHashes: VersionedHashes): Promise<(BlobAndProof | null)[]>;
193202
}

packages/beacon-node/src/execution/engine/mock.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend {
100100
engine_getPayloadBodiesByRangeV1: this.getPayloadBodiesByRange.bind(this),
101101
engine_getClientVersionV1: this.getClientVersionV1.bind(this),
102102
engine_getBlobsV1: this.getBlobs.bind(this),
103+
engine_getBlobsV2: this.getBlobsV2.bind(this),
103104
};
104105
}
105106

@@ -404,6 +405,12 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend {
404405
return versionedHashes.map((_vh) => null);
405406
}
406407

408+
private getBlobsV2(
409+
_versionedHashes: EngineApiRpcParamTypes["engine_getBlobsV2"][0]
410+
): EngineApiRpcReturnTypes["engine_getBlobsV2"] {
411+
return [];
412+
}
413+
407414
private timestampToFork(timestamp: number): ForkPostBellatrix {
408415
if (timestamp > (this.opts.electraForkTimestamp ?? Infinity)) return ForkName.electra;
409416
if (timestamp > (this.opts.denebForkTimestamp ?? Infinity)) return ForkName.deneb;

packages/beacon-node/src/execution/engine/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "@lodestar/params";
1111
import {ExecutionPayload, ExecutionRequests, Root, Wei, bellatrix, capella, deneb, electra, ssz} from "@lodestar/types";
1212
import {BlobAndProof} from "@lodestar/types/deneb";
13+
import {BlobAndProofV2} from "@lodestar/types/fulu";
1314

1415
import {
1516
DATA,
@@ -80,6 +81,7 @@ export type EngineApiRpcParamTypes = {
8081
engine_getClientVersionV1: [ClientVersionRpc];
8182

8283
engine_getBlobsV1: [DATA[]];
84+
engine_getBlobsV2: [DATA[]];
8385
};
8486

8587
export type PayloadStatus = {
@@ -124,6 +126,7 @@ export type EngineApiRpcReturnTypes = {
124126
engine_getClientVersionV1: ClientVersionRpc[];
125127

126128
engine_getBlobsV1: (BlobAndProofRpc | null)[];
129+
engine_getBlobsV2: BlobAndProofV2Rpc[] | null;
127130
};
128131

129132
type ExecutionPayloadRpcWithValue = {
@@ -191,6 +194,11 @@ export type BlobAndProofRpc = {
191194
proof: DATA;
192195
};
193196

197+
export type BlobAndProofV2Rpc = {
198+
blob: DATA;
199+
proofs: DATA[];
200+
};
201+
194202
export type VersionedHashesRpc = DATA[];
195203

196204
export type PayloadAttributesRpc = {
@@ -563,6 +571,13 @@ export function deserializeBlobAndProofs(data: BlobAndProofRpc | null): BlobAndP
563571
: null;
564572
}
565573

574+
export function deserializeBlobAndProofsV2(data: BlobAndProofV2Rpc): BlobAndProofV2 {
575+
return {
576+
blob: dataToBytes(data.blob, BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB),
577+
proofs: data.proofs.map((proof) => dataToBytes(proof, 48)),
578+
};
579+
}
580+
566581
export function assertReqSizeLimit(blockHashesReqCount: number, count: number): void {
567582
if (blockHashesReqCount > count) {
568583
throw new Error(`Requested blocks must not be > ${count}`);

packages/types/src/fulu/sszTypes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {ssz as primitiveSsz} from "../primitive/index.js";
1616

1717
const {BLSSignature, Root, ColumnIndex, RowIndex, Bytes32, Slot, UintNum64} = primitiveSsz;
1818

19+
export const KZGProof = denebSsz.KZGProof;
20+
export const Blob = denebSsz.Blob;
21+
1922
export const Metadata = new ContainerType(
2023
{
2124
...altariSsz.Metadata.fields,

packages/types/src/fulu/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {ValueOf} from "@chainsafe/ssz";
22
import * as ssz from "./sszTypes.js";
33

4+
export type KZGProof = ValueOf<typeof ssz.KZGProof>;
5+
export type Blob = ValueOf<typeof ssz.Blob>;
6+
47
export type Metadata = ValueOf<typeof ssz.Metadata>;
58

69
export type Cell = ValueOf<typeof ssz.Cell>;
@@ -44,3 +47,7 @@ export type LightClientStore = ValueOf<typeof ssz.LightClientStore>;
4447
export type BlockContents = ValueOf<typeof ssz.BlockContents>;
4548
export type SignedBlockContents = ValueOf<typeof ssz.SignedBlockContents>;
4649
export type Contents = Omit<BlockContents, "block">;
50+
export type BlobAndProofV2 = {
51+
blob: Blob;
52+
proofs: KZGProof[];
53+
};

0 commit comments

Comments
 (0)