diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 1c399d71ff72..9e18eb371f99 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -1,5 +1,5 @@ import {ContainerType, fromHexString, toHexString, Type} from "@chainsafe/ssz"; -import {ForkName, isForkBlobs, isForkExecution} from "@lodestar/params"; +import {ForkName, ForkBlobs, isForkBlobs, isForkExecution} from "@lodestar/params"; import { allForks, altair, @@ -27,12 +27,13 @@ import { ArrayOf, Schema, WithVersion, - WithBlockValue, + WithExecutionPayloadValue, reqOnlyBody, ReqSerializers, jsonType, ContainerDataExecutionOptimistic, ContainerData, + TypeJson, } from "../../utils/index.js"; import {fromU64Str, fromGraffitiHex, toU64Str, U64Str, toGraffitiHex} from "../../utils/serdes.js"; import { @@ -43,6 +44,19 @@ import { } from "../../utils/routes.js"; import {ExecutionOptimistic} from "./beacon/block.js"; +type ForkPreBlobs = ForkName.phase0 | ForkName.altair | ForkName.bellatrix | ForkName.capella; + +type ProduceBlockOrContentsRes = + | {data: allForks.BeaconBlock; version: ForkPreBlobs} + | {data: BlockContents; version: ForkBlobs}; +type ProduceBlindedBlockOrContentsRes = + | {data: allForks.BlindedBeaconBlock; version: ForkPreBlobs} + | {data: BlindedBlockContents; version: ForkBlobs}; + +type ProduceBlockV3Res = + | (ProduceBlockOrContentsRes & {executionPayloadBlinded: false}) + | (ProduceBlindedBlockOrContentsRes & {executionPayloadBlinded: true}); + // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes export type BeaconCommitteeSubscription = { @@ -204,7 +218,7 @@ export type Api = { graffiti: string ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: allForks.BeaconBlock; blockValue: Wei}}, + {[HttpStatusCode.OK]: {data: allForks.BeaconBlock}}, HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE > >; @@ -225,7 +239,33 @@ export type Api = { graffiti: string ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: allForks.BeaconBlock | BlockContents; version: ForkName; blockValue: Wei}}, + {[HttpStatusCode.OK]: {data: allForks.BeaconBlock; version: ForkName}}, + HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE + > + >; + + /** + * Requests a beacon node to produce a valid block, which can then be signed by a validator. + * Metadata in the response indicates the type of block produced, and the supported types of block + * will be added to as forks progress. + * @param slot The slot for which the block should be proposed. + * @param randaoReveal The validator's randao reveal value. + * @param graffiti Arbitrary data validator wants to include in block. + * @returns any Success response + * @throws ApiError + */ + produceBlockV3( + slot: Slot, + randaoReveal: BLSSignature, + graffiti: string, + skipRandaoVerification?: boolean + ): Promise< + ApiClientResponse< + { + [HttpStatusCode.OK]: ProduceBlockV3Res & { + executionPayloadValue: Wei; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE > >; @@ -240,7 +280,6 @@ export type Api = { [HttpStatusCode.OK]: { data: allForks.BlindedBeaconBlock | BlindedBlockContents; version: ForkName; - blockValue: Wei; }; }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE @@ -407,6 +446,7 @@ export const routesData: RoutesData = { getSyncCommitteeDuties: {url: "/eth/v1/validator/duties/sync/{epoch}", method: "POST"}, produceBlock: {url: "/eth/v1/validator/blocks/{slot}", method: "GET"}, produceBlockV2: {url: "/eth/v2/validator/blocks/{slot}", method: "GET"}, + produceBlockV3: {url: "/eth/v3/validator/blocks/{slot}", method: "GET"}, produceBlindedBlock: {url: "/eth/v1/validator/blinded_blocks/{slot}", method: "GET"}, produceAttestationData: {url: "/eth/v1/validator/attestation_data", method: "GET"}, produceSyncCommitteeContribution: {url: "/eth/v1/validator/sync_committee_contribution", method: "GET"}, @@ -429,6 +469,10 @@ export type ReqTypes = { getSyncCommitteeDuties: {params: {epoch: Epoch}; body: U64Str[]}; produceBlock: {params: {slot: number}; query: {randao_reveal: string; graffiti: string}}; produceBlockV2: {params: {slot: number}; query: {randao_reveal: string; graffiti: string}}; + produceBlockV3: { + params: {slot: number}; + query: {randao_reveal: string; graffiti: string; skip_randao_verification?: boolean}; + }; produceBlindedBlock: {params: {slot: number}; query: {randao_reveal: string; graffiti: string}}; produceAttestationData: {query: {slot: number; committee_index: number}}; produceSyncCommitteeContribution: {query: {slot: number; subcommittee_index: number; beacon_block_root: string}}; @@ -484,15 +528,24 @@ export function getReqSerializers(): ReqSerializers { {jsonCase: "eth2"} ); - const produceBlock: ReqSerializers["produceBlock"] = { - writeReq: (slot, randaoReveal, graffiti) => ({ + const produceBlock: ReqSerializers["produceBlockV3"] = { + writeReq: (slot, randaoReveal, graffiti, skipRandaoVerification) => ({ params: {slot}, - query: {randao_reveal: toHexString(randaoReveal), graffiti: toGraffitiHex(graffiti)}, + query: { + randao_reveal: toHexString(randaoReveal), + graffiti: toGraffitiHex(graffiti), + skip_randao_verification: skipRandaoVerification, + }, }), - parseReq: ({params, query}) => [params.slot, fromHexString(query.randao_reveal), fromGraffitiHex(query.graffiti)], + parseReq: ({params, query}) => [ + params.slot, + fromHexString(query.randao_reveal), + fromGraffitiHex(query.graffiti), + query.skip_randao_verification, + ], schema: { params: {slot: Schema.UintRequired}, - query: {randao_reveal: Schema.StringRequired, graffiti: Schema.String}, + query: {randao_reveal: Schema.StringRequired, graffiti: Schema.String, skip_randao_verification: Schema.Boolean}, }, }; @@ -523,9 +576,10 @@ export function getReqSerializers(): ReqSerializers { }, }, - produceBlock: produceBlock, - produceBlockV2: produceBlock, - produceBlindedBlock: produceBlock, + produceBlock: produceBlock as ReqSerializers["produceBlock"], + produceBlockV2: produceBlock as ReqSerializers["produceBlock"], + produceBlockV3: produceBlock, + produceBlindedBlock: produceBlock as ReqSerializers["produceBlock"], produceAttestationData: { writeReq: (index, slot) => ({query: {slot, committee_index: index}}), @@ -633,23 +687,43 @@ export function getReturnTypes(): ReturnTypes { {jsonCase: "eth2"} ); + const produceBlockOrContents = WithVersion((fork: ForkName) => + isForkBlobs(fork) ? AllForksBlockContentsResSerializer(() => fork) : ssz[fork].BeaconBlock + ) as TypeJson; + const produceBlindedBlockOrContents = WithVersion( + (fork: ForkName) => + isForkBlobs(fork) + ? AllForksBlindedBlockContentsResSerializer(() => fork) + : ssz.allForksBlinded[isForkExecution(fork) ? fork : ForkName.bellatrix].BeaconBlock + ) as TypeJson; + return { getAttesterDuties: WithDependentRootExecutionOptimistic(ArrayOf(AttesterDuty)), getProposerDuties: WithDependentRootExecutionOptimistic(ArrayOf(ProposerDuty)), getSyncCommitteeDuties: ContainerDataExecutionOptimistic(ArrayOf(SyncDuty)), - produceBlock: WithBlockValue(ContainerData(ssz.phase0.BeaconBlock)), - produceBlockV2: WithBlockValue( - WithVersion((fork: ForkName) => - isForkBlobs(fork) ? AllForksBlockContentsResSerializer(() => fork) : ssz[fork].BeaconBlock - ) - ), - produceBlindedBlock: WithBlockValue( - WithVersion((fork: ForkName) => - isForkBlobs(fork) - ? AllForksBlindedBlockContentsResSerializer(() => fork) - : ssz.allForksBlinded[isForkExecution(fork) ? fork : ForkName.bellatrix].BeaconBlock - ) + + produceBlock: ContainerData(ssz.phase0.BeaconBlock), + produceBlockV2: WithVersion((fork: ForkName) => ssz[fork].BeaconBlock), + produceBlockV3: WithExecutionPayloadValue({ + toJson: (data) => + data.executionPayloadBlinded === true + ? produceBlindedBlockOrContents.toJson(data) + : produceBlockOrContents.toJson(data), + fromJson: (data) => { + if ((data as {execution_payload_blinded: true}).execution_payload_blinded) { + return {executionPayloadBlinded: true, ...produceBlindedBlockOrContents.fromJson(data)}; + } else { + return {executionPayloadBlinded: false, ...produceBlockOrContents.fromJson(data)}; + } + // (data as {execution_payload_blinded: true}).execution_payload_blinded + // ? {executionPayloadBlinded: true, ...produceBlindedBlockOrContents.fromJson(data)} + // : {executionPayloadBlinded: false, ...produceBlockOrContents.fromJson(data)}, + }, + }), + produceBlindedBlock: WithVersion( + (fork: ForkName) => ssz.allForksBlinded[isForkExecution(fork) ? fork : ForkName.bellatrix].BeaconBlock ), + produceAttestationData: ContainerData(ssz.phase0.AttestationData), produceSyncCommitteeContribution: ContainerData(ssz.altair.SyncCommitteeContribution), getAggregatedAttestation: ContainerData(ssz.phase0.Attestation), diff --git a/packages/api/src/utils/schema.ts b/packages/api/src/utils/schema.ts index 03af48195f86..6b08f27bdbab 100644 --- a/packages/api/src/utils/schema.ts +++ b/packages/api/src/utils/schema.ts @@ -34,6 +34,7 @@ export enum Schema { Object, ObjectArray, AnyArray, + Boolean, } /** @@ -68,6 +69,9 @@ function getJsonSchemaItem(schema: Schema): JsonSchema { case Schema.AnyArray: return {type: "array"}; + + case Schema.Boolean: + return {type: "boolean"}; } } diff --git a/packages/api/src/utils/types.ts b/packages/api/src/utils/types.ts index a62fb30b7270..0586a34e04b3 100644 --- a/packages/api/src/utils/types.ts +++ b/packages/api/src/utils/types.ts @@ -29,7 +29,7 @@ export type RouteDef = { export type ReqGeneric = { params?: Record; - query?: Record; + query?: Record; body?: any; headers?: Record; }; @@ -179,18 +179,20 @@ export function WithExecutionOptimistic( } /** - * SSZ factory helper to wrap an existing type with `{blockValue: Wei}` + * SSZ factory helper to wrap an existing type with `{executionPayloadValue: Wei}` */ -export function WithBlockValue(type: TypeJson): TypeJson { +export function WithExecutionPayloadValue( + type: TypeJson +): TypeJson { return { - toJson: ({blockValue, ...data}) => ({ + toJson: ({executionPayloadValue, ...data}) => ({ ...(type.toJson(data as unknown as T) as Record), - block_value: blockValue.toString(), + execution_payload_value: executionPayloadValue.toString(), }), - fromJson: ({block_value, ...data}: T & {block_value: string}) => ({ + fromJson: ({execution_payload_value, ...data}: T & {execution_payload_value: string}) => ({ ...type.fromJson(data), // For cross client usage where beacon or validator are of separate clients, blockValue could be missing - blockValue: BigInt(block_value ?? "0"), + executionPayloadValue: BigInt(execution_payload_value ?? "0"), }), }; }