Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5473bb5
init
ensi321 Feb 26, 2026
9c91667
init
ensi321 Feb 27, 2026
9681ba4
init
ensi321 Mar 3, 2026
048be5e
Fix race condition
ensi321 Mar 3, 2026
4be9bfb
fix: use block post-state for envelope signature verification
ensi321 Mar 3, 2026
91c6787
clean up
ensi321 Mar 3, 2026
7c998d3
Merge branch 'unstable' into nc/epbs-payload-import
ensi321 Mar 3, 2026
a920a05
add todo
ensi321 Mar 5, 2026
27dba2a
Merge branch 'unstable' into nc/epbs-payload-import
ensi321 Mar 5, 2026
548cf77
fix merge
ensi321 Mar 5, 2026
9a949e9
Merge branch 'unstable' into nc/epbs-payload-import
ensi321 Mar 10, 2026
569eb98
Fix merge
ensi321 Mar 10, 2026
aa3ac88
partially address comments
ensi321 Mar 10, 2026
6402206
address comments
ensi321 Mar 10, 2026
952af2c
Merge branch 'unstable' into nc/epbs-payload-import
ensi321 Mar 11, 2026
4b499ca
fix: merge + incorporate changes from #9007
ensi321 Mar 11, 2026
ef6a471
Address comments
ensi321 Mar 11, 2026
4654d10
import payload to fork choice
ensi321 Mar 11, 2026
e882599
Use IDataColumnsInput
ensi321 Mar 11, 2026
dc5a1bd
Merge branch 'unstable' into nc/epbs-payload-import
ensi321 Mar 12, 2026
c981eba
Use queue for payload persisting
ensi321 Mar 12, 2026
41048b9
Add PayloadEnvelopeProcessor
ensi321 Mar 12, 2026
1093831
lint
ensi321 Mar 12, 2026
5c3c615
Merge branch 'unstable' into nc/epbs-payload-import
ensi321 Mar 12, 2026
56729bf
hasAllData and hasComputedAllData
ensi321 Mar 12, 2026
6f8ae62
address comments
ensi321 Mar 13, 2026
8172f1d
Apply suggestions from code review
ensi321 Mar 13, 2026
475a40d
Merge branch 'unstable' into nc/epbs-payload-import
ensi321 Mar 13, 2026
69dc238
Address comments
ensi321 Mar 13, 2026
158da59
address comments
ensi321 Mar 13, 2026
5dfd69b
Fix ethspecify
ensi321 Mar 13, 2026
752e671
Merge branch 'unstable' into nc/epbs-payload-import
nflaig Mar 15, 2026
e43f632
Fix build
nflaig Mar 15, 2026
bbfb962
Remove execution_payload_available from ignored topics
nflaig Mar 15, 2026
bfbe600
refactor: handle payload import dedupe in processor (#9041)
nflaig Mar 18, 2026
2603cf0
Update packages/beacon-node/src/chain/blocks/importBlock.ts
ensi321 Mar 19, 2026
c9132b3
Apply suggestions from code review
ensi321 Mar 19, 2026
7a74153
address comments
ensi321 Mar 19, 2026
996a990
Address comments
ensi321 Mar 19, 2026
993efa2
fix: review payload envelope import pipeline PR #8962 (#9063)
twoeths Mar 19, 2026
a4f7a1a
address comments
ensi321 Mar 19, 2026
581b376
Merge branch 'unstable' into nc/epbs-payload-import
ensi321 Mar 20, 2026
4aa950d
Remove type hacks in payloadEnvelopeInput.ts
nflaig Mar 20, 2026
eff7623
always log payload envelope input creation
nflaig Mar 20, 2026
1cdc693
Fix order of gossip checks
nflaig Mar 20, 2026
4946033
Revert "Fix order of gossip checks"
nflaig Mar 20, 2026
81b5cd2
Adjust naming to match seenBlockInputCache
nflaig Mar 20, 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
14 changes: 14 additions & 0 deletions packages/api/src/beacon/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export enum EventType {
blobSidecar = "blob_sidecar",
/** The node has received a valid DataColumnSidecar (from P2P or API) */
dataColumnSidecar = "data_column_sidecar",
/** The node has verified that the execution payload and blobs for a block are available */
executionPayloadAvailable = "execution_payload_available",
}

export const eventTypes: {[K in EventType]: K} = {
Expand All @@ -108,6 +110,7 @@ export const eventTypes: {[K in EventType]: K} = {
[EventType.payloadAttributes]: EventType.payloadAttributes,
[EventType.blobSidecar]: EventType.blobSidecar,
[EventType.dataColumnSidecar]: EventType.dataColumnSidecar,
[EventType.executionPayloadAvailable]: EventType.executionPayloadAvailable,
};

export type EventData = {
Expand Down Expand Up @@ -157,6 +160,10 @@ export type EventData = {
[EventType.payloadAttributes]: {version: ForkName; data: SSEPayloadAttributes};
[EventType.blobSidecar]: BlobSidecarSSE;
[EventType.dataColumnSidecar]: DataColumnSidecarSSE;
[EventType.executionPayloadAvailable]: {
slot: Slot;
blockRoot: RootHex;
};
};

export type BeaconEvent = {[K in EventType]: {type: K; message: EventData[K]}}[EventType];
Expand Down Expand Up @@ -311,6 +318,13 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type
[EventType.payloadAttributes]: WithVersion((fork) => getPostBellatrixForkTypes(fork).SSEPayloadAttributes),
[EventType.blobSidecar]: blobSidecarSSE,
[EventType.dataColumnSidecar]: dataColumnSidecarSSE,
[EventType.executionPayloadAvailable]: new ContainerType(
{
slot: ssz.Slot,
blockRoot: stringType,
},
{jsonCase: "eth2"}
),

[EventType.lightClientOptimisticUpdate]: WithVersion(
(fork) => getPostAltairForkTypes(fork).LightClientOptimisticUpdate
Expand Down
1 change: 0 additions & 1 deletion packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ runTestCheckAgainstSpec(openApiJson, definitions, testDatas, ignoredOperations,

const ignoredTopics: string[] = [
// TODO GLOAS: required by v5.0.0-alpha.0
"execution_payload_available",
"execution_payload_bid",
"payload_attestation_message",
];
Expand Down
4 changes: 4 additions & 0 deletions packages/api/test/unit/beacon/testData/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,8 @@ export const eventTestData: EventData = {
"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
],
}),
[EventType.executionPayloadAvailable]: {
slot: 10,
blockRoot: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf",
},
};
53 changes: 36 additions & 17 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from "@lodestar/types";
import {fromHex, sleep, toHex, toRootHex} from "@lodestar/utils";
import {BlockInputSource, isBlockInputBlobs, isBlockInputColumns} from "../../../../chain/blocks/blockInput/index.js";
import {PayloadEnvelopeInputSource} from "../../../../chain/blocks/payloadEnvelopeInput/index.js";
import {ImportBlockOpts} from "../../../../chain/blocks/types.js";
import {verifyBlocksInEpoch} from "../../../../chain/blocks/verifyBlock.js";
import {BeaconChain} from "../../../../chain/chain.js";
Expand All @@ -48,6 +49,7 @@ import {
ProduceFullGloas,
} from "../../../../chain/produceBlock/index.js";
import {validateGossipBlock} from "../../../../chain/validation/block.js";
import {validateApiExecutionPayloadEnvelope} from "../../../../chain/validation/executionPayloadEnvelope.js";
import {OpSource} from "../../../../chain/validatorMonitor.js";
import {
computePreFuluKzgCommitmentsInclusionProof,
Expand Down Expand Up @@ -659,6 +661,8 @@ export function getBeaconBlockApi({
throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`);
}

await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope);

const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD;
let dataColumnSidecars: gloas.DataColumnSidecars = [];

Expand All @@ -676,6 +680,7 @@ export function getBeaconBlockApi({
}

if (cachedResult.cells && cachedResult.blobsBundle.commitments.length > 0) {
const timer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();
const cellsAndProofs = cachedResult.cells.map((rowCells, rowIndex) => ({
cells: rowCells,
proofs: cachedResult.blobsBundle.proofs.slice(
Expand All @@ -685,24 +690,42 @@ export function getBeaconBlockApi({
}));

dataColumnSidecars = getDataColumnSidecarsForGloas(slot, envelope.beaconBlockRoot, cellsAndProofs);
timer?.();
}
} else {
// TODO GLOAS: will this api be used by builders or only for self-building?
}

// TODO GLOAS: Verify execution payload envelope signature
// For self-builds, the proposer signs with their own validator key
// For external builders, verify using the builder's registered pubkey
// Use verify_execution_payload_envelope_signature(state, signed_envelope)
// If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N.
const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now();
if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
await sleep(msToBlockSlot);
}

// TODO GLOAS: Process execution payload via state transition
// Call process_execution_payload(state, signed_envelope, execution_engine)
// TODO GLOAS: if block and payload are submitted in parallel, payloadInput may not yet exist.
// A queuing mechanism is needed to handle this case. See https://github.com/ChainSafe/lodestar/issues/8915
const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
if (!payloadInput) {
throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`);
Comment thread
nflaig marked this conversation as resolved.
}
Comment thread
ensi321 marked this conversation as resolved.

// TODO GLOAS: Update fork choice with the execution payload
// Call on_execution_payload(store, signed_envelope) to update fork choice state
payloadInput.addPayloadEnvelope({
envelope: signedExecutionPayloadEnvelope,
source: PayloadEnvelopeInputSource.api,
seenTimestampSec,
peerIdStr: undefined,
});

// TODO GLOAS: Add envelope and data columns to block input via seenBlockInputCache
// and trigger block import (Gloas block import requires both beacon block and envelope)
if (dataColumnSidecars.length > 0) {
for (const columnSidecar of dataColumnSidecars) {
payloadInput.addColumn({
columnSidecar,
source: PayloadEnvelopeInputSource.api,
seenTimestampSec,
peerIdStr: undefined,
});
}
}

const valLogMeta = {
slot,
Expand All @@ -712,23 +735,19 @@ export function getBeaconBlockApi({
dataColumns: dataColumnSidecars.length,
};

// If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N.
const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now();
if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
await sleep(msToBlockSlot);
}

const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime);
metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.api}, delaySec);
chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.api, delaySec, signedExecutionPayloadEnvelope);

chain.logger.info("Publishing execution payload envelope", valLogMeta);

// Publish envelope and data columns
const publishPromises = [
// Gossip the signed execution payload envelope first
() => network.publishSignedExecutionPayloadEnvelope(signedExecutionPayloadEnvelope),
// For self-builds, publish all data column sidecars
...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)),
// Import execution payload. Signature already verified above
() => chain.processExecutionPayload(payloadInput, {validSignature: true}),
];

const sentPeersArr = await promiseAllMaybeAsync<number | void>(publishPromises);
Expand Down
6 changes: 3 additions & 3 deletions packages/beacon-node/src/chain/blocks/blockInput/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ForkName} from "@lodestar/params";
import {ColumnIndex, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types";
import {ColumnIndex, DataColumnSidecars, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types";
import {VersionedHashes} from "../../../execution/index.js";

export enum DAType {
Expand Down Expand Up @@ -107,9 +107,9 @@ export type MissingColumnMeta = {
export interface IDataColumnsInput {
readonly slot: Slot;
readonly blockRootHex: string;
getCustodyColumns(): fulu.DataColumnSidecars;
getCustodyColumns(): DataColumnSidecars;
hasComputedAllData(): boolean;
waitForComputedAllData(timeout: number, signal?: AbortSignal): Promise<fulu.DataColumnSidecars>;
waitForComputedAllData(timeout: number, signal?: AbortSignal): Promise<DataColumnSidecars>;
}

/**
Expand Down
38 changes: 36 additions & 2 deletions packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import {
getSafeExecutionBlockHash,
isGloasBlock,
} from "@lodestar/fork-choice";
import {ForkPostAltair, ForkPostElectra, ForkSeq, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
import {
ForkPostAltair,
ForkPostElectra,
ForkPostGloas,
ForkSeq,
MAX_SEED_LOOKAHEAD,
SLOTS_PER_EPOCH,
} from "@lodestar/params";
import {
CachedBeaconStateAltair,
EpochCache,
Expand All @@ -21,7 +28,17 @@ import {
isStartSlotOfEpoch,
isStateValidatorsNodesPopulated,
} from "@lodestar/state-transition";
import {Attestation, BeaconBlock, altair, capella, electra, isGloasBeaconBlock, phase0, ssz} from "@lodestar/types";
import {
Attestation,
BeaconBlock,
SignedBeaconBlock,
altair,
capella,
electra,
isGloasBeaconBlock,
phase0,
ssz,
} from "@lodestar/types";
import {isErrorAborted, toRootHex} from "@lodestar/utils";
import {ZERO_HASH_HEX} from "../../constants/index.js";
import {callInNextEventLoop} from "../../util/eventLoop.js";
Expand Down Expand Up @@ -123,6 +140,23 @@ export async function importBlock(
// processState manages both block state and payload state variants together for memory/disk management
this.regen.processBlockState(blockRootHex, postState);

// For Gloas blocks, create PayloadEnvelopeInput so it's available for later payload import
Comment thread
ensi321 marked this conversation as resolved.
if (fork >= ForkSeq.gloas) {
this.seenPayloadEnvelopeInputCache.add({
blockRootHex,
block: block as SignedBeaconBlock<ForkPostGloas>,
sampledColumns: this.custodyConfig.sampledColumns,
custodyColumns: this.custodyConfig.custodyColumns,
timeCreatedSec: fullyVerifiedBlock.seenTimestampSec,
});
this.logger.debug("Created PayloadEnvelopeInput for block", {
slot: blockSlot,
root: blockRootHex,
source: source.source,
...(opts.seenTimestampSec !== undefined ? {recvToImport: Date.now() / 1000 - opts.seenTimestampSec} : {}),
});
}

this.metrics?.importBlock.bySource.inc({source: source.source});
this.logger.verbose("Added block to forkchoice and state cache", {slot: blockSlot, root: blockRootHex});

Expand Down
Loading
Loading