Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8b15011
feat: implement should_apply_proposer_boost for gloas
ensi321 Apr 18, 2026
8fcc417
feat: add processParentExecutionPayload and remove withdrawal early r…
ensi321 Apr 15, 2026
6621c7b
refactor: add processParentExecutionPayload as first step in processB…
ensi321 Apr 15, 2026
2e0bc3a
refactor: transform processExecutionPayloadEnvelope to pure verification
ensi321 Apr 15, 2026
81835b5
refactor: remove executionPayloadStateRoot from fork choice onExecuti…
ensi321 Apr 15, 2026
6d503b7
refactor: simplify envelope import pipeline for deferred processing
ensi321 Apr 15, 2026
5e460d5
feat: block production, gossip validation, and cleanup for deferred p…
ensi321 Apr 15, 2026
f265712
fix: type errors in processParentExecutionPayload and produceBlockBody
ensi321 Apr 15, 2026
ee90b26
feat: range sync envelope support for Gloas (supersedes #9155)
ensi321 Apr 15, 2026
5e81b3d
feat: unknown payload sync for incoming blocks (supersedes #9102)
ensi321 Apr 15, 2026
125777c
feat: Gloas DataColumnSidecar validation in range sync
ensi321 Apr 15, 2026
c790c86
fix: guard payload-seen check on known block and use seenPayloadEnvelope
ensi321 Apr 16, 2026
2be23e5
Update comments
ensi321 Apr 14, 2026
967fdd2
Update comments
ensi321 Apr 14, 2026
f273fd3
Fix spec test
ensi321 Apr 16, 2026
1e4aac4
Address comments & follow up on spec change
ensi321 Apr 16, 2026
f5d78ba
Swap latestBlockHash and latestExecutionPayloadBid
ensi321 Apr 17, 2026
5ac939e
fix upgrade state
ensi321 Apr 17, 2026
9c60ed9
Bump version, skip fast_confirmation
ensi321 Apr 20, 2026
e0da2d8
feat: implement EIP-7843 slot_number in ExecutionPayload (specs#4840)
ensi321 Apr 20, 2026
e30cbf2
fix spec test
ensi321 Apr 20, 2026
e821ddf
Add definition of remaining engine API
ensi321 Apr 21, 2026
cb59e63
feat: cache the last 2 PayloadEnvelopeInputs
twoeths Apr 21, 2026
89e60ae
fix build
ensi321 Apr 22, 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
4 changes: 2 additions & 2 deletions packages/api/src/beacon/routes/beacon/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
method: "POST",
req: {
writeReqJson: ({signedExecutionPayloadEnvelope}) => {
const fork = config.getForkName(signedExecutionPayloadEnvelope.message.slot);
const fork = config.getForkName(signedExecutionPayloadEnvelope.message.payload.slotNumber);
return {
body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.toJson(signedExecutionPayloadEnvelope),
headers: {
Expand All @@ -621,7 +621,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
};
},
writeReqSsz: ({signedExecutionPayloadEnvelope}) => {
const fork = config.getForkName(signedExecutionPayloadEnvelope.message.slot);
const fork = config.getForkName(signedExecutionPayloadEnvelope.message.payload.slotNumber);
return {
body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.serialize(signedExecutionPayloadEnvelope),
headers: {
Expand Down
4 changes: 0 additions & 4 deletions packages/api/src/beacon/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,13 @@ export type EventData = {
builderIndex: BuilderIndex;
blockHash: RootHex;
blockRoot: RootHex;
stateRoot: RootHex;
executionOptimistic: boolean;
};
[EventType.executionPayloadGossip]: {
slot: Slot;
builderIndex: BuilderIndex;
blockHash: RootHex;
blockRoot: RootHex;
stateRoot: RootHex;
};
[EventType.executionPayloadAvailable]: {
slot: Slot;
Expand Down Expand Up @@ -376,7 +374,6 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type
builderIndex: ssz.BuilderIndex,
blockHash: stringType,
blockRoot: stringType,
stateRoot: stringType,
executionOptimistic: ssz.Boolean,
},
{jsonCase: "eth2"}
Expand All @@ -387,7 +384,6 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type
builderIndex: ssz.BuilderIndex,
blockHash: stringType,
blockRoot: stringType,
stateRoot: stringType,
},
{jsonCase: "eth2"}
),
Expand Down
2 changes: 0 additions & 2 deletions packages/api/test/unit/beacon/testData/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,15 +279,13 @@ export const eventTestData: EventData = {
builderIndex: 42,
blockHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
blockRoot: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf",
stateRoot: "0x600e852a08c1200654ddf11025f1ceacb3c2e74bdd5c630cde0838b2591b69f9",
executionOptimistic: false,
},
[EventType.executionPayloadGossip]: {
slot: 10,
builderIndex: 42,
blockHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
blockRoot: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf",
stateRoot: "0x600e852a08c1200654ddf11025f1ceacb3c2e74bdd5c630cde0838b2591b69f9",
},
[EventType.executionPayloadAvailable]: {
slot: 10,
Expand Down
6 changes: 2 additions & 4 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,11 +651,11 @@ export function getBeaconBlockApi({
async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}) {
const seenTimestampSec = Date.now() / 1000;
const envelope = signedExecutionPayloadEnvelope.message;
const slot = envelope.slot;
const slot = envelope.payload.slotNumber;
const fork = config.getForkName(slot);
const blockRootHex = toRootHex(envelope.beaconBlockRoot);
const blockHashHex = toRootHex(envelope.payload.blockHash);
const stateRootHex = toRootHex(envelope.stateRoot);
// stateRoot removed from envelope in consensus-specs#5094

if (!isForkPostGloas(fork)) {
throw new ApiError(400, `publishExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`);
Expand Down Expand Up @@ -740,7 +740,6 @@ export function getBeaconBlockApi({
slot,
blockRoot: blockRootHex,
blockHash: blockHashHex,
stateRoot: stateRootHex,
builderIndex: envelope.builderIndex,
isSelfBuild,
dataColumns: dataColumnSidecars.length,
Expand Down Expand Up @@ -768,7 +767,6 @@ export function getBeaconBlockApi({
builderIndex: envelope.builderIndex,
blockHash: blockHashHex,
blockRoot: blockRootHex,
stateRoot: stateRootHex,
});

const sentPeersArr = await publishPromise;
Expand Down
4 changes: 0 additions & 4 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1648,10 +1648,6 @@ export function getValidatorApi(
executionRequests: executionRequests,
builderIndex: BUILDER_INDEX_SELF_BUILD,
beaconBlockRoot,
slot,
// TODO GLOAS: stateRoot is no longer computed during block production.
// This field will be removed when we implement defer payload processing
stateRoot: ZERO_HASH,
};

logger.info("Produced execution payload envelope", {
Expand Down
149 changes: 58 additions & 91 deletions packages/beacon-node/src/chain/blocks/importExecutionPayload.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {routes} from "@lodestar/api";
import {ExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice";
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {getExecutionPayloadEnvelopeSignatureSet, isStatePostGloas} from "@lodestar/state-transition";
import {byteArrayEquals, fromHex, toRootHex} from "@lodestar/utils";
import {isStatePostGloas} from "@lodestar/state-transition";
import {verifyExecutionPayloadEnvelope, verifyExecutionPayloadEnvelopeSignature} from "./verifyExecutionPayloadEnvelope.js";
import {fromHex} from "@lodestar/utils";
import {ExecutionPayloadStatus} from "../../execution/index.js";
import {isQueueErrorAborted} from "../../util/queue/index.js";
import {BeaconChain} from "../chain.js";
Expand Down Expand Up @@ -69,18 +69,20 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe
/**
* Import an execution payload envelope after all data is available.
*
* This function:
* 1. Emits `execution_payload_available` if payload is for current slot
* 2. Gets the ProtoBlock from fork choice
* 3. Applies write-queue backpressure (waitForSpace) early, before verification
* 4. Regenerates the block state
* 5. Runs EL verification (notifyNewPayload) in parallel with signature verification and processExecutionPayloadEnvelope
* 6. Persists verified payload envelope to hot DB
* 7. Updates fork choice
* 8. Caches the post-execution payload state
* 9. Records metrics for column sources
* 10. Emits `execution_payload` for recent enough payloads after successful import
* With deferred processing (consensus-specs#5094), the envelope is purely verified
* here — no state mutation. State effects are applied in the next block via
* processParentExecutionPayload.
*
* Steps:
* 1. Emit `execution_payload_available` for payload attestation
* 2. Get the ProtoBlock from fork choice
* 3. Apply write-queue backpressure
* 4. Regenerate block state for envelope field validation
* 5. Run EL verification and signature verification in parallel, plus pure envelope verification
* 6. Persist verified payload envelope to hot DB
* 7. Update fork choice (no stateRoot — FULL shares PENDING's stateRoot)
* 8. Record metrics
* 9. Emit `execution_payload` event
*/
export async function importExecutionPayload(
this: BeaconChain,
Expand All @@ -92,15 +94,12 @@ export async function importExecutionPayload(
const envelope = signedEnvelope.message;
const blockRootHex = payloadInput.blockRootHex;
const blockHashHex = payloadInput.getBlockHashHex();
const fork = this.config.getForkName(envelope.slot);
const fork = this.config.getForkName(envelope.payload.slotNumber);

// 1. Emit `execution_payload_available` event at the start of import. At this point the payload input
// is already complete, so the payload and required data are available for payload attestation.
// This event is only about availability, not validity of the execution payload, hence we can emit
// it before getting a response from the execution client on whether the payload is valid or not.
if (this.clock.currentSlot - envelope.slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
// 1. Emit `execution_payload_available` event at the start of import
if (this.clock.currentSlot - envelope.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
this.emitter.emit(routes.events.EventType.executionPayloadAvailable, {
slot: envelope.slot,
slot: envelope.payload.slotNumber,
blockRoot: blockRootHex,
});
}
Expand Down Expand Up @@ -138,7 +137,7 @@ export async function importExecutionPayload(
}

// 6. Run verification steps in parallel
const [execResult, signatureValid, postPayloadResult] = await Promise.all([
const [execResult, signatureValid] = await Promise.all([
this.executionEngine.notifyNewPayload(
fork,
envelope.payload,
Expand All @@ -149,45 +148,39 @@ export async function importExecutionPayload(

opts.validSignature === true
? Promise.resolve(true)
: (async () => {
const signatureSet = getExecutionPayloadEnvelopeSignatureSet(
this.config,
this.pubkeyCache,
blockState,
signedEnvelope,
payloadInput.proposerIndex
);
return this.bls.verifySignatureSets([signatureSet]);
})(),

// Signature verified separately above.
// State root check is done separately below with better error typing (matching block pipeline pattern).
(async () => {
try {
return {
postPayloadState: blockState.processExecutionPayloadEnvelope(signedEnvelope, {
verifySignature: false,
verifyStateRoot: false,
}),
};
} catch (e) {
throw new PayloadError(
{
code: PayloadErrorCode.STATE_TRANSITION_ERROR,
message: (e as Error).message,
},
`State transition error: ${(e as Error).message}`
);
}
})(),
: verifyExecutionPayloadEnvelopeSignature(
this.config,
blockState,
this.pubkeyCache,
signedEnvelope,
payloadInput.proposerIndex,
this.bls
),
]);

// 5a. Check signature verification result
// 5a. Verify envelope fields against state (spec: verify_execution_payload_envelope)
try {
// When validSignature is true, the envelope came from gossip/API where both
// signature and executionRequestsRoot were already verified — skip re-hashing
verifyExecutionPayloadEnvelope(this.config, blockState, envelope, {
verifyExecutionRequestsRoot: !opts.validSignature,
});
} catch (e) {
throw new PayloadError(
{
code: PayloadErrorCode.STATE_TRANSITION_ERROR,
message: (e as Error).message,
},
`Envelope verification error: ${(e as Error).message}`
);
}

// 5b. Check signature verification result
if (!signatureValid) {
throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE});
}

// 5b. Handle EL response
// 5c. Handle EL response
switch (execResult.status) {
case ExecutionPayloadStatus.VALID:
break;
Expand All @@ -213,69 +206,43 @@ export async function importExecutionPayload(
});
}

// 5c. Verify envelope state root matches post-state
const postPayloadState = postPayloadResult.postPayloadState;
const postPayloadStateRoot = postPayloadState.hashTreeRoot();
if (!byteArrayEquals(envelope.stateRoot, postPayloadStateRoot)) {
throw new PayloadError({
code: PayloadErrorCode.STATE_TRANSITION_ERROR,
message: `Envelope state root mismatch expected=${toRootHex(envelope.stateRoot)} actual=${toRootHex(postPayloadStateRoot)}`,
});
}

// 6. Persist payload envelope to hot DB (performed asynchronously to avoid blocking)
// 6. Persist payload envelope to hot DB
this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => {
if (!isQueueErrorAborted(e)) {
this.logger.error(
"Error pushing payload envelope to unfinalized write queue",
{slot: envelope.slot, blockRoot: blockRootHex},
{slot: envelope.payload.slotNumber, blockRoot: blockRootHex},
e as Error
);
}
});

// 7. Update fork choice
this.forkChoice.onExecutionPayload(
blockRootHex,
blockHashHex,
envelope.payload.blockNumber,
toRootHex(postPayloadStateRoot),
toForkChoiceExecutionStatus(execResult.status)
);

// 8. Cache payload state
this.regen.processState(blockRootHex, postPayloadState);
if (postPayloadState.slot % SLOTS_PER_EPOCH === 0) {
const {checkpoint} = postPayloadState.computeAnchorCheckpoint();
this.regen.addCheckpointState(checkpoint, postPayloadState);
}
// 7. Update fork choice — no separate stateRoot since envelope doesn't produce post-state
const execStatus = toForkChoiceExecutionStatus(execResult.status);
this.forkChoice.onExecutionPayload(blockRootHex, blockHashHex, envelope.payload.blockNumber, execStatus);

// 9. Record metrics for payload envelope and column sources
// 8. Record metrics for payload envelope and column sources
this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source});
for (const {source} of payloadInput.getSampledColumnsWithSource()) {
this.metrics?.importPayload.columnsBySource.inc({source});
}

const stateRootHex = toRootHex(envelope.stateRoot);

// 10. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads
if (this.clock.currentSlot - envelope.slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
// 9. Emit event after payload is fully verified and imported to fork choice
if (this.clock.currentSlot - envelope.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
this.emitter.emit(routes.events.EventType.executionPayload, {
slot: envelope.slot,
slot: envelope.payload.slotNumber,
builderIndex: envelope.builderIndex,
blockHash: blockHashHex,
blockRoot: blockRootHex,
stateRoot: stateRootHex,
// TODO GLOAS: revisit once we support optimistic import
executionOptimistic: false,
});
}

this.logger.verbose("Execution payload imported", {
slot: envelope.slot,
slot: envelope.payload.slotNumber,
builderIndex: envelope.builderIndex,
blockRoot: blockRootHex,
blockHash: blockHashHex,
stateRoot: stateRootHex,
});
}
Loading
Loading