Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
27a2075
feat: add cached PTC window to the state
nflaig Apr 13, 2026
e80c801
Merge branch 'unstable' into nflaig/alpha4-spec-4979
ensi321 Apr 16, 2026
613aa40
Update specref
ensi321 Apr 16, 2026
42c3077
Merge branch 'unstable' into nflaig/alpha4-spec-4979
ensi321 Apr 17, 2026
f34a278
feat: add EIP-7928 BlockAccessList to gloas ExecutionPayload
ensi321 Apr 17, 2026
9058ad0
feat: add EIP-7928 blockAccessList to engine API serialization
ensi321 Apr 17, 2026
8120218
feat: bump engine API versions
ensi321 Apr 17, 2026
bbe17c7
Apply suggestion from @nflaig
ensi321 Apr 17, 2026
a1cb951
update comment
ensi321 Apr 17, 2026
9bc6eb6
feat: implement should_apply_proposer_boost for gloas
ensi321 Apr 18, 2026
4531a52
Skip test
ensi321 Apr 18, 2026
5517f57
Shift to cache as primary approach
ensi321 Apr 19, 2026
000b21c
Address @nflaig's comment
ensi321 Apr 19, 2026
06d23f3
lint
ensi321 Apr 20, 2026
b4e9577
Revert changes to initializeBeaconStateFromEth1
ensi321 Apr 20, 2026
c4282b9
Revert changes to sanity test
ensi321 Apr 20, 2026
1332882
init
ensi321 Apr 15, 2026
aaa16f6
refactor: revert dual-state from block import, block production, and …
ensi321 Apr 15, 2026
bf29eba
Clean up
ensi321 Apr 15, 2026
bbb5929
refactor: update Gloas SSZ types for deferred payload processing (spe…
ensi321 Apr 15, 2026
644d7f3
feat: add processParentExecutionPayload and remove withdrawal early r…
ensi321 Apr 15, 2026
68457ea
refactor: add processParentExecutionPayload as first step in processB…
ensi321 Apr 15, 2026
9a4a2c9
refactor: transform processExecutionPayloadEnvelope to pure verification
ensi321 Apr 15, 2026
7b851dd
refactor: remove executionPayloadStateRoot from fork choice onExecuti…
ensi321 Apr 15, 2026
5fbb0bc
refactor: simplify envelope import pipeline for deferred processing
ensi321 Apr 15, 2026
067edb1
feat: block production, gossip validation, and cleanup for deferred p…
ensi321 Apr 15, 2026
a3385bd
fix: type errors in processParentExecutionPayload and produceBlockBody
ensi321 Apr 15, 2026
e2c96a5
feat: range sync envelope support for Gloas (supersedes #9155)
ensi321 Apr 15, 2026
da8f5bd
feat: unknown payload sync for incoming blocks (supersedes #9102)
ensi321 Apr 15, 2026
c360e70
feat: Gloas DataColumnSidecar validation in range sync
ensi321 Apr 15, 2026
4474255
fix: guard payload-seen check on known block and use seenPayloadEnvelope
ensi321 Apr 16, 2026
4923d10
Update comments
ensi321 Apr 14, 2026
78ca807
Update comments
ensi321 Apr 14, 2026
190fd13
Fix spec test
ensi321 Apr 16, 2026
f5821e9
Address comments & follow up on spec change
ensi321 Apr 16, 2026
b5f24e4
Swap latestBlockHash and latestExecutionPayloadBid
ensi321 Apr 17, 2026
fecfcb3
fix upgrade state
ensi321 Apr 17, 2026
397aaf9
Bump version, skip fast_confirmation
ensi321 Apr 20, 2026
bf2747d
feat: implement EIP-7843 slot_number in ExecutionPayload (specs#4840)
ensi321 Apr 20, 2026
0e493ef
merge
ensi321 Apr 20, 2026
9afce2e
Merge branch 'nc/should-apply-proposer-boost' into nc/alpha.5-vibe
ensi321 Apr 20, 2026
984d50d
fix spec test
ensi321 Apr 20, 2026
2a3586e
feat: cache the last 2 PayloadEnvelopeInputs
twoeths Apr 21, 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
157 changes: 60 additions & 97 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 @@ -68,18 +68,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 @@ -90,15 +92,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 All @@ -112,12 +111,10 @@ export async function importExecutionPayload(
});
}

// 3. Apply backpressure from the write queue early, before doing verification work.
// The actual DB write is deferred until after verification succeeds.
// 3. Apply backpressure from the write queue early, before doing verification work
await this.unfinalizedPayloadEnvelopeWrites.waitForSpace();

// 4. Get pre-state for processExecutionPayloadEnvelope
// We need the block state (post-block, pre-payload) to process the envelope
// 4. Get block state for envelope field validation
const blockState = await this.regen.getBlockSlotState(
protoBlock,
protoBlock.slot,
Expand All @@ -132,9 +129,7 @@ export async function importExecutionPayload(
}

// 5. Run verification steps in parallel
// Note: No data availability check needed here - importExecutionPayload is only
// called when payloadInput.isComplete() is true, so all data is already available.
const [execResult, signatureValid, postPayloadResult] = await Promise.all([
const [execResult, signatureValid] = await Promise.all([
this.executionEngine.notifyNewPayload(
fork,
envelope.payload,
Expand All @@ -145,45 +140,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 @@ -209,69 +198,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