Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
b6f7c3a
feat: stateless flow for local block production post-gloas
nflaig Jul 5, 2026
ddcd146
inline envelope fork lookup
nflaig Jul 5, 2026
374b535
rename flag to statelessBlockProduction and set default based on conf…
nflaig Jul 5, 2026
e8244d1
validate submitted blob data against bid commitments
nflaig Jul 5, 2026
930d890
add Gloas to docs wordlist
nflaig Jul 5, 2026
76a45a3
Merge branch 'unstable' into nflaig/stateless-flow
nflaig Jul 12, 2026
50a8996
Merge remote-tracking branch 'origin/unstable' into nflaig/stateless-…
nflaig Jul 14, 2026
f65b099
require include_payload param in produceBlockV4
nflaig Jul 14, 2026
b447138
support builder selection params in produceBlockV4
nflaig Jul 14, 2026
ba362d6
rename statelessBlockProduction flag to payloadLocal
nflaig Jul 14, 2026
b65420b
harden broadcast validation when publishing execution payload envelope
nflaig Jul 14, 2026
469a1a6
clarify why consensus checks always run for published envelopes
nflaig Jul 14, 2026
8c67de6
align bid selection logs and censorship handling with produceBlockV3
nflaig Jul 14, 2026
866b52f
add todo for envelope equivocation checks
nflaig Jul 14, 2026
9eb96e0
clarify bid value payment is protocol-enforced
nflaig Jul 14, 2026
03b46b6
simplify bid value comment
nflaig Jul 14, 2026
025b68e
log suspected builder censorship as warn
nflaig Jul 14, 2026
7b04cb9
fix stale comment on stateless flow default
nflaig Jul 14, 2026
23f061b
harden execution payload envelope publishing edge cases
nflaig Jul 14, 2026
e7ac8f0
only report builder censorship if builder flow is enabled
nflaig Jul 14, 2026
f1b2b9f
clarify import signature verification comment
nflaig Jul 14, 2026
7331786
clarify equivocation check todo
nflaig Jul 14, 2026
39264ba
scope equivocation todo to envelope publishing
nflaig Jul 14, 2026
0282a41
always treat envelope signature as verified on api import
nflaig Jul 14, 2026
24eabeb
tweak comments
nflaig Jul 15, 2026
c0fdc24
Merge branch 'unstable' into nflaig/stateless-flow
nflaig Jul 15, 2026
878e9f2
Merge branch 'unstable' into nflaig/stateless-flow
nflaig Jul 17, 2026
c0e5ad9
fix: only time data column sidecar computation when cells are computed
nflaig Jul 17, 2026
c03f295
return 404 when requesting envelope for a builder bid block
nflaig Jul 17, 2026
9709fe1
Merge branch 'unstable' into nflaig/stateless-flow
nflaig Jul 17, 2026
975693c
Apply suggestion from myself
nflaig Jul 17, 2026
b7b1b43
cover circuit breaker scenarios in unit tests
nflaig Jul 17, 2026
6f2bd9d
treat builderonly as builderalways post-gloas
nflaig Jul 17, 2026
f5ea449
remove support for builderonly block selection
nflaig Jul 17, 2026
58644c3
Merge branch 'unstable' into nflaig/stateless-flow
nflaig Jul 17, 2026
ddca7b2
warn and rewrite builderonly to builderalways on beacon node
nflaig Jul 17, 2026
0474b09
clean up builder selection defaulting comment and test mocks
nflaig Jul 17, 2026
b2d14ad
forward builder selection post-gloas with fork-aware default
nflaig Jul 17, 2026
ac25374
Merge branch 'unstable' into nflaig/stateless-flow
nflaig Jul 24, 2026
ebebebb
rename signedEnvelope to signedEnvelopeOrContents
nflaig Jul 24, 2026
97e5395
chore: add slot to missing cached block production error
nflaig Jul 24, 2026
519af81
fix: return 500 for block state regeneration failure on envelope publish
nflaig Jul 24, 2026
c80042e
reuse single payload envelope input lookup on envelope publish
nflaig Jul 24, 2026
daa3c35
fix log
nflaig Jul 24, 2026
7ffe483
include submitted blob count in envelope publish logs
nflaig Jul 24, 2026
3390ca1
use blockRoot key in missing cached block production error
nflaig Jul 24, 2026
51da9f7
track payload publish errors and enrich envelope publish error messages
nflaig Jul 24, 2026
a519000
include executionPayloadIncluded in published envelope log
nflaig Jul 24, 2026
0312108
separate metrics
nflaig Jul 24, 2026
8c69c54
lint
nflaig Jul 24, 2026
09f35c8
improve gloas block proposal logs
nflaig Jul 25, 2026
fe599a2
Merge branch 'unstable' into nflaig/stateless-flow
nflaig Jul 25, 2026
1093c43
re-add execution payload value to produceBlockV4
nflaig Jul 25, 2026
ef4f390
tweak comment
nflaig Jul 25, 2026
21d1956
fix metrics
nflaig Jul 25, 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
1 change: 1 addition & 0 deletions .wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Flamegraphs
GPG
Geth
Github
Gloas
Goerli
Golang
Gossipsub
Expand Down
65 changes: 51 additions & 14 deletions packages/api/src/beacon/routes/beacon/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Slot,
deneb,
gloas,
isSignedExecutionPayloadEnvelopeContents,
ssz,
sszTypesFor,
} from "@lodestar/types";
Expand All @@ -32,6 +33,7 @@ import {
ExecutionOptimisticFinalizedAndVersionMeta,
MetaHeader,
} from "../../../utils/metadata.js";
import {toBoolean} from "../../../utils/serdes.js";
import {WireFormat} from "../../../utils/wireFormat.js";

export const BlockHeaderResponseType = new ContainerType({
Expand Down Expand Up @@ -187,11 +189,22 @@ export type Endpoints = {
* 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.
*
* The request body is either a `SignedExecutionPayloadEnvelopeContents` (envelope with blobs
* and KZG proofs, stateless flow) or a `SignedExecutionPayloadEnvelope` (stateful flow,
* the beacon node attaches blobs and KZG proofs from its block production cache).
*/
publishExecutionPayloadEnvelope: Endpoint<
"POST",
{signedExecutionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope},
{body: unknown; headers: {[MetaHeader.Version]: string}},
{
signedEnvelope: gloas.SignedExecutionPayloadEnvelopeContents | gloas.SignedExecutionPayloadEnvelope;
broadcastValidation?: BroadcastValidation;
},
{
body: unknown;
headers: {[MetaHeader.Version]: string; [MetaHeader.BlobDataIncluded]: string};
query: {broadcast_validation?: string};
},
EmptyResponseData,
EmptyMeta
>;
Expand Down Expand Up @@ -453,40 +466,64 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
url: "/eth/v1/beacon/execution_payload_envelopes",
method: "POST",
req: {
writeReqJson: ({signedExecutionPayloadEnvelope}) => {
const fork = config.getForkName(signedExecutionPayloadEnvelope.message.payload.slotNumber);
writeReqJson: ({signedEnvelope, broadcastValidation}) => {
const blobDataIncluded = isSignedExecutionPayloadEnvelopeContents(signedEnvelope);
const fork = config.getForkName(
(blobDataIncluded ? signedEnvelope.signedExecutionPayloadEnvelope : signedEnvelope).message.payload
.slotNumber
);
return {
body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.toJson(signedExecutionPayloadEnvelope),
body: blobDataIncluded
? getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelopeContents.toJson(signedEnvelope)
: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.toJson(signedEnvelope),
headers: {
[MetaHeader.Version]: fork,
[MetaHeader.BlobDataIncluded]: blobDataIncluded.toString(),
},
query: {broadcast_validation: broadcastValidation},
};
},
parseReqJson: ({body, headers}) => {
parseReqJson: ({body, headers, query}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
const blobDataIncluded = toBoolean(fromHeaders(headers, MetaHeader.BlobDataIncluded));
return {
signedExecutionPayloadEnvelope: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.fromJson(body),
signedEnvelope: blobDataIncluded
? getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelopeContents.fromJson(body)
: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.fromJson(body),
broadcastValidation: query.broadcast_validation as BroadcastValidation,
};
},
writeReqSsz: ({signedExecutionPayloadEnvelope}) => {
const fork = config.getForkName(signedExecutionPayloadEnvelope.message.payload.slotNumber);
writeReqSsz: ({signedEnvelope, broadcastValidation}) => {
const blobDataIncluded = isSignedExecutionPayloadEnvelopeContents(signedEnvelope);
const fork = config.getForkName(
(blobDataIncluded ? signedEnvelope.signedExecutionPayloadEnvelope : signedEnvelope).message.payload
.slotNumber
);
return {
body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.serialize(signedExecutionPayloadEnvelope),
body: blobDataIncluded
? getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelopeContents.serialize(signedEnvelope)
: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.serialize(signedEnvelope),
headers: {
[MetaHeader.Version]: fork,
[MetaHeader.BlobDataIncluded]: blobDataIncluded.toString(),
},
query: {broadcast_validation: broadcastValidation},
};
},
parseReqSsz: ({body, headers}) => {
parseReqSsz: ({body, headers, query}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
const blobDataIncluded = toBoolean(fromHeaders(headers, MetaHeader.BlobDataIncluded));
Comment thread
nflaig marked this conversation as resolved.
return {
signedExecutionPayloadEnvelope:
getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.deserialize(body),
signedEnvelope: blobDataIncluded
? getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelopeContents.deserialize(body)
: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.deserialize(body),
broadcastValidation: query.broadcast_validation as BroadcastValidation,
};
},
schema: {
body: Schema.Object,
headers: {[MetaHeader.Version]: Schema.String},
query: {broadcast_validation: Schema.String},
headers: {[MetaHeader.Version]: Schema.String, [MetaHeader.BlobDataIncluded]: Schema.String},
},
},
resp: EmptyResponseCodec,
Expand Down
34 changes: 30 additions & 4 deletions packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
EmptyResponseCodec,
EmptyResponseData,
JsonOnlyReq,
WithMeta,
WithVersion,
} from "../../utils/codecs.js";
import {getPostBellatrixForkTypes, getPostGloasForkTypes, toForkName} from "../../utils/fork.js";
Expand Down Expand Up @@ -98,6 +99,8 @@ export const ProduceBlockV4MetaType = new ContainerType(
...VersionType.fields,
/** Consensus rewards paid to the proposer for this block, in Wei */
consensusBlockValue: ssz.UintBn64,
/** Specifies whether the response contains full block contents or only the beacon block */
executionPayloadIncluded: ssz.Boolean,
},
{jsonCase: "eth2"}
);
Expand Down Expand Up @@ -421,6 +424,12 @@ export type Endpoints = {
* 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.
*
* When self-building and `includePayload` is true, the response contains the full `BlockContents`
* (block, execution payload envelope, KZG proofs and blobs) which enables stateless envelope
* publishing via any beacon node. When `includePayload` is false, only the `BeaconBlock` is
* returned and the beacon node caches the envelope and blobs internally.
* When committing to a builder bid, only the `BeaconBlock` is returned in either case.
*/
produceBlockV4: Endpoint<
"GET",
Expand All @@ -433,6 +442,8 @@ export type Endpoints = {
graffiti?: string;
skipRandaoVerification?: boolean;
builderBoostFactor?: UintBn64;
/** Include execution payload envelope and blobs in the response when self-building */
includePayload: boolean;
} & Omit<ExtraProduceBlockOpts, "blindedLocal">,
{
params: {slot: number};
Expand All @@ -444,16 +455,18 @@ export type Endpoints = {
builder_selection?: string;
builder_boost_factor?: string;
strict_fee_recipient_check?: boolean;
include_payload: boolean;
};
},
BeaconBlock<ForkPostGloas>,
BeaconBlock<ForkPostGloas> | BlockContents<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.
* Retrieves the cached execution payload envelope for a given slot and beacon block root,
* to be signed and published via `publishExecutionPayloadEnvelope`.
* Used in the stateful (`includePayload=false`) local build flow.
*/
getExecutionPayloadEnvelope: Endpoint<
"GET",
Expand Down Expand Up @@ -906,6 +919,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
builderSelection,
builderBoostFactor,
strictFeeRecipientCheck,
includePayload,
}) => ({
params: {slot},
query: {
Expand All @@ -916,6 +930,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
builder_selection: builderSelection,
builder_boost_factor: builderBoostFactor?.toString(),
strict_fee_recipient_check: strictFeeRecipientCheck,
include_payload: includePayload,
},
}),
parseReq: ({params, query}) => ({
Expand All @@ -927,6 +942,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
builderSelection: query.builder_selection as BuilderSelection,
builderBoostFactor: parseBuilderBoostFactor(query.builder_boost_factor),
strictFeeRecipientCheck: query.strict_fee_recipient_check,
includePayload: query.include_payload,
}),
schema: {
params: {slot: Schema.UintRequired},
Expand All @@ -938,21 +954,31 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
builder_selection: Schema.String,
builder_boost_factor: Schema.String,
strict_fee_recipient_check: Schema.Boolean,
include_payload: Schema.BooleanRequired,
Comment thread
nflaig marked this conversation as resolved.
},
},
},
resp: {
data: WithVersion((fork) => getPostGloasForkTypes(fork).BeaconBlock),
data: WithMeta(
({version, executionPayloadIncluded}) =>
(executionPayloadIncluded
? getPostGloasForkTypes(version).BlockContents
: getPostGloasForkTypes(version).BeaconBlock) as Type<
BeaconBlock<ForkPostGloas> | BlockContents<ForkPostGloas>
>
),
meta: {
toJson: (meta) => ProduceBlockV4MetaType.toJson(meta),
fromJson: (val) => ProduceBlockV4MetaType.fromJson(val),
toHeadersObject: (meta) => ({
[MetaHeader.Version]: meta.version,
[MetaHeader.ConsensusBlockValue]: meta.consensusBlockValue.toString(),
[MetaHeader.ExecutionPayloadIncluded]: meta.executionPayloadIncluded.toString(),
}),
fromHeaders: (headers) => ({
version: toForkName(headers.getRequired(MetaHeader.Version)),
consensusBlockValue: BigInt(headers.getRequired(MetaHeader.ConsensusBlockValue)),
executionPayloadIncluded: toBoolean(headers.getRequired(MetaHeader.ExecutionPayloadIncluded)),
}),
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/utils/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ export type ExecutionOptimisticAndDependentRootMeta = ValueOf<typeof ExecutionOp

export enum MetaHeader {
Version = "Eth-Consensus-Version",
BlobDataIncluded = "Eth-Blob-Data-Included",
ConsensusBlockValue = "Eth-Consensus-Block-Value",
ExecutionPayloadBlinded = "Eth-Execution-Payload-Blinded",
ExecutionPayloadIncluded = "Eth-Execution-Payload-Included",
ExecutionPayloadValue = "Eth-Execution-Payload-Value",

/* Lodestar-specific (non-standardized) headers */
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/utils/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export enum Schema {
ObjectArray,
AnyArray,
Boolean,
BooleanRequired,
}

/**
Expand Down Expand Up @@ -71,6 +72,7 @@ function getJsonSchemaItem(schema: Schema): JsonSchema {
return {type: "array"};

case Schema.Boolean:
case Schema.BooleanRequired:
return {type: "boolean"};
}
}
Expand All @@ -81,6 +83,7 @@ function isRequired(schema: Schema): boolean {
case Schema.StringRequired:
case Schema.UintOrStringRequired:
case Schema.StringArrayRequired:
case Schema.BooleanRequired:
return true;

default:
Expand Down
5 changes: 4 additions & 1 deletion packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ export const testData: GenericServerTestCases<Endpoints> = {
res: undefined,
},
publishExecutionPayloadEnvelope: {
args: {signedExecutionPayloadEnvelope: ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue()},
args: {
signedEnvelope: ssz.gloas.SignedExecutionPayloadEnvelopeContents.defaultValue(),
broadcastValidation: BroadcastValidation.gossip,
},
res: undefined,
},
publishExecutionPayloadBid: {
Expand Down
4 changes: 3 additions & 1 deletion packages/api/test/unit/beacon/testData/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,14 @@ export const testData: GenericServerTestCases<Endpoints> = {
feeRecipient,
builderSelection: BuilderSelection.ExecutionAlways,
strictFeeRecipientCheck: true,
includePayload: true,
},
res: {
data: ssz.gloas.BeaconBlock.defaultValue(),
data: ssz.gloas.BlockContents.defaultValue(),
meta: {
version: ForkName.gloas,
consensusBlockValue: ssz.Wei.defaultValue(),
executionPayloadIncluded: true,
},
},
},
Expand Down
12 changes: 11 additions & 1 deletion packages/api/test/utils/checkAgainstSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ export function runTestCheckAgainstSpec<Es extends Record<string, Endpoint>>(
stringifyProperties(reqJson.params ?? {});
stringifyProperties(reqJson.query ?? {});

// Parse headers back to typed values as the spec defines boolean headers as `{schema: type: boolean}`
if (reqJson.headers) {
reqJson.headers = parseHeaders(reqJson.headers as Record<string, string>) as typeof reqJson.headers;
}

const ignoredProperties = ignoredProperty?.request;
if (ignoredProperties) {
// Remove ignored properties from schema validation
Expand Down Expand Up @@ -218,13 +223,18 @@ function prettyAjvErrors(errors: ErrorObject[] | null | undefined): string {
return errors.map((e) => `${e.instancePath ?? "."} - ${e.message}`).join("\n");
}

type StringifiedProperty = string | StringifiedProperty[];
type StringifiedProperty = string | boolean | StringifiedProperty[];

function stringifyProperty(value: unknown): StringifiedProperty {
if (typeof value === "number") {
return value.toString(10);
}

// The spec defines boolean query params as `{schema: type: boolean}`, keep them as booleans
if (typeof value === "boolean") {
return value;
}

if (Array.isArray(value)) {
return value.map(stringifyProperty);
}
Expand Down
Loading
Loading