Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
24f3bf8
feat: implement epbs block production
nflaig Feb 2, 2026
c117757
Merge remote-tracking branch 'origin/unstable' into nflaig/epbs-block…
nflaig Feb 8, 2026
ddd4339
wip
nflaig Feb 8, 2026
3a2535c
Merge branch 'unstable' into nflaig/epbs-block-production
nflaig Feb 12, 2026
096fda8
Fix lint
nflaig Feb 12, 2026
2669838
Merge branch 'unstable' into nflaig/epbs-block-production
nflaig Feb 13, 2026
1871373
Review packages/api
nflaig Feb 13, 2026
be868f3
Updates for alpha.2 spec
nflaig Feb 13, 2026
daec190
Formatting
nflaig Feb 13, 2026
2a1c870
Fix build
nflaig Feb 13, 2026
d0d1a37
Add beacon_block_root to getExecutionPayloadEnvelope
nflaig Feb 13, 2026
d9f90a2
Pass beacon block root from validator client
nflaig Feb 13, 2026
ba769a7
Major refactoring
nflaig Feb 13, 2026
c57ded8
Update beacon-api spec to v5.0.0-alpha.0
nflaig Feb 13, 2026
65be0b4
Review publishExecutionPayloadEnvelope
nflaig Feb 14, 2026
919196f
Merge branch 'unstable' into nflaig/epbs-block-production
nflaig Feb 14, 2026
3b28c1f
Add todo for data column sidecar event
nflaig Feb 14, 2026
af1faa9
Review getExecutionPayloadEnvelope
nflaig Feb 14, 2026
1d48e29
Fix parentBlockNumber
nflaig Feb 14, 2026
b847fc4
There is no slashing for signing two envelopes
nflaig Feb 14, 2026
3756e73
Rephrase comment
nflaig Feb 14, 2026
d1a37ef
Clean up comments
nflaig Feb 14, 2026
7bc2ac8
Review block proposing service
nflaig Feb 14, 2026
93f801f
Review produceBlockV4
nflaig Feb 14, 2026
30bdc97
Add todo
nflaig Feb 14, 2026
83ca642
Restore comment
nflaig Feb 14, 2026
ac5d22a
comments
nflaig Feb 14, 2026
9480e67
Review vc block production
nflaig Feb 14, 2026
efe7b33
Merge branch 'unstable' into nflaig/epbs-block-production
nflaig Feb 15, 2026
fa1e11d
Improve cache assertions in publishExecutionPayloadEnvelope
nflaig Feb 15, 2026
6f0d8f8
Merge branch 'unstable' into nflaig/epbs-block-production
nflaig Feb 15, 2026
66fc969
Fix serialization of builder index over the wire
nflaig Feb 16, 2026
6a33fac
Use BUILDER_INDEX_SELF_BUILD const
nflaig Feb 16, 2026
85655d5
Use BuilderIndex type
nflaig Feb 16, 2026
372f774
Merge branch 'unstable' into nflaig/epbs-block-production
nflaig Feb 16, 2026
eccf56f
Add comment
nflaig Feb 19, 2026
a378856
Check slot consistency of envelope during publishing
nflaig Feb 19, 2026
63400d5
Merge branch 'unstable' into nflaig/epbs-block-production
nflaig Feb 19, 2026
d1611c7
Remove builder index when getting the envelope
nflaig Feb 19, 2026
f2031cc
Remove builder index from log
nflaig Feb 19, 2026
21ae1b8
Revert some changes in params
nflaig Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 87 additions & 19 deletions packages/api/src/beacon/routes/beacon/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import {ChainForkConfig} from "@lodestar/config";
import {
ForkName,
ForkPostDeneb,
ForkPostGloas,
ForkPreBellatrix,
ForkPreDeneb,
ForkPreElectra,
isForkPostBellatrix,
isForkPostDeneb,
isForkPostGloas,
} from "@lodestar/params";
import {
BeaconBlockBody,
Expand All @@ -17,11 +19,12 @@ import {
SignedBlockContents,
Slot,
deneb,
gloas,
ssz,
sszTypesFor,
} from "@lodestar/types";
import {EmptyMeta, EmptyResponseCodec, EmptyResponseData, WithVersion} from "../../../utils/codecs.js";
import {getPostBellatrixForkTypes, toForkName} from "../../../utils/fork.js";
import {getPostBellatrixForkTypes, getPostGloasForkTypes, toForkName} from "../../../utils/fork.js";
import {fromHeaders} from "../../../utils/headers.js";
import {Endpoint, RequestCodec, RouteDefinitions, Schema} from "../../../utils/index.js";
import {
Expand Down Expand Up @@ -209,6 +212,20 @@ export type Endpoints = {
EmptyMeta
>;

/**
* Publish signed execution payload envelope.
* Instructs the beacon node to broadcast a signed execution payload envelope to the network,
* to be gossiped for payload validation. A success response (20x) indicates that the envelope
* passed gossip validation and was successfully broadcast onto the network.
*/
publishExecutionPayloadEnvelope: Endpoint<
"POST",
{signedExecutionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope},
{body: unknown; headers: {[MetaHeader.Version]: string}},
EmptyResponseData,
EmptyMeta
>;

/**
* Get block BlobSidecar
* Retrieves BlobSidecar included in requested block.
Expand Down Expand Up @@ -406,11 +423,14 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
const slot = signedBlockContents.signedBlock.message.slot;
const fork = config.getForkName(slot);
return {
body: isForkPostDeneb(fork)
? sszTypesFor(fork).SignedBlockContents.toJson(signedBlockContents as SignedBlockContents<ForkPostDeneb>)
: sszTypesFor(fork).SignedBeaconBlock.toJson(
signedBlockContents.signedBlock as SignedBeaconBlock<ForkPreDeneb>
),
body:
isForkPostDeneb(fork) && !isForkPostGloas(fork)
? sszTypesFor(fork).SignedBlockContents.toJson(
signedBlockContents as SignedBlockContents<ForkPostDeneb>
)
: sszTypesFor(fork).SignedBeaconBlock.toJson(
signedBlockContents.signedBlock as SignedBeaconBlock<ForkPreDeneb & ForkPostGloas>
),
headers: {
[MetaHeader.Version]: fork,
},
Expand All @@ -420,9 +440,10 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
parseReqJson: ({body, headers, query}) => {
const forkName = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedBlockContents: isForkPostDeneb(forkName)
? sszTypesFor(forkName).SignedBlockContents.fromJson(body)
: {signedBlock: ssz[forkName].SignedBeaconBlock.fromJson(body)},
signedBlockContents:
isForkPostDeneb(forkName) && !isForkPostGloas(forkName)
? sszTypesFor(forkName).SignedBlockContents.fromJson(body)
: {signedBlock: ssz[forkName].SignedBeaconBlock.fromJson(body)},
broadcastValidation: query.broadcast_validation as BroadcastValidation,
};
},
Expand All @@ -431,13 +452,14 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
const fork = config.getForkName(slot);

return {
body: isForkPostDeneb(fork)
? sszTypesFor(fork).SignedBlockContents.serialize(
signedBlockContents as SignedBlockContents<ForkPostDeneb>
)
: sszTypesFor(fork).SignedBeaconBlock.serialize(
signedBlockContents.signedBlock as SignedBeaconBlock<ForkPreDeneb>
),
body:
isForkPostDeneb(fork) && !isForkPostGloas(fork)
? sszTypesFor(fork).SignedBlockContents.serialize(
signedBlockContents as SignedBlockContents<ForkPostDeneb>
)
: sszTypesFor(fork).SignedBeaconBlock.serialize(
signedBlockContents.signedBlock as SignedBeaconBlock<ForkPreDeneb & ForkPostGloas>
),
headers: {
[MetaHeader.Version]: fork,
},
Expand All @@ -447,9 +469,10 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
parseReqSsz: ({body, headers, query}) => {
const forkName = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedBlockContents: isForkPostDeneb(forkName)
? sszTypesFor(forkName).SignedBlockContents.deserialize(body)
: {signedBlock: ssz[forkName].SignedBeaconBlock.deserialize(body)},
signedBlockContents:
isForkPostDeneb(forkName) && !isForkPostGloas(forkName)
? sszTypesFor(forkName).SignedBlockContents.deserialize(body)
: {signedBlock: ssz[forkName].SignedBeaconBlock.deserialize(body)},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably need a gloas.SignedBlockContents

broadcastValidation: query.broadcast_validation as BroadcastValidation,
};
},
Expand Down Expand Up @@ -566,6 +589,51 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
requestWireFormat: WireFormat.ssz,
},
},
publishExecutionPayloadEnvelope: {
url: "/eth/v1/beacon/execution_payload_envelope",
method: "POST",
req: {
writeReqJson: ({signedExecutionPayloadEnvelope}) => {
const fork = config.getForkName(signedExecutionPayloadEnvelope.message.slot);
return {
body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.toJson(signedExecutionPayloadEnvelope),
headers: {
[MetaHeader.Version]: fork,
},
};
},
parseReqJson: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedExecutionPayloadEnvelope: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.fromJson(body),
};
},
writeReqSsz: ({signedExecutionPayloadEnvelope}) => {
const fork = config.getForkName(signedExecutionPayloadEnvelope.message.slot);
return {
body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.serialize(signedExecutionPayloadEnvelope),
headers: {
[MetaHeader.Version]: fork,
},
};
},
parseReqSsz: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedExecutionPayloadEnvelope:
getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.deserialize(body),
};
},
schema: {
body: Schema.Object,
headers: {[MetaHeader.Version]: Schema.String},
},
},
resp: EmptyResponseCodec,
init: {
requestWireFormat: WireFormat.ssz,
},
},
getBlobSidecars: {
url: "/eth/v1/beacon/blob_sidecars/{block_id}",
method: "GET",
Expand Down
158 changes: 157 additions & 1 deletion packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {ContainerType, Type, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {
ForkPostDeneb,
ForkPostGloas,
ForkPreDeneb,
VALIDATOR_REGISTRY_LIMIT,
isForkPostDeneb,
Expand All @@ -22,6 +23,7 @@ import {
UintBn64,
ValidatorIndex,
altair,
gloas,
phase0,
ssz,
sszTypesFor,
Expand All @@ -36,7 +38,7 @@ import {
JsonOnlyReq,
WithVersion,
} from "../../utils/codecs.js";
import {getPostBellatrixForkTypes, toForkName} from "../../utils/fork.js";
import {getPostBellatrixForkTypes, getPostGloasForkTypes, toForkName} from "../../utils/fork.js";
import {fromHeaders} from "../../utils/headers.js";
import {Endpoint, RouteDefinitions, Schema} from "../../utils/index.js";
import {
Expand Down Expand Up @@ -89,6 +91,17 @@ export type ProduceBlockV3Meta = ValueOf<typeof ProduceBlockV3MetaType> & {
executionPayloadSource: ProducedBlockSource;
};

export const ProduceBlockV4MetaType = new ContainerType(
{
...VersionType.fields,
/** Consensus rewards paid to the proposer for this block, in Wei */
consensusBlockValue: ssz.UintBn64,
},
{jsonCase: "eth2"}
);

export type ProduceBlockV4Meta = ValueOf<typeof ProduceBlockV4MetaType>;

export const AttesterDutyType = new ContainerType(
{
/** The validator's public key, uniquely identifying them */
Expand Down Expand Up @@ -357,6 +370,59 @@ export type Endpoints = {
ProduceBlockV3Meta
>;

/**
* Requests a beacon node to produce a valid block, which can then be signed by a validator.
*
* Post-Gloas, proposers submit execution payload bids rather than full execution payloads,
* so there is no longer a concept of blinded or unblinded blocks. Builders release the payload later.
* This endpoint is specific to the post-Gloas forks and is not backwards compatible with previous forks.
*/
produceBlockV4: Endpoint<
"GET",
{
/** The slot for which the block should be proposed */
slot: Slot;
/** The validator's randao reveal value */
randaoReveal: BLSSignature;
/** Arbitrary data validator wants to include in block */
graffiti?: string;
skipRandaoVerification?: boolean;
builderBoostFactor?: UintBn64;
} & Omit<ExtraProduceBlockOpts, "blindedLocal">,
{
params: {slot: number};
query: {
randao_reveal: string;
graffiti?: string;
skip_randao_verification?: string;
fee_recipient?: string;
builder_selection?: string;
builder_boost_factor?: string;
strict_fee_recipient_check?: boolean;
};
},
BeaconBlock<ForkPostGloas>,
ProduceBlockV4Meta
>;

/**
* Get execution payload envelope.
* Retrieves execution payload envelope for a given slot and beacon block root.
* The envelope contains the full execution payload along with associated metadata.
*/
getExecutionPayloadEnvelope: Endpoint<
"GET",
{
/** Slot for which the execution payload envelope is requested */
slot: Slot;
/** Root of the beacon block that this envelope is for */
beaconBlockRoot: Root;
},
{params: {slot: Slot; beacon_block_root: string}},
gloas.ExecutionPayloadEnvelope,
VersionMeta
>;

/**
* Produce an attestation data
* Requests that the beacon node produce an AttestationData.
Expand Down Expand Up @@ -763,6 +829,96 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
},
},
},
produceBlockV4: {
url: "/eth/v4/validator/blocks/{slot}",
method: "GET",
req: {
writeReq: ({
slot,
randaoReveal,
graffiti,
skipRandaoVerification,
feeRecipient,
builderSelection,
builderBoostFactor,
strictFeeRecipientCheck,
}) => ({
params: {slot},
query: {
randao_reveal: toHex(randaoReveal),
graffiti: toGraffitiHex(graffiti),
skip_randao_verification: writeSkipRandaoVerification(skipRandaoVerification),
fee_recipient: feeRecipient,
builder_selection: builderSelection,
builder_boost_factor: builderBoostFactor?.toString(),
strict_fee_recipient_check: strictFeeRecipientCheck,
},
}),
parseReq: ({params, query}) => ({
slot: params.slot,
randaoReveal: fromHex(query.randao_reveal),
graffiti: fromGraffitiHex(query.graffiti),
skipRandaoVerification: parseSkipRandaoVerification(query.skip_randao_verification),
feeRecipient: query.fee_recipient,
builderSelection: query.builder_selection as BuilderSelection,
builderBoostFactor: parseBuilderBoostFactor(query.builder_boost_factor),
strictFeeRecipientCheck: query.strict_fee_recipient_check,
}),
schema: {
params: {slot: Schema.UintRequired},
query: {
randao_reveal: Schema.StringRequired,
graffiti: Schema.String,
skip_randao_verification: Schema.String,
fee_recipient: Schema.String,
builder_selection: Schema.String,
builder_boost_factor: Schema.String,
strict_fee_recipient_check: Schema.Boolean,
},
},
},
resp: {
data: WithVersion((fork) => getPostGloasForkTypes(fork).BeaconBlock),
meta: {
toJson: (meta) => ProduceBlockV4MetaType.toJson(meta),
fromJson: (val) => ProduceBlockV4MetaType.fromJson(val),
toHeadersObject: (meta) => ({
[MetaHeader.Version]: meta.version,
[MetaHeader.ConsensusBlockValue]: meta.consensusBlockValue.toString(),
}),
fromHeaders: (headers) => ({
version: toForkName(headers.getRequired(MetaHeader.Version)),
consensusBlockValue: BigInt(headers.getRequired(MetaHeader.ConsensusBlockValue)),
}),
},
},
},
getExecutionPayloadEnvelope: {
url: "/eth/v1/validator/execution_payload_envelope/{slot}/{beacon_block_root}",
method: "GET",
req: {
writeReq: ({slot, beaconBlockRoot}) => ({
params: {
slot,
beacon_block_root: toRootHex(beaconBlockRoot),
},
}),
parseReq: ({params}) => ({
slot: params.slot,
beaconBlockRoot: fromHex(params.beacon_block_root),
}),
schema: {
params: {
slot: Schema.UintRequired,
beacon_block_root: Schema.StringRequired,
},
},
},
resp: {
data: ssz.gloas.ExecutionPayloadEnvelope,
meta: VersionCodec,
},
},
produceAttestationData: {
url: "/eth/v1/validator/attestation_data",
method: "GET",
Expand Down
10 changes: 10 additions & 0 deletions packages/api/src/utils/fork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
ForkPostAltair,
ForkPostBellatrix,
ForkPostDeneb,
ForkPostGloas,
isForkPostAltair,
isForkPostBellatrix,
isForkPostDeneb,
isForkPostGloas,
} from "@lodestar/params";
import {SSZTypesFor, sszTypesFor} from "@lodestar/types";

Expand Down Expand Up @@ -42,3 +44,11 @@ export function getPostDenebForkTypes(fork: ForkName): SSZTypesFor<ForkPostDeneb

return sszTypesFor(fork);
}

export function getPostGloasForkTypes(fork: ForkName): SSZTypesFor<ForkPostGloas> {
if (!isForkPostGloas(fork)) {
throw Error(`Invalid fork=${fork} for post-gloas fork types`);
}

return sszTypesFor(fork);
}
Loading
Loading