11import { 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" ;
33import { ExecutionPayload , ExecutionRequests , Root , RootHex , Wei } from "@lodestar/types" ;
44import { BlobAndProof } from "@lodestar/types/deneb" ;
5+ import { BlobAndProofV2 } from "@lodestar/types/fulu" ;
56import { strip0xPrefix } from "@lodestar/utils" ;
67import {
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 */
100102const 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
103112const notifyNewPayloadOpts : ReqOpts = { routeId : "notifyNewPayload" } ;
104113const forkchoiceUpdatedV1Opts : ReqOpts = { routeId : "forkchoiceUpdated" } ;
@@ -115,7 +124,7 @@ const getPayloadOpts: ReqOpts = {routeId: "getPayload"};
115124 */
116125export 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 [ ] > {
0 commit comments