From a79196404049569b42686ab18c047ed45489a4c9 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:10:15 -0700 Subject: [PATCH 01/61] feat: implement EIP-7843 slot_number in ExecutionPayload (specs#4840) Move slot from ExecutionPayloadEnvelope to ExecutionPayload as slotNumber (uint64) and add slotNumber to PayloadAttributes per consensus-specs#4840. Updates all verification, gossip validation, signing, serialization, and SSZ byte extraction accordingly. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/src/beacon/routes/beacon/block.ts | 4 +- .../src/api/impl/beacon/blocks/index.ts | 2 +- .../src/api/impl/validator/index.ts | 4 -- .../chain/blocks/importExecutionPayload.ts | 14 ++--- .../validation/executionPayloadEnvelope.ts | 16 ++--- .../executionPayloadEnvelopeArchive.ts | 2 +- .../beacon-node/src/execution/engine/mock.ts | 6 +- packages/beacon-node/src/network/network.ts | 2 +- .../src/network/processor/gossipHandlers.ts | 5 +- .../src/sync/utils/downloadByRange.ts | 60 ++++++++++++++++++- packages/beacon-node/src/util/sszBytes.ts | 27 ++++++++- .../test/unit/util/sszBytes.test.ts | 6 +- .../fork-choice/src/protoArray/protoArray.ts | 1 - .../signatureSets/executionPayloadEnvelope.ts | 2 +- packages/types/src/gloas/sszTypes.ts | 2 - packages/types/src/gloas/types.ts | 1 + .../validator/src/services/validatorStore.ts | 6 +- 17 files changed, 119 insertions(+), 41 deletions(-) diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index 6ac6377fa9d1..c4199f505bb2 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -606,7 +606,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { - const fork = config.getForkName(signedExecutionPayloadEnvelope.message.slot); + const fork = config.getForkName(signedExecutionPayloadEnvelope.message.payload.slotNumber); return { body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.toJson(signedExecutionPayloadEnvelope), headers: { @@ -621,7 +621,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { - const fork = config.getForkName(signedExecutionPayloadEnvelope.message.slot); + const fork = config.getForkName(signedExecutionPayloadEnvelope.message.payload.slotNumber); return { body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.serialize(signedExecutionPayloadEnvelope), headers: { diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index fa8e523d3f1c..b6b584b24ee2 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -651,7 +651,7 @@ 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); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 607479db091f..52a124759272 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -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", { diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index 613a9223f74f..662098ed67da 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -92,15 +92,15 @@ 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) { + 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, }); } @@ -228,7 +228,7 @@ export async function importExecutionPayload( 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 ); } @@ -259,9 +259,9 @@ export async function importExecutionPayload( 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) { + 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, @@ -272,7 +272,7 @@ export async function importExecutionPayload( } this.logger.verbose("Execution payload imported", { - slot: envelope.slot, + slot: envelope.payload.slotNumber, builderIndex: envelope.builderIndex, blockRoot: blockRootHex, blockHash: blockHashHex, diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index 256b5f567b7a..fc62d61cca86 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -53,7 +53,7 @@ async function validateExecutionPayloadEnvelope( throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN, blockRoot: blockRootHex, - slot: envelope.slot, + slot: payload.slotNumber, }); } @@ -65,13 +65,13 @@ async function validateExecutionPayloadEnvelope( }); } - // [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot -- i.e. validate that `envelope.slot >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)` + // [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot -- i.e. validate that `payload.slotNumber >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)` const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); - if (envelope.slot < finalizedSlot) { + if (payload.slotNumber < finalizedSlot) { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.BELONG_TO_FINALIZED_BLOCK, - envelopeSlot: envelope.slot, + envelopeSlot: payload.slotNumber, finalizedSlot, }); } @@ -80,11 +80,11 @@ async function validateExecutionPayloadEnvelope( // TODO GLOAS: implement this. Technically if we cannot get proto block from fork choice, // it is possible that the block didn't pass the validation - // [REJECT] `block.slot` equals `envelope.slot`. - if (block.slot !== envelope.slot) { + // [REJECT] `block.slot` equals `payload.slotNumber`. + if (block.slot !== payload.slotNumber) { throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { code: ExecutionPayloadEnvelopeErrorCode.SLOT_MISMATCH, - envelopeSlot: envelope.slot, + envelopeSlot: payload.slotNumber, blockSlot: block.slot, }); } @@ -114,7 +114,7 @@ async function validateExecutionPayloadEnvelope( throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.UNKNOWN_BLOCK_STATE, blockRoot: blockRootHex, - slot: envelope.slot, + slot: payload.slotNumber, }); }); if (!isStatePostGloas(blockState)) { diff --git a/packages/beacon-node/src/db/repositories/executionPayloadEnvelopeArchive.ts b/packages/beacon-node/src/db/repositories/executionPayloadEnvelopeArchive.ts index 5b282b1b1bd7..439f2de1f822 100644 --- a/packages/beacon-node/src/db/repositories/executionPayloadEnvelopeArchive.ts +++ b/packages/beacon-node/src/db/repositories/executionPayloadEnvelopeArchive.ts @@ -19,7 +19,7 @@ export class ExecutionPayloadEnvelopeArchiveRepository extends Repository).withdrawals = ssz.capella.Withdrawals.defaultValue(); } + if (ForkSeq[fork] >= ForkSeq.gloas && payloadAttributes.slotNumber != null) { + (executionPayload as gloas.ExecutionPayload).slotNumber = payloadAttributes.slotNumber; + } + this.preparingPayloads.set(payloadId, { executionPayload: serializeExecutionPayload(fork, executionPayload), blobsBundle: serializeBlobsBundle({ diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 623fca31039c..4cd24034df98 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -505,7 +505,7 @@ export class Network implements INetwork { } async publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise { - const epoch = computeEpochAtSlot(signedEnvelope.message.slot); + const epoch = computeEpochAtSlot(signedEnvelope.message.payload.slotNumber); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip( diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 32debf5db525..65d403ccaf18 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -1065,7 +1065,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand await validateGossipExecutionPayloadEnvelope(chain, signedEnvelope); } catch (e) { if (e instanceof ExecutionPayloadEnvelopeError) { - const {slot, beaconBlockRoot} = signedEnvelope.message; + const {beaconBlockRoot} = signedEnvelope.message; + const slot = signedEnvelope.message.payload.slotNumber; logger.debug("Gossip envelope has error", {slot, root: toRootHex(beaconBlockRoot), code: e.type.code}); if (e.type.code === ExecutionPayloadEnvelopeErrorCode.BLOCK_ROOT_UNKNOWN) { // TODO GLOAS: UnknownBlockSync to handle this @@ -1088,7 +1089,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand throw e; } - const slot = envelope.slot; + const slot = envelope.payload.slotNumber; const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime); metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec); chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.gossip, delaySec, signedEnvelope); diff --git a/packages/beacon-node/src/sync/utils/downloadByRange.ts b/packages/beacon-node/src/sync/utils/downloadByRange.ts index 8d554db255bd..17ae0c80eac1 100644 --- a/packages/beacon-node/src/sync/utils/downloadByRange.ts +++ b/packages/beacon-node/src/sync/utils/downloadByRange.ts @@ -1,6 +1,6 @@ import {ChainForkConfig} from "@lodestar/config"; import {ForkPostDeneb, ForkPostFulu, ForkPreFulu, isForkPostFulu} from "@lodestar/params"; -import {SignedBeaconBlock, Slot, deneb, fulu, phase0} from "@lodestar/types"; +import {SignedBeaconBlock, Slot, deneb, fulu, gloas, phase0} from "@lodestar/types"; import {LodestarError, Logger, byteArrayEquals, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils"; import { BlockInputSource, @@ -882,6 +882,9 @@ export enum DownloadByRangeErrorCode { /** Cached block input type mismatches new data */ MISMATCH_BLOCK_FORK = "DOWNLOAD_BY_RANGE_ERROR_MISMATCH_BLOCK_FORK", MISMATCH_BLOCK_INPUT_TYPE = "DOWNLOAD_BY_RANGE_ERROR_MISMATCH_BLOCK_INPUT_TYPE", + + /** Envelope's beaconBlockRoot does not match the block root */ + INVALID_ENVELOPE_BEACON_BLOCK_ROOT = "DOWNLOAD_BY_RANGE_ERROR_INVALID_ENVELOPE_BEACON_BLOCK_ROOT", } export type DownloadByRangeErrorType = @@ -973,6 +976,61 @@ export type DownloadByRangeErrorType = blockRoot: string; expected: DAType; actual: DAType; + } + | { + code: DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT; + slot: Slot; + expected: string; + actual: string; }; export class DownloadByRangeError extends LodestarError {} + +/** + * Validates SignedExecutionPayloadEnvelopes received for a range request. + * For each envelope whose slot appears in the downloaded blocks, verifies that + * envelope.message.beaconBlockRoot matches the corresponding block's root. + * Envelopes for slots not in the batch (orphaned payloads) are silently ignored. + */ +export function validateEnvelopesByRangeResponse( + validatedBlocks: ValidatedBlock[], + batchBlocks: IBlockInput[] | undefined, + payloadEnvelopes: gloas.SignedExecutionPayloadEnvelope[] +): Map { + // Build a map of slot -> blockRoot for all blocks in the batch + const batchBlockRoots = new Map(); + if (batchBlocks) { + for (const blockInput of batchBlocks) { + batchBlockRoots.set(blockInput.slot, fromHex(blockInput.blockRootHex)); + } + } + for (const {block, blockRoot} of validatedBlocks) { + batchBlockRoots.set(block.message.slot, blockRoot); + } + + const payloadEnvelopeMap = new Map(); + + for (const payloadEnvelope of payloadEnvelopes) { + const slot = payloadEnvelope.message.payload.slotNumber; + const batchBlockRoot = batchBlockRoots.get(slot); + + // Envelopes for slots not in the batch are silently ignored (orphaned payloads) + if (batchBlockRoot === undefined) { + continue; + } + + // Verify beaconBlockRoot matches the block's root + if (!byteArrayEquals(payloadEnvelope.message.beaconBlockRoot, batchBlockRoot)) { + throw new DownloadByRangeError({ + code: DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT, + slot, + expected: toRootHex(batchBlockRoot), + actual: toRootHex(payloadEnvelope.message.beaconBlockRoot), + }); + } + + payloadEnvelopeMap.set(slot, payloadEnvelope); + } + + return payloadEnvelopeMap; +} diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index 725d2fa6954c..23f20ee80d55 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -560,8 +560,8 @@ export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Arr * ├─ 4 bytes: executionRequests offset * ├─ 8 bytes: builderIndex (offset 108-115) * ├─ 32 bytes: beaconBlockRoot (offset 116-147) - * ├─ 8 bytes: slot (offset 148-155) - * └─ 32 bytes: stateRoot (offset 156-187) + * └─ variable: payload data (starts at envelope + 48) + * └─ ExecutionPayload fixed portion includes slotNumber at offset 528 */ const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET = 4; const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE = 96; @@ -576,8 +576,29 @@ const BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE = EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET + EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE; // 116 +// Envelope fixed portion (without slot): payload_offset(4) + requests_offset(4) + builderIndex(8) + beaconBlockRoot(32) = 48 +const EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE = + EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET + + EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET + + EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE + + ROOT_SIZE; // 48 + +// slotNumber offset within ExecutionPayload fixed portion: +// parentHash(32) + feeRecipient(20) + stateRoot(32) + receiptsRoot(32) + logsBloom(256) + +// prevRandao(32) + blockNumber(8) + gasLimit(8) + gasUsed(8) + timestamp(8) + +// extraData_offset(4) + baseFeePerGas(32) + blockHash(32) + transactions_offset(4) + +// withdrawals_offset(4) + blobGasUsed(8) + excessBlobGas(8) = 528 +const SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD = 528; + +// Payload data starts right after the envelope's fixed portion +const ENVELOPE_START_IN_SIGNED = + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET + + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE; // 100 + const SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE = - BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + ROOT_SIZE; // 148 + ENVELOPE_START_IN_SIGNED + + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 48 + 528 = 676 export function getSlotFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): Slot | null { if (data.length < SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + SLOT_SIZE) { diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index b58562abd8cd..2fa572d9eca3 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -571,7 +571,7 @@ describe("SignedExecutionPayloadEnvelope SSZ serialized picking", () => { for (const {slot, blockRoot} of testCases) { it(`slot=${slot}`, () => { const envelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue(); - envelope.message.slot = slot; + envelope.message.payload.slotNumber = slot; envelope.message.beaconBlockRoot = fromHex(blockRoot); const bytes = ssz.gloas.SignedExecutionPayloadEnvelope.serialize(envelope); @@ -581,8 +581,8 @@ describe("SignedExecutionPayloadEnvelope SSZ serialized picking", () => { } it("getSlotFromExecutionPayloadEnvelopeSerialized - invalid data", () => { - // Slot is at offset 148, need at least 156 bytes - const invalidSizes = [0, 50, 100, 155]; + // slotNumber is at offset 676 within the serialized payload, need at least 684 bytes + const invalidSizes = [0, 50, 100, 683]; for (const size of invalidSizes) { expect(getSlotFromExecutionPayloadEnvelopeSerialized(Buffer.alloc(size))).toBeNull(); } diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index 4d239d52e8db..0141a9034a56 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -127,7 +127,6 @@ export class ProtoArray { block.executionPayloadBlockHash, (block as {executionPayloadNumber?: number}).executionPayloadNumber ?? 0, block.stateRoot, - null, ExecutionStatus.Valid ); } diff --git a/packages/state-transition/src/signatureSets/executionPayloadEnvelope.ts b/packages/state-transition/src/signatureSets/executionPayloadEnvelope.ts index 806a18ff69f6..42855cf01c87 100644 --- a/packages/state-transition/src/signatureSets/executionPayloadEnvelope.ts +++ b/packages/state-transition/src/signatureSets/executionPayloadEnvelope.ts @@ -11,7 +11,7 @@ export function getExecutionPayloadEnvelopeSigningRoot( config: BeaconConfig, envelope: gloas.ExecutionPayloadEnvelope ): Uint8Array { - const domain = config.getDomain(envelope.slot, DOMAIN_BEACON_BUILDER); + const domain = config.getDomain(envelope.payload.slotNumber, DOMAIN_BEACON_BUILDER); return computeSigningRoot(ssz.gloas.ExecutionPayloadEnvelope, envelope, domain); } diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index 329a359177a3..ffcd6e771d3d 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -180,8 +180,6 @@ export const ExecutionPayloadEnvelope = new ContainerType( executionRequests: electraSsz.ExecutionRequests, builderIndex: BuilderIndex, beaconBlockRoot: Root, - slot: Slot, - stateRoot: Root, }, {typeName: "ExecutionPayloadEnvelope", jsonCase: "eth2"} ); diff --git a/packages/types/src/gloas/types.ts b/packages/types/src/gloas/types.ts index 114900031aa4..81e2a978ecc4 100644 --- a/packages/types/src/gloas/types.ts +++ b/packages/types/src/gloas/types.ts @@ -12,6 +12,7 @@ export type PayloadAttestationMessage = ValueOf; export type ProposerPreferences = ValueOf; export type SignedProposerPreferences = ValueOf; +export type ExecutionPayload = ValueOf; export type ExecutionPayloadBid = ValueOf; export type SignedExecutionPayloadBid = ValueOf; export type BlockAccessList = ValueOf; diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 4716d3498756..ea84e9f15833 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -496,11 +496,11 @@ export class ValidatorStore { logger?: LoggerVc ): Promise { // Make sure the envelope slot is not higher than the current slot to avoid potential attacks. - if (envelope.slot > currentSlot) { - throw Error(`Not signing envelope with slot ${envelope.slot} greater than current slot ${currentSlot}`); + if (envelope.payload.slotNumber > currentSlot) { + throw Error(`Not signing envelope with slot ${envelope.payload.slotNumber} greater than current slot ${currentSlot}`); } - const signingSlot = envelope.slot; + const signingSlot = envelope.payload.slotNumber; const domain = this.config.getDomain(signingSlot, DOMAIN_BEACON_BUILDER); const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadEnvelope, envelope, domain); From 3958681a50212aa082f167c5585db3291ad565f7 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:31:15 -0700 Subject: [PATCH 02/61] Revert changes introduced by accident --- .../src/sync/utils/downloadByRange.ts | 60 +------------------ 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/packages/beacon-node/src/sync/utils/downloadByRange.ts b/packages/beacon-node/src/sync/utils/downloadByRange.ts index 17ae0c80eac1..8d554db255bd 100644 --- a/packages/beacon-node/src/sync/utils/downloadByRange.ts +++ b/packages/beacon-node/src/sync/utils/downloadByRange.ts @@ -1,6 +1,6 @@ import {ChainForkConfig} from "@lodestar/config"; import {ForkPostDeneb, ForkPostFulu, ForkPreFulu, isForkPostFulu} from "@lodestar/params"; -import {SignedBeaconBlock, Slot, deneb, fulu, gloas, phase0} from "@lodestar/types"; +import {SignedBeaconBlock, Slot, deneb, fulu, phase0} from "@lodestar/types"; import {LodestarError, Logger, byteArrayEquals, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils"; import { BlockInputSource, @@ -882,9 +882,6 @@ export enum DownloadByRangeErrorCode { /** Cached block input type mismatches new data */ MISMATCH_BLOCK_FORK = "DOWNLOAD_BY_RANGE_ERROR_MISMATCH_BLOCK_FORK", MISMATCH_BLOCK_INPUT_TYPE = "DOWNLOAD_BY_RANGE_ERROR_MISMATCH_BLOCK_INPUT_TYPE", - - /** Envelope's beaconBlockRoot does not match the block root */ - INVALID_ENVELOPE_BEACON_BLOCK_ROOT = "DOWNLOAD_BY_RANGE_ERROR_INVALID_ENVELOPE_BEACON_BLOCK_ROOT", } export type DownloadByRangeErrorType = @@ -976,61 +973,6 @@ export type DownloadByRangeErrorType = blockRoot: string; expected: DAType; actual: DAType; - } - | { - code: DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT; - slot: Slot; - expected: string; - actual: string; }; export class DownloadByRangeError extends LodestarError {} - -/** - * Validates SignedExecutionPayloadEnvelopes received for a range request. - * For each envelope whose slot appears in the downloaded blocks, verifies that - * envelope.message.beaconBlockRoot matches the corresponding block's root. - * Envelopes for slots not in the batch (orphaned payloads) are silently ignored. - */ -export function validateEnvelopesByRangeResponse( - validatedBlocks: ValidatedBlock[], - batchBlocks: IBlockInput[] | undefined, - payloadEnvelopes: gloas.SignedExecutionPayloadEnvelope[] -): Map { - // Build a map of slot -> blockRoot for all blocks in the batch - const batchBlockRoots = new Map(); - if (batchBlocks) { - for (const blockInput of batchBlocks) { - batchBlockRoots.set(blockInput.slot, fromHex(blockInput.blockRootHex)); - } - } - for (const {block, blockRoot} of validatedBlocks) { - batchBlockRoots.set(block.message.slot, blockRoot); - } - - const payloadEnvelopeMap = new Map(); - - for (const payloadEnvelope of payloadEnvelopes) { - const slot = payloadEnvelope.message.payload.slotNumber; - const batchBlockRoot = batchBlockRoots.get(slot); - - // Envelopes for slots not in the batch are silently ignored (orphaned payloads) - if (batchBlockRoot === undefined) { - continue; - } - - // Verify beaconBlockRoot matches the block's root - if (!byteArrayEquals(payloadEnvelope.message.beaconBlockRoot, batchBlockRoot)) { - throw new DownloadByRangeError({ - code: DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT, - slot, - expected: toRootHex(batchBlockRoot), - actual: toRootHex(payloadEnvelope.message.beaconBlockRoot), - }); - } - - payloadEnvelopeMap.set(slot, payloadEnvelope); - } - - return payloadEnvelopeMap; -} From a912bc84d18e65390d07e69a3f2cb963e775ddce Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:52:10 -0700 Subject: [PATCH 03/61] Bump version, skip fast_confirmation --- packages/api/test/unit/beacon/testData/events.ts | 2 -- packages/beacon-node/test/spec/utils/specTestIterator.ts | 2 +- spec-tests-version.json | 2 +- specrefs/.ethspecify.yml | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index 8de345a96411..a6c8c06ad055 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -279,7 +279,6 @@ export const eventTestData: EventData = { builderIndex: 42, blockHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", blockRoot: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf", - stateRoot: "0x600e852a08c1200654ddf11025f1ceacb3c2e74bdd5c630cde0838b2591b69f9", executionOptimistic: false, }, [EventType.executionPayloadGossip]: { @@ -287,7 +286,6 @@ export const eventTestData: EventData = { builderIndex: 42, blockHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", blockRoot: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf", - stateRoot: "0x600e852a08c1200654ddf11025f1ceacb3c2e74bdd5c630cde0838b2591b69f9", }, [EventType.executionPayloadAvailable]: { slot: 10, diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 1616cdf5040b..ce6b66f452db 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -87,7 +87,7 @@ export const defaultSkipOpts: SkipOpts = { /^gloas\/fork_choice\/on_execution_payload\/.*$/, ], skippedTests: [], - skippedRunners: [], + skippedRunners: ["fast_confirmation"], }; /** diff --git a/spec-tests-version.json b/spec-tests-version.json index 8e29f2192c02..64560a60b747 100644 --- a/spec-tests-version.json +++ b/spec-tests-version.json @@ -1,6 +1,6 @@ { "ethereumConsensusSpecsTests": { - "specVersion": "v1.7.0-alpha.4", + "specVersion": "v1.7.0-alpha.5", "specTestsRepoUrl": "https://github.com/ethereum/consensus-specs", "outputDirBase": "spec-tests", "testsToDownload": [ diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml index 41b05a145e51..9b773fc0fcf6 100644 --- a/specrefs/.ethspecify.yml +++ b/specrefs/.ethspecify.yml @@ -1,4 +1,4 @@ -version: v1.7.0-alpha.4 +version: v1.7.0-alpha.5 style: full specrefs: From 03c43490d2a8758c4b51a37b323f09aa4e19809d Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:58:40 -0700 Subject: [PATCH 04/61] Rest of the changes --- packages/api/src/beacon/routes/events.ts | 4 ---- .../src/api/impl/beacon/blocks/index.ts | 3 --- .../src/chain/blocks/importExecutionPayload.ts | 14 ++------------ .../src/chain/produceBlock/produceBlockBody.ts | 2 ++ .../src/network/processor/gossipHandlers.ts | 1 - packages/fork-choice/src/protoArray/protoArray.ts | 1 + .../src/block/processExecutionPayloadEnvelope.ts | 11 +++-------- packages/types/src/gloas/sszTypes.ts | 6 ++++-- packages/types/src/gloas/types.ts | 1 - packages/validator/src/services/block.ts | 3 +-- 10 files changed, 13 insertions(+), 33 deletions(-) diff --git a/packages/api/src/beacon/routes/events.ts b/packages/api/src/beacon/routes/events.ts index 9c2a28458684..e631f0ced8bd 100644 --- a/packages/api/src/beacon/routes/events.ts +++ b/packages/api/src/beacon/routes/events.ts @@ -186,7 +186,6 @@ export type EventData = { builderIndex: BuilderIndex; blockHash: RootHex; blockRoot: RootHex; - stateRoot: RootHex; executionOptimistic: boolean; }; [EventType.executionPayloadGossip]: { @@ -194,7 +193,6 @@ export type EventData = { builderIndex: BuilderIndex; blockHash: RootHex; blockRoot: RootHex; - stateRoot: RootHex; }; [EventType.executionPayloadAvailable]: { slot: Slot; @@ -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"} @@ -387,7 +384,6 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type builderIndex: ssz.BuilderIndex, blockHash: stringType, blockRoot: stringType, - stateRoot: stringType, }, {jsonCase: "eth2"} ), diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index b6b584b24ee2..73bf3b4caab7 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -655,7 +655,6 @@ export function getBeaconBlockApi({ const fork = config.getForkName(slot); const blockRootHex = toRootHex(envelope.beaconBlockRoot); const blockHashHex = toRootHex(envelope.payload.blockHash); - const stateRootHex = toRootHex(envelope.stateRoot); if (!isForkPostGloas(fork)) { throw new ApiError(400, `publishExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`); @@ -740,7 +739,6 @@ export function getBeaconBlockApi({ slot, blockRoot: blockRootHex, blockHash: blockHashHex, - stateRoot: stateRootHex, builderIndex: envelope.builderIndex, isSelfBuild, dataColumns: dataColumnSidecars.length, @@ -768,7 +766,6 @@ export function getBeaconBlockApi({ builderIndex: envelope.builderIndex, blockHash: blockHashHex, blockRoot: blockRootHex, - stateRoot: stateRootHex, }); const sentPeersArr = await publishPromise; diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index 662098ed67da..c25f83329a9b 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -2,7 +2,7 @@ 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 {fromHex, toRootHex} from "@lodestar/utils"; import {ExecutionPayloadStatus} from "../../execution/index.js"; import {isQueueErrorAborted} from "../../util/queue/index.js"; import {BeaconChain} from "../chain.js"; @@ -213,15 +213,9 @@ export async function importExecutionPayload( }); } - // 5c. Verify envelope state root matches post-state + // 5c. Compute post-payload state root 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) this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => { @@ -256,8 +250,6 @@ export async function importExecutionPayload( 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.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { this.emitter.emit(routes.events.EventType.executionPayload, { @@ -265,7 +257,6 @@ export async function importExecutionPayload( builderIndex: envelope.builderIndex, blockHash: blockHashHex, blockRoot: blockRootHex, - stateRoot: stateRootHex, // TODO GLOAS: revisit once we support optimistic import executionOptimistic: false, }); @@ -276,6 +267,5 @@ export async function importExecutionPayload( builderIndex: envelope.builderIndex, blockRoot: blockRootHex, blockHash: blockHashHex, - stateRoot: stateRootHex, }); } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index e8004ab94519..a2edacdf77b5 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -46,6 +46,7 @@ import { electra, fulu, gloas, + ssz, } from "@lodestar/types"; import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {ZERO_HASH_HEX} from "../../constants/index.js"; @@ -269,6 +270,7 @@ export async function produceBlockBody( value: 0, executionPayment: 0, blobKzgCommitments: blobsBundle.commitments, + executionRequestsRoot: ssz.electra.ExecutionRequests.hashTreeRoot(executionRequests), }; const signedBid: gloas.SignedExecutionPayloadBid = { message: bid, diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 65d403ccaf18..b9607e77f9d5 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -1119,7 +1119,6 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand builderIndex: envelope.builderIndex, blockHash: toRootHex(envelope.payload.blockHash), blockRoot: blockRootHex, - stateRoot: toRootHex(envelope.stateRoot), }); chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => { diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index 0141a9034a56..4d239d52e8db 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -127,6 +127,7 @@ export class ProtoArray { block.executionPayloadBlockHash, (block as {executionPayloadNumber?: number}).executionPayloadNumber ?? 0, block.stateRoot, + null, ExecutionStatus.Valid ); } diff --git a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts index c7f858a908d0..b410c0a87fa3 100644 --- a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts +++ b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts @@ -27,7 +27,7 @@ export function processExecutionPayloadEnvelope( const {verifySignature = true, verifyStateRoot = true} = opts ?? {}; const envelope = signedEnvelope.message; const payload = envelope.payload; - const fork = state.config.getForkSeq(envelope.slot); + const fork = state.config.getForkSeq(payload.slotNumber); if (verifySignature && !verifyExecutionPayloadEnvelopeSignature(state, signedEnvelope)) { throw Error(`Execution payload envelope has invalid signature builderIndex=${envelope.builderIndex}`); @@ -74,11 +74,6 @@ export function processExecutionPayloadEnvelope( postState.commit(); - if (verifyStateRoot && !byteArrayEquals(envelope.stateRoot, postState.hashTreeRoot())) { - throw new Error( - `Envelope's state root does not match state envelope=${toRootHex(envelope.stateRoot)} state=${toRootHex(postState.hashTreeRoot())}` - ); - } return postState; } @@ -102,8 +97,8 @@ function validateExecutionPayloadEnvelope( ); } - if (envelope.slot !== state.slot) { - throw new Error(`Slot mismatch between envelope and state envelope=${envelope.slot} state=${state.slot}`); + if (payload.slotNumber !== state.slot) { + throw new Error(`Slot mismatch between payload and state payload=${payload.slotNumber} state=${state.slot}`); } // Verify consistency with the committed bid diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index ffcd6e771d3d..6f90dfc04e02 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -151,6 +151,7 @@ export const ExecutionPayloadBid = new ContainerType( value: UintNum64, executionPayment: UintNum64, blobKzgCommitments: denebSsz.BlobKzgCommitments, + executionRequestsRoot: Root, }, {typeName: "ExecutionPayloadBid", jsonCase: "eth2"} ); @@ -209,6 +210,7 @@ export const BeaconBlockBody = new ContainerType( // executionRequests: ExecutionRequests, // Removed in GLOAS:EIP7732 signedExecutionPayloadBid: SignedExecutionPayloadBid, // New in GLOAS:EIP7732 payloadAttestations: new ListCompositeType(PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS), // New in GLOAS:EIP7732 + parentExecutionRequests: electraSsz.ExecutionRequests, }, {typeName: "BeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -266,7 +268,7 @@ export const BeaconState = new ContainerType( nextSyncCommittee: altairSsz.SyncCommittee, // Execution // latestExecutionPayloadHeader: ExecutionPayloadHeader, // Removed in GLOAS:EIP7732 - latestExecutionPayloadBid: ExecutionPayloadBid, // New in GLOAS:EIP7732 + latestBlockHash: Bytes32, // Withdrawals nextWithdrawalIndex: capellaSsz.BeaconState.fields.nextWithdrawalIndex, nextWithdrawalValidatorIndex: capellaSsz.BeaconState.fields.nextWithdrawalValidatorIndex, @@ -287,7 +289,7 @@ export const BeaconState = new ContainerType( executionPayloadAvailability: new BitVectorType(SLOTS_PER_HISTORICAL_ROOT), // New in GLOAS:EIP7732 builderPendingPayments: new VectorCompositeType(BuilderPendingPayment, 2 * SLOTS_PER_EPOCH), // New in GLOAS:EIP7732 builderPendingWithdrawals: new ListCompositeType(BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT), // New in GLOAS:EIP7732 - latestBlockHash: Bytes32, // New in GLOAS:EIP7732 + latestExecutionPayloadBid: ExecutionPayloadBid, payloadExpectedWithdrawals: capellaSsz.Withdrawals, // New in GLOAS:EIP7732 ptcWindow: PtcWindow, // New in GLOAS:EIP7732 }, diff --git a/packages/types/src/gloas/types.ts b/packages/types/src/gloas/types.ts index 81e2a978ecc4..de7949e54651 100644 --- a/packages/types/src/gloas/types.ts +++ b/packages/types/src/gloas/types.ts @@ -16,7 +16,6 @@ export type ExecutionPayload = ValueOf; export type ExecutionPayloadBid = ValueOf; export type SignedExecutionPayloadBid = ValueOf; export type BlockAccessList = ValueOf; -export type ExecutionPayload = ValueOf; export type ExecutionPayloadEnvelope = ValueOf; export type SignedExecutionPayloadEnvelope = ValueOf; export type BeaconBlockBody = ValueOf; diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index a3f198273d5e..ebe04f09971a 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -241,9 +241,8 @@ export class BlockProposingService { beaconBlockRoot, }); const envelope = envelopeRes.value(); - const stateRootHex = toRootHex(envelope.stateRoot); - this.logger.debug("Retrieved execution payload envelope", {...debugLogCtx, stateRoot: stateRootHex}); + this.logger.debug("Retrieved execution payload envelope", debugLogCtx); // Step 4: Sign and publish the envelope const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(pubkey, envelope, slot, this.logger); From d3d74a948005952ed77fe36950f605f5ab050181 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:16:22 -0700 Subject: [PATCH 05/61] Skip some tests --- .../test/spec/utils/specTestIterator.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index ce6b66f452db..0dd85ca17d18 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -77,17 +77,19 @@ export const defaultSkipOpts: SkipOpts = { // cell level DAS is ready /^fulu\/ssz_static\/PartialDataColumn(Header|PartsMetadata|Sidecar)\/.*$/, /^gloas\/ssz_static\/PartialDataColumn(Header|PartsMetadata|Sidecar)\/.*$/, - // TODO GLOAS: EIP-7928 (blockAccessList) and EIP-7843 (slotNumber) are added to gloas ExecutionPayload - // in Lodestar ahead of their integration into the consensus-specs gloas fork, so the - // upstream fixtures encode ExecutionPayload without these fields. Unskip once the spec tests include them. - /^gloas\/ssz_static\/ExecutionPayload\/.*$/, - /^gloas\/ssz_static\/ExecutionPayloadEnvelope\/.*$/, - /^gloas\/ssz_static\/SignedExecutionPayloadEnvelope\/.*$/, - /^gloas\/operations\/execution_payload\/.*$/, - /^gloas\/fork_choice\/on_execution_payload\/.*$/, + // TODO GLOAS: Unskip these tests as we get closer to alpha.5 ready + /^gloas\/fork_choice\/.*$/, + /^gloas\/fork\/.*$/, + /^gloas\/transition\/.*$/, + /^gloas\/operations\/parent_execution_payload\/.*$/, ], - skippedTests: [], - skippedRunners: ["fast_confirmation"], + skippedTests: [ + // TODO GLOAS: Unskip these tests as we get closer to alpha.5 ready + /^gloas\/sanity\/blocks\/pyspec_tests\/builder_payment_after_missed_epochs$/, + /^gloas\/operations\/withdrawals\/pyspec_tests\/zero_hash_genesis_skips_withdrawals$/, + ], + // TODO GLOAS: Investigate why networking tests are failing since alpha.5 + skippedRunners: ["fast_confirmation", "networking"], }; /** From 40b5f9f09c27c18d831cc48d61ee9f7d67a893b5 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:22:23 -0700 Subject: [PATCH 06/61] lint --- packages/beacon-node/src/util/sszBytes.ts | 7 ++----- packages/beacon-node/test/spec/presets/fork_choice.test.ts | 3 +-- .../chain/blocks/verifyPayloadsDataAvailability.test.ts | 2 -- packages/validator/src/services/validatorStore.ts | 4 +++- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index 23f20ee80d55..bb0d988a0ccb 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -592,13 +592,10 @@ const SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD = 528; // Payload data starts right after the envelope's fixed portion const ENVELOPE_START_IN_SIGNED = - SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET + - SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE; // 100 + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE; // 100 const SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE = - ENVELOPE_START_IN_SIGNED + - EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + - SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 48 + 528 = 676 + ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 48 + 528 = 676 export function getSlotFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): Slot | null { if (data.length < SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + SLOT_SIZE) { diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 4795a3979714..6988c9ba279c 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -381,7 +381,6 @@ const forkChoiceTest = const beaconBlockRoot = toHex(envelope.message.beaconBlockRoot); const blockHash = toHex(envelope.message.payload.blockHash); const blockNumber = envelope.message.payload.blockNumber; - const stateRoot = toHex(envelope.message.stateRoot); // Add predefined VALID status for the payload's block hash so the EL mock accepts it executionEngineBackend.addPredefinedPayloadStatus(blockHash, { @@ -394,7 +393,7 @@ const forkChoiceTest = beaconBlockRoot, blockHash, blockNumber, - stateRoot, + "0x", ExecutionStatus.Valid ); if (!isValid) throw Error("Expect error since this is a negative test"); diff --git a/packages/beacon-node/test/unit/chain/blocks/verifyPayloadsDataAvailability.test.ts b/packages/beacon-node/test/unit/chain/blocks/verifyPayloadsDataAvailability.test.ts index d42b42bb34b8..8eda8ccb43ed 100644 --- a/packages/beacon-node/test/unit/chain/blocks/verifyPayloadsDataAvailability.test.ts +++ b/packages/beacon-node/test/unit/chain/blocks/verifyPayloadsDataAvailability.test.ts @@ -33,7 +33,6 @@ function buildPayloadEnvelopeInput({blobCount, sampledColumns}: {blobCount: numb const signedEnvelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue(); signedEnvelope.message.beaconBlockRoot = blockRoot; - signedEnvelope.message.slot = block.message.slot; payloadInput.addPayloadEnvelope({ envelope: signedEnvelope, @@ -177,7 +176,6 @@ describe("PayloadEnvelopeInput.waitForEnvelopeAndAllData", () => { const signedEnvelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue(); signedEnvelope.message.beaconBlockRoot = blockRoot; - signedEnvelope.message.slot = block.message.slot; return {payloadInput, signedEnvelope}; } diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index ea84e9f15833..39eb8ddd44bb 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -497,7 +497,9 @@ export class ValidatorStore { ): Promise { // Make sure the envelope slot is not higher than the current slot to avoid potential attacks. if (envelope.payload.slotNumber > currentSlot) { - throw Error(`Not signing envelope with slot ${envelope.payload.slotNumber} greater than current slot ${currentSlot}`); + throw Error( + `Not signing envelope with slot ${envelope.payload.slotNumber} greater than current slot ${currentSlot}` + ); } const signingSlot = envelope.payload.slotNumber; From da7cc4172f9976cb7a560474364a0d075ce80d3f Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:38:39 -0700 Subject: [PATCH 07/61] ethspecify --- packages/state-transition/src/util/gloas.ts | 1 + packages/types/src/gloas/sszTypes.ts | 8 +- specrefs/.ethspecify.yml | 4 - specrefs/configs.yml | 7 + specrefs/constants.yml | 7 + specrefs/containers.yml | 56 +- specrefs/dataclasses.yml | 39 +- specrefs/functions.yml | 1462 +++++++++++++++---- specrefs/types.yml | 7 + 9 files changed, 1285 insertions(+), 306 deletions(-) diff --git a/packages/state-transition/src/util/gloas.ts b/packages/state-transition/src/util/gloas.ts index 6f1115697ed8..6f1989acda0b 100644 --- a/packages/state-transition/src/util/gloas.ts +++ b/packages/state-transition/src/util/gloas.ts @@ -172,6 +172,7 @@ export function isAttestationSameSlotRootCache(rootCache: RootCache, data: Attes return isMatchingBlockRoot && isCurrentBlockRoot; } +// TODO GLOAS: This function no longer exists in v1.7.0-alpha.5 specs. Remove it when appropriate to do so export function isParentBlockFull(state: CachedBeaconStateGloas): boolean { return byteArrayEquals(state.latestExecutionPayloadBid.blockHash, state.latestBlockHash); } diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index 6f90dfc04e02..59fd84ebb1ec 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -210,7 +210,7 @@ export const BeaconBlockBody = new ContainerType( // executionRequests: ExecutionRequests, // Removed in GLOAS:EIP7732 signedExecutionPayloadBid: SignedExecutionPayloadBid, // New in GLOAS:EIP7732 payloadAttestations: new ListCompositeType(PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS), // New in GLOAS:EIP7732 - parentExecutionRequests: electraSsz.ExecutionRequests, + parentExecutionRequests: electraSsz.ExecutionRequests, // New in GLOAS:EIP7732 }, {typeName: "BeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -268,7 +268,7 @@ export const BeaconState = new ContainerType( nextSyncCommittee: altairSsz.SyncCommittee, // Execution // latestExecutionPayloadHeader: ExecutionPayloadHeader, // Removed in GLOAS:EIP7732 - latestBlockHash: Bytes32, + latestBlockHash: Bytes32, // New in GLOAS:EIP7732 // Withdrawals nextWithdrawalIndex: capellaSsz.BeaconState.fields.nextWithdrawalIndex, nextWithdrawalValidatorIndex: capellaSsz.BeaconState.fields.nextWithdrawalValidatorIndex, @@ -289,12 +289,12 @@ export const BeaconState = new ContainerType( executionPayloadAvailability: new BitVectorType(SLOTS_PER_HISTORICAL_ROOT), // New in GLOAS:EIP7732 builderPendingPayments: new VectorCompositeType(BuilderPendingPayment, 2 * SLOTS_PER_EPOCH), // New in GLOAS:EIP7732 builderPendingWithdrawals: new ListCompositeType(BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT), // New in GLOAS:EIP7732 - latestExecutionPayloadBid: ExecutionPayloadBid, + latestExecutionPayloadBid: ExecutionPayloadBid, // New in GLOAS:EIP7732 payloadExpectedWithdrawals: capellaSsz.Withdrawals, // New in GLOAS:EIP7732 ptcWindow: PtcWindow, // New in GLOAS:EIP7732 }, {typeName: "BeaconState", jsonCase: "eth2"} -); +); // New in GLOAS:EIP7732 export const DataColumnSidecar = new ContainerType( { diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml index 9b773fc0fcf6..e40c5a2a1e9b 100644 --- a/specrefs/.ethspecify.yml +++ b/specrefs/.ethspecify.yml @@ -142,7 +142,6 @@ exceptions: - max_compressed_len#phase0 - max_message_size#phase0 - record_block_timeliness#phase0 - - saturating_sub#phase0 - seconds_to_milliseconds#phase0 - store_target_checkpoint_state#phase0 - update_proposer_boost_root#phase0 @@ -281,7 +280,6 @@ exceptions: # gloas - add_builder_to_registry#gloas - apply_withdrawals#gloas - - compute_balance_weighted_acceptance#gloas - compute_balance_weighted_selection#gloas - compute_fork_version#gloas - compute_proposer_indices#gloas @@ -317,7 +315,6 @@ exceptions: - is_valid_proposal_slot#gloas - notify_ptc_messages#gloas - on_block#gloas - - on_execution_payload#gloas - on_payload_attestation_message#gloas - prepare_execution_payload#gloas - process_attestation#gloas @@ -364,7 +361,6 @@ exceptions: - is_inclusion_list_bits_inclusive#heze - is_payload_inclusion_list_satisfied#heze - is_valid_inclusion_list_signature#heze - - on_execution_payload#heze - on_inclusion_list#heze - prepare_execution_payload#heze - process_inclusion_list#heze diff --git a/specrefs/configs.yml b/specrefs/configs.yml index c767599dfc58..9d582ecc38dc 100644 --- a/specrefs/configs.yml +++ b/specrefs/configs.yml @@ -170,6 +170,13 @@ CHURN_LIMIT_QUOTIENT: uint64 = 65536 +- name: CONFIRMATION_BYZANTINE_THRESHOLD#phase0 + sources: [] + spec: | + + CONFIRMATION_BYZANTINE_THRESHOLD: uint64 = 25 + + - name: CONTRIBUTION_DUE_BPS#altair sources: - file: packages/config/src/chainConfig/configs/mainnet.ts diff --git a/specrefs/constants.yml b/specrefs/constants.yml index 0d23d38d2be2..f326a95a0f6f 100644 --- a/specrefs/constants.yml +++ b/specrefs/constants.yml @@ -107,6 +107,13 @@ BYTES_PER_PROOF: uint64 = 48 +- name: COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR#phase0 + sources: [] + spec: | + + COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR: uint64 = 5 + + - name: COMPOUNDING_WITHDRAWAL_PREFIX#electra sources: - file: packages/params/src/index.ts diff --git a/specrefs/containers.yml b/specrefs/containers.yml index f4008da2944f..e674c487259b 100644 --- a/specrefs/containers.yml +++ b/specrefs/containers.yml @@ -244,7 +244,7 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const BeaconBlockBody = spec: | - + class BeaconBlockBody(Container): randao_reveal: BLSSignature eth1_data: Eth1Data @@ -266,6 +266,8 @@ signed_execution_payload_bid: SignedExecutionPayloadBid # [New in Gloas:EIP7732] payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] + # [New in Gloas:EIP7732] + parent_execution_requests: ExecutionRequests - name: BeaconBlockHeader#phase0 @@ -572,7 +574,7 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const BeaconState = spec: | - + class BeaconState(Container): genesis_time: uint64 genesis_validators_root: Root @@ -601,7 +603,7 @@ # [Modified in Gloas:EIP7732] # Removed `latest_execution_payload_header` # [New in Gloas:EIP7732] - latest_execution_payload_bid: ExecutionPayloadBid + latest_block_hash: Hash32 next_withdrawal_index: WithdrawalIndex next_withdrawal_validator_index: ValidatorIndex historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] @@ -626,7 +628,7 @@ # [New in Gloas:EIP7732] builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT] # [New in Gloas:EIP7732] - latest_block_hash: Hash32 + latest_execution_payload_bid: ExecutionPayloadBid # [New in Gloas:EIP7732] payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in Gloas:EIP7732] @@ -636,7 +638,7 @@ - name: BeaconState#heze sources: [] spec: | - + class BeaconState(Container): genesis_time: uint64 genesis_validators_root: Root @@ -662,8 +664,7 @@ inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] current_sync_committee: SyncCommittee next_sync_committee: SyncCommittee - # [Modified in Heze:EIP7805] - latest_execution_payload_bid: ExecutionPayloadBid + latest_block_hash: Hash32 next_withdrawal_index: WithdrawalIndex next_withdrawal_validator_index: ValidatorIndex historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] @@ -682,7 +683,8 @@ execution_payload_availability: Bitvector[SLOTS_PER_HISTORICAL_ROOT] builder_pending_payments: Vector[BuilderPendingPayment, 2 * SLOTS_PER_EPOCH] builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT] - latest_block_hash: Hash32 + # [Modified in Heze:EIP7805] + latest_execution_payload_bid: ExecutionPayloadBid payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] @@ -984,12 +986,40 @@ excess_blob_gas: uint64 +- name: ExecutionPayload#gloas + sources: [] + spec: | + + class ExecutionPayload(Container): + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + blob_gas_used: uint64 + excess_blob_gas: uint64 + # [New in Gloas:EIP7928] + block_access_list: BlockAccessList + # [New in Gloas:EIP7843] + slot_number: uint64 + + - name: ExecutionPayloadBid#gloas sources: - file: packages/types/src/gloas/sszTypes.ts search: export const ExecutionPayloadBid = spec: | - + class ExecutionPayloadBid(Container): parent_block_hash: Hash32 parent_block_root: Root @@ -1002,12 +1032,13 @@ value: Gwei execution_payment: Gwei blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + execution_requests_root: Root - name: ExecutionPayloadBid#heze sources: [] spec: | - + class ExecutionPayloadBid(Container): parent_block_hash: Hash32 parent_block_root: Root @@ -1020,6 +1051,7 @@ value: Gwei execution_payment: Gwei blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + execution_requests_root: Root # [New in Heze:EIP7805] inclusion_list_bits: Bitvector[INCLUSION_LIST_COMMITTEE_SIZE] @@ -1029,14 +1061,12 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const ExecutionPayloadEnvelope = spec: | - + class ExecutionPayloadEnvelope(Container): payload: ExecutionPayload execution_requests: ExecutionRequests builder_index: BuilderIndex beacon_block_root: Root - slot: Slot - state_root: Root - name: ExecutionPayloadHeader#bellatrix diff --git a/specrefs/dataclasses.yml b/specrefs/dataclasses.yml index 559fe34b21e3..816b1612943c 100644 --- a/specrefs/dataclasses.yml +++ b/specrefs/dataclasses.yml @@ -68,6 +68,20 @@ processed_sweep_withdrawals_count: uint64 +- name: FastConfirmationStore#phase0 + sources: [] + spec: | + + class FastConfirmationStore(object): + store: Store + confirmed_root: Root + previous_epoch_observed_justified_checkpoint: Checkpoint + current_epoch_observed_justified_checkpoint: Checkpoint + previous_epoch_greatest_unrealized_checkpoint: Checkpoint + previous_slot_head: Root + current_slot_head: Root + + - name: GetInclusionListResponse#heze sources: [] spec: | @@ -310,16 +324,31 @@ parent_beacon_block_root: Root +- name: PayloadAttributes#gloas + sources: [] + spec: | + + class PayloadAttributes(object): + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: ExecutionAddress + withdrawals: Sequence[Withdrawal] + parent_beacon_block_root: Root + # [New in Gloas:EIP7843] + slot_number: uint64 + + - name: PayloadAttributes#heze sources: [] spec: | - + class PayloadAttributes(object): timestamp: uint64 prev_randao: Bytes32 suggested_fee_recipient: ExecutionAddress withdrawals: Sequence[Withdrawal] parent_beacon_block_root: Root + slot_number: uint64 # [New in Heze:EIP7805] inclusion_list_transactions: Sequence[Transaction] @@ -350,7 +379,7 @@ - name: Store#gloas sources: [] spec: | - + class Store(object): time: uint64 genesis_time: uint64 @@ -369,7 +398,7 @@ latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) # [New in Gloas:EIP7732] - payload_states: Dict[Root, BeaconState] = field(default_factory=dict) + payloads: Dict[Root, ExecutionPayloadEnvelope] = field(default_factory=dict) # [New in Gloas:EIP7732] payload_timeliness_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict) # [New in Gloas:EIP7732] @@ -381,7 +410,7 @@ - name: Store#heze sources: [] spec: | - + class Store(object): time: uint64 genesis_time: uint64 @@ -399,7 +428,7 @@ checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) - payload_states: Dict[Root, BeaconState] = field(default_factory=dict) + payloads: Dict[Root, ExecutionPayloadEnvelope] = field(default_factory=dict) payload_timeliness_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict) payload_data_availability_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field( default_factory=dict diff --git a/specrefs/functions.yml b/specrefs/functions.yml index 709db3a6a3fc..264fb1179eab 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -86,6 +86,19 @@ set_or_append_list(state.inactivity_scores, index, uint64(0)) +- name: adjust_committee_weight_estimate_to_ensure_safety#phase0 + sources: [] + spec: | + + def adjust_committee_weight_estimate_to_ensure_safety(estimate: Gwei) -> Gwei: + """ + Return adjusted ``estimate`` of the weight of a committee for a sequence of slots + spanning an epoch boundary that does not cover any full epoch. + """ + ceil = (estimate + 999) // 1000 + return Gwei(ceil * (1000 + COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR)) + + - name: apply_deposit#phase0 sources: - file: packages/state-transition/src/block/processDeposit.ts @@ -201,6 +214,49 @@ store.optimistic_header = store.finalized_header +- name: apply_parent_execution_payload#gloas + sources: [] + spec: | + + def apply_parent_execution_payload( + state: BeaconState, + parent_bid: ExecutionPayloadBid, + requests: ExecutionRequests, + ) -> None: + parent_slot = parent_bid.slot + parent_epoch = compute_epoch_at_slot(parent_slot) + + # Process execution requests from parent's payload. The execution + # requests are processed at state.slot (child's slot), not the parent's slot. + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + for_ops(requests.deposits, process_deposit_request) + for_ops(requests.withdrawals, process_withdrawal_request) + for_ops(requests.consolidations, process_consolidation_request) + + # Settle the builder payment + if parent_epoch == get_current_epoch(state): + payment_index = SLOTS_PER_EPOCH + parent_slot % SLOTS_PER_EPOCH + settle_builder_payment(state, payment_index) + elif parent_epoch == get_previous_epoch(state): + payment_index = parent_slot % SLOTS_PER_EPOCH + settle_builder_payment(state, payment_index) + elif parent_bid.value > 0: + state.builder_pending_withdrawals.append( + BuilderPendingWithdrawal( + fee_recipient=parent_bid.fee_recipient, + amount=parent_bid.value, + builder_index=parent_bid.builder_index, + ) + ) + + # Update parent payload availability and latest block hash + state.execution_payload_availability[parent_slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1 + state.latest_block_hash = parent_bid.block_hash + + - name: apply_pending_deposit#electra sources: - file: packages/state-transition/src/epoch/processPendingDeposits.ts @@ -440,6 +496,34 @@ return Epoch(epoch + 1 + MAX_SEED_LOOKAHEAD) +- name: compute_adversarial_weight#phase0 + sources: [] + spec: | + + def compute_adversarial_weight( + store: Store, + balance_source: BeaconState, + start_slot: Slot, + end_slot: Slot, + ) -> Gwei: + """ + Return maximum possible adversarial weight in the committees of the slots + between ``start_slot`` and ``end_slot`` (inclusive of both). + """ + total_active_balance = get_total_active_balance(balance_source) + maximum_weight = estimate_committee_weight_between_slots( + total_active_balance, start_slot, end_slot + ) + max_adversarial_weight = maximum_weight // 100 * CONFIRMATION_BYZANTINE_THRESHOLD + + # Discount total weight of equivocating validators + equivocation_score = get_equivocation_score(store, balance_source, start_slot, end_slot) + if max_adversarial_weight > equivocation_score: + return Gwei(max_adversarial_weight - equivocation_score) + else: + return Gwei(0) + + - name: compute_attestation_subnet_prefix_bits#phase0 sources: - file: packages/params/src/index.ts @@ -453,29 +537,10 @@ return uint64(ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS) -- name: compute_balance_weighted_acceptance#gloas - sources: [] - spec: | - - def compute_balance_weighted_acceptance( - state: BeaconState, index: ValidatorIndex, seed: Bytes32, i: uint64 - ) -> bool: - """ - Return whether to accept the selection of the validator ``index``, with probability - proportional to its ``effective_balance``, and randomness given by ``seed`` and ``i``. - """ - MAX_RANDOM_VALUE = 2**16 - 1 - random_bytes = hash(seed + uint_to_bytes(i // 16)) - offset = i % 16 * 2 - random_value = bytes_to_uint64(random_bytes[offset : offset + 2]) - effective_balance = state.validators[index].effective_balance - return effective_balance * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_ELECTRA * random_value - - - name: compute_balance_weighted_selection#gloas sources: [] spec: | - + def compute_balance_weighted_selection( state: BeaconState, indices: Sequence[ValidatorIndex], @@ -489,17 +554,24 @@ are themselves sampled from ``indices`` by shuffling it, otherwise ``indices`` is traversed in order. """ + MAX_RANDOM_VALUE = 2**16 - 1 total = uint64(len(indices)) assert total > 0 + effective_balances = [state.validators[index].effective_balance for index in indices] selected: List[ValidatorIndex] = [] i = uint64(0) while len(selected) < size: + offset = i % 16 * 2 + if offset == 0: + random_bytes = hash(seed + uint_to_bytes(i // 16)) next_index = i % total if shuffle_indices: next_index = compute_shuffled_index(next_index, total, seed) - candidate_index = indices[next_index] - if compute_balance_weighted_acceptance(state, candidate_index, seed, i): - selected.append(candidate_index) + weight = effective_balances[next_index] * MAX_RANDOM_VALUE + random_value = bytes_to_uint64(random_bytes[offset : offset + 2]) + threshold = MAX_EFFECTIVE_BALANCE_ELECTRA * random_value + if weight >= threshold: + selected.append(indices[next_index]) i += 1 return selected @@ -595,6 +667,41 @@ return Domain(domain_type + fork_data_root[:28]) +- name: compute_empty_slot_support_discount#phase0 + sources: [] + spec: | + + def compute_empty_slot_support_discount( + store: Store, balance_source: BeaconState, block_root: Root + ) -> Gwei: + """ + Return weight that can be discounted during the safety threshold computation + if there are empty slots preceding the block. + """ + block = store.blocks[block_root] + parent_block = store.blocks[block.parent_root] + # No empty slot + if parent_block.slot + 1 == block.slot: + return Gwei(0) + + # Discount votes supporting the parent block if they are from the committees of empty slots + parent_support_in_empty_slots = get_block_support_between_slots( + store, + balance_source, + block.parent_root, + Slot(parent_block.slot + 1), + Slot(block.slot - 1), + ) + # Adversarial weight is not discounted + adversarial_weight = compute_adversarial_weight( + store, balance_source, Slot(parent_block.slot + 1), Slot(block.slot - 1) + ) + if parent_support_in_empty_slots > adversarial_weight: + return parent_support_in_empty_slots - adversarial_weight + else: + return Gwei(0) + + - name: compute_epoch_at_slot#phase0 sources: - file: packages/state-transition/src/util/epoch.ts @@ -885,6 +992,46 @@ return GENESIS_FORK_VERSION +- name: compute_honest_ffg_support_for_current_target#phase0 + sources: [] + spec: | + + def compute_honest_ffg_support_for_current_target(store: Store) -> Gwei: + """ + Compute honest FFG support of the current epoch target. + """ + current_slot = get_current_slot(store) + current_epoch = compute_epoch_at_slot(current_slot) + balance_source = get_pulled_up_head_state(store) + total_active_balance = get_total_active_balance(balance_source) + + # Compute FFG support for the target + ffg_support_for_checkpoint = get_current_target_score(store) + + # Compute the total FFG weight up to, but excluding, the current slot + ffg_weight_till_now = estimate_committee_weight_between_slots( + total_active_balance, compute_start_slot_at_epoch(current_epoch), Slot(current_slot - 1) + ) + + # Compute remaining honest FFG weight + remaining_ffg_weight = total_active_balance - ffg_weight_till_now + remaining_honest_ffg_weight = Gwei( + remaining_ffg_weight // 100 * (100 - CONFIRMATION_BYZANTINE_THRESHOLD) + ) + + # Compute potential adversarial weight + adversarial_weight = compute_adversarial_weight( + store, balance_source, compute_start_slot_at_epoch(current_epoch), Slot(current_slot - 1) + ) + + # Compute min honest FFG support + min_honest_ffg_support = ffg_support_for_checkpoint - min( + adversarial_weight, ffg_support_for_checkpoint + ) + + return Gwei(min_honest_ffg_support + remaining_honest_ffg_weight) + + - name: compute_matrix#fulu sources: - file: packages/beacon-node/src/util/dataColumns.ts @@ -1146,32 +1293,76 @@ update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) +- name: compute_safety_threshold#phase0 + sources: [] + spec: | + + def compute_safety_threshold(store: Store, block_root: Root, balance_source: BeaconState) -> Gwei: + """ + Compute the LMD-GHOST safety threshold for ``block_root``. + """ + current_slot = get_current_slot(store) + block = store.blocks[block_root] + parent_block = store.blocks[block.parent_root] + + total_active_balance = get_total_active_balance(balance_source) + proposer_score = compute_proposer_score(balance_source) + maximum_support = estimate_committee_weight_between_slots( + total_active_balance, Slot(parent_block.slot + 1), Slot(current_slot - 1) + ) + support_discount = get_support_discount(store, balance_source, block_root) + adversarial_weight = get_adversarial_weight(store, balance_source, block_root) + + # Return (maximum_support + proposer_score - support_discount) // 2 + adversarial_weight + # with an underflow guard + if support_discount < maximum_support + proposer_score + 2 * adversarial_weight: + return (maximum_support + proposer_score + 2 * adversarial_weight - support_discount) // 2 + else: + return Gwei(0) + + - name: compute_shuffled_index#phase0 sources: - file: packages/state-transition/src/util/seed.ts search: export function computeShuffledIndex( spec: | - + def compute_shuffled_index(index: uint64, index_count: uint64, seed: Bytes32) -> uint64: """ Return the shuffled index corresponding to ``seed`` (and ``index_count``). """ assert index < index_count + return compute_shuffled_permutation(index_count, seed)[index] + +- name: compute_shuffled_permutation#phase0 + sources: [] + spec: | + + def compute_shuffled_permutation(index_count: uint64, seed: Bytes32) -> Sequence[uint64]: + """ + Return the full shuffled permutation corresponding to ``seed`` (and ``index_count``). + """ # Swap or not (https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf) # See the 'generalized domain' algorithm on page 3 + indices = [uint64(i) for i in range(index_count)] for current_round in range(SHUFFLE_ROUND_COUNT): - pivot = bytes_to_uint64(hash(seed + uint_to_bytes(uint8(current_round)))[0:8]) % index_count - flip = (pivot + index_count - index) % index_count - position = max(index, flip) - source = hash( - seed + uint_to_bytes(uint8(current_round)) + uint_to_bytes(uint32(position // 256)) - ) - byte = uint8(source[(position % 256) // 8]) - bit = (byte >> (position % 8)) % 2 - index = flip if bit else index - - return index + round_bytes = current_round.to_bytes(1, "little") + pivot = int.from_bytes(hash(seed + round_bytes)[0:8], "little") % index_count + source_by_bucket: Dict[uint64, Bytes32] = {} + for i in range(index_count): + flip = (pivot + index_count - indices[i]) % index_count + position = max(indices[i], flip) + position_bucket = position // 256 + if position_bucket not in source_by_bucket: + source_by_bucket[position_bucket] = hash( + seed + round_bytes + position_bucket.to_bytes(4, "little") + ) + source = source_by_bucket[position_bucket] + byte_val = source[(position % 256) // 8] + bit = (byte_val >> int(position % 8)) % 2 + indices[i] = flip if bit else indices[i] + return indices - name: compute_signed_block_header#deneb @@ -1595,6 +1786,54 @@ state.balances[index] = 0 if delta > state.balances[index] else state.balances[index] - delta +- name: estimate_committee_weight_between_slots#phase0 + sources: [] + spec: | + + def estimate_committee_weight_between_slots( + total_active_balance: Gwei, start_slot: Slot, end_slot: Slot + ) -> Gwei: + """ + Return estimate of the total weight of committees + between ``start_slot`` and ``end_slot`` (inclusive of both). + """ + + # Sanity check + if start_slot > end_slot: + return Gwei(0) + + # If an entire epoch is covered by the range, return the total active balance + if is_full_validator_set_covered(start_slot, end_slot): + return total_active_balance + + start_epoch = compute_epoch_at_slot(start_slot) + end_epoch = compute_epoch_at_slot(end_slot) + committee_weight = total_active_balance // SLOTS_PER_EPOCH + if start_epoch == end_epoch: + return committee_weight * (end_slot - start_slot + 1) + else: + # First, calculate the number of committees in the end epoch + num_slots_in_end_epoch = compute_slots_since_epoch_start(end_slot) + 1 + # Next, calculate the number of slots remaining in the end epoch + remaining_slots_in_end_epoch = SLOTS_PER_EPOCH - num_slots_in_end_epoch + # Then, calculate the number of slots in the start epoch + num_slots_in_start_epoch = SLOTS_PER_EPOCH - compute_slots_since_epoch_start(start_slot) + + start_epoch_weight = committee_weight * num_slots_in_start_epoch + end_epoch_weight = committee_weight * num_slots_in_end_epoch + + # A range that spans an epoch boundary, but does not span any full epoch + # needs pro-rata calculation, see https://gist.github.com/saltiniroberto/9ee53d29c33878d79417abb2b4468c20 + # start_epoch_weight_pro_rated = start_epoch_weight * (1 - num_slots_in_end_epoch / SLOTS_PER_EPOCH) + start_epoch_weight_pro_rated = ( + start_epoch_weight // SLOTS_PER_EPOCH * remaining_slots_in_end_epoch + ) + + return adjust_committee_weight_estimate_to_ensure_safety( + Gwei(start_epoch_weight_pro_rated + end_epoch_weight) + ) + + - name: eth_aggregate_pubkeys#altair sources: [] spec: | @@ -1707,6 +1946,101 @@ return FINALIZED_ROOT_GINDEX +- name: find_latest_confirmed_descendant#phase0 + sources: [] + spec: | + + def find_latest_confirmed_descendant( + fcr_store: FastConfirmationStore, latest_confirmed_root: Root + ) -> Root: + """ + Return the most recent confirmed block in the suffix of the canonical chain + starting from ``latest_confirmed_root``. + """ + store = fcr_store.store + head = get_head(store) + current_epoch = get_current_store_epoch(store) + confirmed_root = latest_confirmed_root + + if ( + get_block_epoch(store, confirmed_root) + 1 == current_epoch + and get_voting_source(store, fcr_store.previous_slot_head).epoch + 2 >= current_epoch + and ( + is_start_slot_at_epoch(get_current_slot(store)) + or ( + will_no_conflicting_checkpoint_be_justified(store) + and ( + store.unrealized_justifications[fcr_store.previous_slot_head].epoch + 1 + >= current_epoch + or store.unrealized_justifications[head].epoch + 1 >= current_epoch + ) + ) + ) + ): + # Get suffix of the canonical chain + canonical_roots = get_ancestor_roots(store, head, confirmed_root) + + # Starting with the child of the latest_confirmed_root + # move towards the head in attempt to advance the confirmed block + # and stop when the first unconfirmed descendant is encountered + for block_root in canonical_roots: + block_epoch = get_block_epoch(store, block_root) + + # If the current epoch is reached, exit the loop + # as this code is meant to confirm blocks from the previous epoch + if block_epoch == current_epoch: + break + + # The algorithm can only rely on the previous head + # if it is a descendant of the block that is attempted to be confirmed + if not is_ancestor(store, fcr_store.previous_slot_head, block_root): + break + + if not is_one_confirmed(store, get_current_balance_source(fcr_store), block_root): + break + + confirmed_root = block_root + + if ( + is_start_slot_at_epoch(get_current_slot(store)) + or store.unrealized_justifications[head].epoch + 1 >= current_epoch + ): + # Get suffix of the canonical chain + canonical_roots = get_ancestor_roots(store, head, confirmed_root) + + tentative_confirmed_root = confirmed_root + + for block_root in canonical_roots: + block_epoch = get_block_epoch(store, block_root) + tentative_confirmed_epoch = get_block_epoch(store, tentative_confirmed_root) + + # The following condition can only be true the first time + # the algorithm advances to a block from the current epoch + if block_epoch > tentative_confirmed_epoch: + # To confirm blocks from the current epoch ensure that + # current epoch target will be justified + if not will_current_target_be_justified(store): + break + + if not is_one_confirmed(store, get_current_balance_source(fcr_store), block_root): + break + + tentative_confirmed_root = block_root + + # The tentative_confirmed_root can only be confirmed + # if it is for sure not going to be reorged out in either the current or next epoch. + if get_block_epoch(store, tentative_confirmed_root) == current_epoch or ( + get_voting_source(store, tentative_confirmed_root).epoch + 2 >= current_epoch + and ( + is_start_slot_at_epoch(get_current_slot(store)) + or will_no_conflicting_checkpoint_be_justified(store) + ) + ): + confirmed_root = tentative_confirmed_root + + return confirmed_root + + - name: get_activation_exit_churn_limit#electra sources: - file: packages/state-transition/src/util/validator.ts @@ -1735,6 +2069,24 @@ ] +- name: get_adversarial_weight#phase0 + sources: [] + spec: | + + def get_adversarial_weight(store: Store, balance_source: BeaconState, block_root: Root) -> Gwei: + """ + Return maximum adversarial weight that can support the block. + """ + current_slot = get_current_slot(store) + block = store.blocks[block_root] + if get_block_epoch(store, block_root) > get_block_epoch(store, block.parent_root): + # Use the first epoch slot as the start slot when crossing epoch boundary + start_slot = compute_start_slot_at_epoch(get_block_epoch(store, block_root)) + return compute_adversarial_weight(store, balance_source, start_slot, Slot(current_slot - 1)) + else: + return compute_adversarial_weight(store, balance_source, block.slot, Slot(current_slot - 1)) + + - name: get_aggregate_and_proof#phase0 sources: - file: packages/validator/src/services/validatorStore.ts @@ -1773,8 +2125,8 @@ - file: packages/config/src/forkConfig/index.ts search: "getAggregateDueMs(fork: ForkName): number {" spec: | - - def get_aggregate_due_ms(epoch: Epoch) -> uint64: + + def get_aggregate_due_ms() -> uint64: return get_slot_component_duration_ms(AGGREGATE_DUE_BPS) @@ -1783,12 +2135,10 @@ - file: packages/config/src/forkConfig/index.ts search: "getAggregateDueMs(fork: ForkName): number {" spec: | - - def get_aggregate_due_ms(epoch: Epoch) -> uint64: - # [New in Gloas] - if epoch >= GLOAS_FORK_EPOCH: - return get_slot_component_duration_ms(AGGREGATE_DUE_BPS_GLOAS) - return get_slot_component_duration_ms(AGGREGATE_DUE_BPS) + + def get_aggregate_due_ms() -> uint64: + # [Modified in Gloas] + return get_slot_component_duration_ms(AGGREGATE_DUE_BPS_GLOAS) - name: get_aggregate_signature#phase0 @@ -1839,6 +2189,28 @@ ) +- name: get_ancestor_roots#phase0 + sources: [] + spec: | + + def get_ancestor_roots(store: Store, block_root: Root, terminal_root: Root) -> Sequence[Root]: + """ + Return a list of ancestors of ``block_root`` inclusive until ``terminal_root`` exclusive. + """ + root = block_root + ancestor_roots: list[Root] = [] + while store.blocks[root].slot > store.blocks[terminal_root].slot: + ancestor_roots.insert(0, root) + root = store.blocks[root].parent_root + + # Return when terminal_root is reached + if root == terminal_root: + return ancestor_roots + + # Return empty list if terminal_root is not in the chain of block_root + return [] + + - name: get_attestation_component_deltas#phase0 sources: [] spec: | @@ -1903,8 +2275,8 @@ - file: packages/config/src/forkConfig/index.ts search: "getAttestationDueMs(fork: ForkName): number {" spec: | - - def get_attestation_due_ms(epoch: Epoch) -> uint64: + + def get_attestation_due_ms() -> uint64: return get_slot_component_duration_ms(ATTESTATION_DUE_BPS) @@ -1913,12 +2285,10 @@ - file: packages/config/src/forkConfig/index.ts search: "getAttestationDueMs(fork: ForkName): number {" spec: | - - def get_attestation_due_ms(epoch: Epoch) -> uint64: - # [New in Gloas] - if epoch >= GLOAS_FORK_EPOCH: - return get_slot_component_duration_ms(ATTESTATION_DUE_BPS_GLOAS) - return get_slot_component_duration_ms(ATTESTATION_DUE_BPS) + + def get_attestation_due_ms() -> uint64: + # [Modified in Gloas] + return get_slot_component_duration_ms(ATTESTATION_DUE_BPS_GLOAS) - name: get_attestation_participation_flag_indices#altair @@ -2361,6 +2731,17 @@ ] +- name: get_block_epoch#phase0 + sources: [] + spec: | + + def get_block_epoch(store: Store, block_root: Root) -> Epoch: + """ + Return an epoch of the block. + """ + return compute_epoch_at_slot(store.blocks[block_root].slot) + + - name: get_block_root#phase0 sources: - file: packages/state-transition/src/util/blockRoot.ts @@ -2400,6 +2781,62 @@ return bls.Sign(privkey, signing_root) +- name: get_block_slot#phase0 + sources: [] + spec: | + + def get_block_slot(store: Store, block_root: Root) -> Slot: + """ + Return a slot of the block. + """ + return store.blocks[block_root].slot + + +- name: get_block_support_between_slots#phase0 + sources: [] + spec: | + + def get_block_support_between_slots( + store: Store, + balance_source: BeaconState, + block_root: Root, + start_slot: Slot, + end_slot: Slot, + ) -> Gwei: + """ + Return support of the block by validators assigned to slots + between ``start_slot`` and ``end_slot`` (inclusive of both). + """ + participants: Set[ValidatorIndex] = set() + for slot in range(start_slot, end_slot + 1): + participants.update(get_slot_committee(store, Slot(slot))) + + # Keep validators that were active at the balance_source epoch to be consistent + # with get_total_active_balance() computation, also filter out slashed validators + unslashed_and_active_indices = [ + i + for i in participants + if ( + not balance_source.validators[i].slashed + and is_active_validator(balance_source.validators[i], get_current_epoch(balance_source)) + ) + ] + + return Gwei( + sum( + balance_source.validators[i].effective_balance + for i in unslashed_and_active_indices + # Check that validator has voted in the support of the block + # and has not been slashed + if ( + i in store.latest_messages + and store.latest_messages[i].root == block_root + and i not in store.equivocating_indices + ) + ) + ) + + - name: get_builder_payment_quorum_threshold#gloas sources: - file: packages/state-transition/src/util/gloas.ts @@ -2515,6 +2952,17 @@ return get_ancestor(store, root, epoch_first_slot).root +- name: get_checkpoint_for_block#phase0 + sources: [] + spec: | + + def get_checkpoint_for_block(store: Store, block_root: Root, epoch: Epoch) -> Checkpoint: + """ + Return a checkpoint in the chain of the block at the ``epoch``. + """ + return Checkpoint(epoch=epoch, root=get_checkpoint_block(store, block_root, epoch)) + + - name: get_committee_assignment#phase0 sources: - file: packages/state-transition/src/cache/epochCache.ts @@ -2631,8 +3079,8 @@ - file: packages/config/src/forkConfig/index.ts search: "getSyncContributionDueMs(fork: ForkName): number {" spec: | - - def get_contribution_due_ms(epoch: Epoch) -> uint64: + + def get_contribution_due_ms() -> uint64: return get_slot_component_duration_ms(CONTRIBUTION_DUE_BPS) @@ -2641,12 +3089,19 @@ - file: packages/config/src/forkConfig/index.ts search: "getSyncContributionDueMs(fork: ForkName): number {" spec: | - - def get_contribution_due_ms(epoch: Epoch) -> uint64: - # [New in Gloas] - if epoch >= GLOAS_FORK_EPOCH: - return get_slot_component_duration_ms(CONTRIBUTION_DUE_BPS_GLOAS) - return get_slot_component_duration_ms(CONTRIBUTION_DUE_BPS) + + def get_contribution_due_ms() -> uint64: + # [Modified in Gloas] + return get_slot_component_duration_ms(CONTRIBUTION_DUE_BPS_GLOAS) + + +- name: get_current_balance_source#phase0 + sources: [] + spec: | + + def get_current_balance_source(fcr_store: FastConfirmationStore) -> BeaconState: + store = fcr_store.store + return store.checkpoint_states[fcr_store.current_epoch_observed_justified_checkpoint] - name: get_current_epoch#phase0 @@ -2680,6 +3135,52 @@ return compute_epoch_at_slot(get_current_slot(store)) +- name: get_current_target#phase0 + sources: [] + spec: | + + def get_current_target(store: Store) -> Checkpoint: + """ + Return current epoch target. + """ + head = get_head(store) + current_epoch = get_current_store_epoch(store) + return get_checkpoint_for_block(store, head, current_epoch) + + +- name: get_current_target_score#phase0 + sources: [] + spec: | + + def get_current_target_score(store: Store) -> Gwei: + """ + Return the estimate of FFG support of the current epoch target by using LMD-GHOST votes. + """ + target = get_current_target(store) + state = get_pulled_up_head_state(store) + unslashed_and_active_indices = [ + i + for i in get_active_validator_indices(state, get_current_epoch(state)) + if not state.validators[i].slashed + ] + return Gwei( + sum( + state.validators[i].effective_balance + for i in unslashed_and_active_indices + if ( + i in store.latest_messages + and i not in store.equivocating_indices + and target + == get_checkpoint_for_block( + store, + store.latest_messages[i].root, + get_latest_message_epoch(store.latest_messages[i]), + ) + ) + ) + ) + + - name: get_custody_groups#fulu sources: - file: packages/beacon-node/src/util/dataColumns.ts @@ -2945,6 +3446,37 @@ return bls.Sign(privkey, signing_root) +- name: get_equivocation_score#phase0 + sources: [] + spec: | + + def get_equivocation_score( + store: Store, + balance_source: BeaconState, + start_slot: Slot, + end_slot: Slot, + ) -> Gwei: + """ + Return total weight of equivocating participants of all committees + in the slots between ``start_slot`` and ``end_slot`` (inclusive of both). + """ + committee_indices: Set[ValidatorIndex] = set() + for slot in range(start_slot, end_slot + 1): + committee_indices.update(get_slot_committee(store, Slot(slot))) + + # Keep equivocating validators that were active at the balance_source epoch to be consistent + # with get_total_active_balance() computation + active_equivocating_indices = [ + i + for i in committee_indices.intersection(store.equivocating_indices) + if is_active_validator(balance_source.validators[i], get_current_epoch(balance_source)) + ] + + return Gwei( + sum(balance_source.validators[i].effective_balance for i in active_equivocating_indices) + ) + + - name: get_eth1_pending_deposit_count#electra sources: [] spec: | @@ -3245,6 +3777,22 @@ ) +- name: get_fast_confirmation_store#phase0 + sources: [] + spec: | + + def get_fast_confirmation_store(store: Store) -> FastConfirmationStore: + return FastConfirmationStore( + store=store, + confirmed_root=store.finalized_checkpoint.root, + previous_epoch_observed_justified_checkpoint=store.finalized_checkpoint, + current_epoch_observed_justified_checkpoint=store.finalized_checkpoint, + previous_epoch_greatest_unrealized_checkpoint=store.finalized_checkpoint, + previous_slot_head=store.finalized_checkpoint.root, + current_slot_head=store.finalized_checkpoint.root, + ) + + - name: get_filtered_block_tree#phase0 sources: [] spec: | @@ -3335,7 +3883,7 @@ - name: get_forkchoice_store#gloas sources: [] spec: | - + def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -> Store: assert anchor_block.state_root == hash_tree_root(anchor_state) anchor_root = hash_tree_root(anchor_block) @@ -3359,7 +3907,7 @@ checkpoint_states={justified_checkpoint: copy(anchor_state)}, unrealized_justifications={anchor_root: justified_checkpoint}, # [New in Gloas:EIP7732] - payload_states={anchor_root: copy(anchor_state)}, + payloads={}, # [New in Gloas:EIP7732] payload_timeliness_vote={ anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE)) @@ -3374,7 +3922,7 @@ - name: get_forkchoice_store#heze sources: [] spec: | - + def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -> Store: assert anchor_block.state_root == hash_tree_root(anchor_state) anchor_root = hash_tree_root(anchor_block) @@ -3396,7 +3944,7 @@ block_timeliness={anchor_root: [True, True]}, checkpoint_states={justified_checkpoint: copy(anchor_state)}, unrealized_justifications={anchor_root: justified_checkpoint}, - payload_states={anchor_root: copy(anchor_state)}, + payloads={}, payload_timeliness_vote={ anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE)) }, @@ -3683,8 +4231,8 @@ - name: get_inclusion_list_submission_due_ms#heze sources: [] spec: | - - def get_inclusion_list_submission_due_ms(epoch: Epoch) -> uint64: + + def get_inclusion_list_submission_due_ms() -> uint64: return get_slot_component_duration_ms(INCLUSION_LIST_SUBMISSION_DUE_BPS) @@ -3775,6 +4323,85 @@ ) +- name: get_latest_confirmed#phase0 + sources: [] + spec: | + + def get_latest_confirmed(fcr_store: FastConfirmationStore) -> Root: + """ + Return the most recent confirmed block by executing the FCR algorithm. + """ + store = fcr_store.store + confirmed_root = fcr_store.confirmed_root + current_epoch = get_current_store_epoch(store) + + # Revert to finalized block if either of the following is true: + # 1) the latest confirmed block's epoch is older than the previous epoch, + # 2) the latest confirmed block does not belong to the canonical chain, + # 3) the confirmed chain starting from the current epoch observed justified checkpoint + # cannot be re-confirmed at the start of the current epoch. + head = get_head(store) + if ( + get_block_epoch(store, confirmed_root) + 1 < current_epoch + or not is_ancestor(store, head, confirmed_root) + or ( + is_start_slot_at_epoch(get_current_slot(store)) + and not is_confirmed_chain_safe(fcr_store, confirmed_root) + ) + ): + confirmed_root = store.finalized_checkpoint.root + + # Restart the confirmation chain if each of the following conditions are true: + # 1) it is the start of the current epoch, + # 2) epoch of fcr_store.current_epoch_observed_justified_checkpoint.root equals to the previous epoch, + # 3) fcr_store.current_epoch_observed_justified_checkpoint equals to unrealized justification of the head, + # 4) confirmed block is older than the block of fcr_store.current_epoch_observed_justified_checkpoint. + is_epoch_start = is_start_slot_at_epoch(get_current_slot(store)) + observed_justified_block_slot = get_block_slot( + store, fcr_store.current_epoch_observed_justified_checkpoint.root + ) + is_observed_justified_block_epoch_ok = ( + compute_epoch_at_slot(observed_justified_block_slot) + 1 == current_epoch + ) + is_head_unrealized_justified_ok = ( + fcr_store.current_epoch_observed_justified_checkpoint + == store.unrealized_justifications[head] + ) + is_confirmed_block_stale = get_block_slot(store, confirmed_root) < observed_justified_block_slot + if ( + is_epoch_start + and is_observed_justified_block_epoch_ok + and is_head_unrealized_justified_ok + and is_confirmed_block_stale + ): + confirmed_root = fcr_store.current_epoch_observed_justified_checkpoint.root + + # Attempt to further advance the latest confirmed block + if get_block_epoch(store, confirmed_root) + 1 >= current_epoch: + return find_latest_confirmed_descendant(fcr_store, confirmed_root) + else: + return confirmed_root + + +- name: get_latest_message_epoch#phase0 + sources: [] + spec: | + + def get_latest_message_epoch(latest_message: LatestMessage) -> Epoch: + """ + Return epoch of the ``latest_message``. + """ + return latest_message.epoch + + +- name: get_latest_message_epoch#gloas + sources: [] + spec: | + + def get_latest_message_epoch(latest_message: LatestMessage) -> Epoch: + return compute_epoch_at_slot(latest_message.slot) + + - name: get_lc_execution_root#capella sources: [] spec: | @@ -3826,37 +4453,15 @@ - name: get_lc_execution_root#electra sources: [] spec: | - + def get_lc_execution_root(header: LightClientHeader) -> Root: epoch = compute_epoch_at_slot(header.beacon.slot) - # [New in Electra] - if epoch >= ELECTRA_FORK_EPOCH: - return hash_tree_root(header.execution) - - # [Modified in Electra] + # [New in Deneb] if epoch >= DENEB_FORK_EPOCH: - execution_header = deneb.ExecutionPayloadHeader( - parent_hash=header.execution.parent_hash, - fee_recipient=header.execution.fee_recipient, - state_root=header.execution.state_root, - receipts_root=header.execution.receipts_root, - logs_bloom=header.execution.logs_bloom, - prev_randao=header.execution.prev_randao, - block_number=header.execution.block_number, - gas_limit=header.execution.gas_limit, - gas_used=header.execution.gas_used, - timestamp=header.execution.timestamp, - extra_data=header.execution.extra_data, - base_fee_per_gas=header.execution.base_fee_per_gas, - block_hash=header.execution.block_hash, - transactions_root=header.execution.transactions_root, - withdrawals_root=header.execution.withdrawals_root, - blob_gas_used=header.execution.blob_gas_used, - excess_blob_gas=header.execution.excess_blob_gas, - ) - return hash_tree_root(execution_header) + return hash_tree_root(header.execution) + # [Modified in Deneb] if epoch >= CAPELLA_FORK_EPOCH: execution_header = capella.ExecutionPayloadHeader( parent_hash=header.execution.parent_hash, @@ -4041,13 +4646,13 @@ - name: get_node_children#gloas sources: [] spec: | - + def get_node_children( store: Store, blocks: Dict[Root, BeaconBlock], node: ForkChoiceNode ) -> Sequence[ForkChoiceNode]: if node.payload_status == PAYLOAD_STATUS_PENDING: children = [ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_EMPTY)] - if node.root in store.payload_states: + if is_payload_verified(store, node.root): children.append(ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_FULL)) return children else: @@ -4064,19 +4669,24 @@ - name: get_parent_payload_status#gloas sources: [] spec: | - + def get_parent_payload_status(store: Store, block: BeaconBlock) -> PayloadStatus: parent = store.blocks[block.parent_root] parent_block_hash = block.body.signed_execution_payload_bid.message.parent_block_hash message_block_hash = parent.body.signed_execution_payload_bid.message.block_hash + + # Check for uninitialized genesis block hash + if message_block_hash == Hash32(): + return PAYLOAD_STATUS_EMPTY + return PAYLOAD_STATUS_FULL if parent_block_hash == message_block_hash else PAYLOAD_STATUS_EMPTY - name: get_payload_attestation_due_ms#gloas sources: [] spec: | - - def get_payload_attestation_due_ms(epoch: Epoch) -> uint64: + + def get_payload_attestation_due_ms() -> uint64: return get_slot_component_duration_ms(PAYLOAD_ATTESTATION_DUE_BPS) @@ -4223,6 +4833,15 @@ return None +- name: get_previous_balance_source#phase0 + sources: [] + spec: | + + def get_previous_balance_source(fcr_store: FastConfirmationStore) -> BeaconState: + store = fcr_store.store + return store.checkpoint_states[fcr_store.previous_epoch_observed_justified_checkpoint] + + - name: get_previous_epoch#phase0 sources: - file: packages/state-transition/src/util/epoch.ts @@ -4301,8 +4920,8 @@ - name: get_proposer_inclusion_list_cutoff_ms#heze sources: [] spec: | - - def get_proposer_inclusion_list_cutoff_ms(epoch: Epoch) -> uint64: + + def get_proposer_inclusion_list_cutoff_ms() -> uint64: return get_slot_component_duration_ms(PROPOSER_INCLUSION_LIST_CUTOFF_BPS) @@ -4325,8 +4944,8 @@ - file: packages/config/src/forkConfig/index.ts search: "getProposerReorgCutoffMs(_fork: ForkName): number {" spec: | - - def get_proposer_reorg_cutoff_ms(epoch: Epoch) -> uint64: + + def get_proposer_reorg_cutoff_ms() -> uint64: return get_slot_component_duration_ms(PROPOSER_REORG_CUTOFF_BPS) @@ -4390,6 +5009,24 @@ return None +- name: get_pulled_up_head_state#phase0 + sources: [] + spec: | + + def get_pulled_up_head_state(store: Store) -> BeaconState: + """ + Return the state of the head pulled up to the current epoch if needed. + """ + head = get_head(store) + head_state = store.block_states[head] + if get_current_epoch(head_state) < get_current_store_epoch(store): + pulled_up_state = copy(head_state) + process_slots(pulled_up_state, compute_start_slot_at_epoch(get_current_store_epoch(store))) + return pulled_up_state + else: + return head_state + + - name: get_randao_mix#phase0 sources: - file: packages/state-transition/src/util/seed.ts @@ -4435,6 +5072,23 @@ return hash(domain_type + uint_to_bytes(epoch) + mix) +- name: get_slot_committee#phase0 + sources: [] + spec: | + + def get_slot_committee(store: Store, slot: Slot) -> Set[ValidatorIndex]: + """ + Return participants of all committees in ``slot``. + """ + head = get_head(store) + shuffling_source = store.block_states[head] + committees_count = get_committee_count_per_slot(shuffling_source, compute_epoch_at_slot(slot)) + participants: Set[ValidatorIndex] = set() + for i in range(committees_count): + participants.update(get_beacon_committee(shuffling_source, slot, CommitteeIndex(i))) + return participants + + - name: get_slot_component_duration_ms#phase0 sources: - file: packages/config/src/forkConfig/index.ts @@ -4492,6 +5146,17 @@ return uint64(generalized_index % 2 ** (floorlog2(generalized_index))) +- name: get_support_discount#phase0 + sources: [] + spec: | + + def get_support_discount(store: Store, balance_source: BeaconState, block_root: Root) -> Gwei: + """ + Return weight that can be discounted during the safety threshold computation for the block. + """ + return compute_empty_slot_support_discount(store, balance_source, block_root) + + - name: get_sync_committee_message#altair sources: - file: packages/validator/src/services/validatorStore.ts @@ -4537,8 +5202,8 @@ - file: packages/config/src/forkConfig/index.ts search: "getSyncMessageDueMs(fork: ForkName): number {" spec: | - - def get_sync_message_due_ms(epoch: Epoch) -> uint64: + + def get_sync_message_due_ms() -> uint64: return get_slot_component_duration_ms(SYNC_MESSAGE_DUE_BPS) @@ -4547,12 +5212,10 @@ - file: packages/config/src/forkConfig/index.ts search: "getSyncMessageDueMs(fork: ForkName): number {" spec: | - - def get_sync_message_due_ms(epoch: Epoch) -> uint64: - # [New in Gloas] - if epoch >= GLOAS_FORK_EPOCH: - return get_slot_component_duration_ms(SYNC_MESSAGE_DUE_BPS_GLOAS) - return get_slot_component_duration_ms(SYNC_MESSAGE_DUE_BPS) + + def get_sync_message_due_ms() -> uint64: + # [Modified in Gloas] + return get_slot_component_duration_ms(SYNC_MESSAGE_DUE_BPS_GLOAS) - name: get_sync_subcommittee_pubkeys#altair @@ -4905,8 +5568,8 @@ - name: get_view_freeze_cutoff_ms#heze sources: [] spec: | - - def get_view_freeze_cutoff_ms(epoch: Epoch) -> uint64: + + def get_view_freeze_cutoff_ms() -> uint64: return get_slot_component_duration_ms(VIEW_FREEZE_CUTOFF_BPS) @@ -5196,17 +5859,13 @@ - file: packages/state-transition/src/util/gloas.ts search: export function initiateBuilderExit( spec: | - + def initiate_builder_exit(state: BeaconState, builder_index: BuilderIndex) -> None: """ Initiate the exit of the builder with index ``index``. """ - # Return if builder already initiated exit - builder = state.builders[builder_index] - if builder.withdrawable_epoch != FAR_FUTURE_EPOCH: - return - # Set builder exit epoch + builder = state.builders[builder_index] builder.withdrawable_epoch = get_current_epoch(state) + MIN_BUILDER_WITHDRAWABILITY_DELAY @@ -5326,6 +5985,17 @@ return bytes_to_uint64(hash(slot_signature)[0:8]) % modulo == 0 +- name: is_ancestor#phase0 + sources: [] + spec: | + + def is_ancestor(store: Store, block_root: Root, ancestor_root: Root) -> bool: + """ + Return ``True`` if ``ancestor_root`` is an ancestor of ``block_root``. + """ + return get_ancestor(store, block_root, store.blocks[ancestor_root].slot) == ancestor_root + + - name: is_assigned_to_sync_committee#altair sources: [] spec: | @@ -5466,6 +6136,48 @@ return withdrawal_credentials[:1] == COMPOUNDING_WITHDRAWAL_PREFIX +- name: is_confirmed_chain_safe#phase0 + sources: [] + spec: | + + def is_confirmed_chain_safe(fcr_store: FastConfirmationStore, confirmed_root: Root) -> bool: + """ + Return ``True`` if and only if all blocks of the confirmed chain + starting from current_epoch_observed_justified_checkpoint are LMD-GHOST safe. + """ + store = fcr_store.store + # Check if the confirmed_root is descendant of current_epoch_observed_justified_checkpoint + if not is_ancestor( + store, confirmed_root, fcr_store.current_epoch_observed_justified_checkpoint.root + ): + return False + + current_epoch = get_current_store_epoch(store) + if fcr_store.current_epoch_observed_justified_checkpoint.epoch + 1 >= current_epoch: + # Exclude the justified checkpoint block if it is from the previous epoch + # as then this block will always be canonical in this case. + start_root_exclusive = fcr_store.current_epoch_observed_justified_checkpoint.root + else: + # Limit reconfirmation to the first block of the previous epoch + # as if it is successful, reconfirmation of the ancestors is implied. + ancestor_at_previous_epoch_start = get_ancestor( + store, confirmed_root, compute_start_slot_at_epoch(Epoch(current_epoch - 1)) + ) + if get_block_epoch(store, ancestor_at_previous_epoch_start) + 1 == current_epoch: + # The parent of the first block of the previous epoch + start_root_exclusive = store.blocks[ancestor_at_previous_epoch_start].parent_root + else: + # The last block of the epoch before the previous one + start_root_exclusive = ancestor_at_previous_epoch_start + + # Run is_one_confirmed for each block in the confirmed chain with the previous epoch balance source + chain_roots = get_ancestor_roots(store, confirmed_root, start_root_exclusive) + return all( + is_one_confirmed(store, get_previous_balance_source(fcr_store), root) + for root in chain_roots + ) + + - name: is_data_available#deneb sources: - file: packages/beacon-node/src/chain/blocks/verifyBlocksDataAvailability.ts @@ -5646,6 +6358,19 @@ return epochs_since_finalization <= REORG_MAX_EPOCHS_SINCE_FINALIZATION +- name: is_full_validator_set_covered#phase0 + sources: [] + spec: | + + def is_full_validator_set_covered(start_slot: Slot, end_slot: Slot) -> bool: + """ + Return ``True`` if the range between ``start_slot`` and ``end_slot`` (inclusive of both) includes an entire epoch. + """ + start_full_epoch = compute_epoch_at_slot(start_slot + (SLOTS_PER_EPOCH - 1)) + end_full_epoch = compute_epoch_at_slot(Slot(end_slot + 1)) + return start_full_epoch < end_full_epoch + + - name: is_fully_withdrawable_validator#capella sources: [] spec: | @@ -5796,6 +6521,19 @@ return store.next_sync_committee != SyncCommittee() +- name: is_one_confirmed#phase0 + sources: [] + spec: | + + def is_one_confirmed(store: Store, balance_source: BeaconState, block_root: Root) -> bool: + """ + Return ``True`` if and only if the block is LMD-GHOST safe. + """ + support = get_attestation_score(store, block_root, balance_source) + safety_threshold = compute_safety_threshold(store, block_root, balance_source) + return support > safety_threshold + + - name: is_optimistic#bellatrix sources: [] spec: | @@ -5820,16 +6558,6 @@ return False -- name: is_parent_block_full#gloas - sources: - - file: packages/state-transition/src/util/gloas.ts - search: export function isParentBlockFull( - spec: | - - def is_parent_block_full(state: BeaconState) -> bool: - return state.latest_execution_payload_bid.block_hash == state.latest_block_hash - - - name: is_parent_node_full#gloas sources: [] spec: | @@ -5907,7 +6635,7 @@ - name: is_payload_data_available#gloas sources: [] spec: | - + def is_payload_data_available(store: Store, root: Root) -> bool: """ Return whether the blob data for the beacon block with root ``root`` @@ -5918,7 +6646,7 @@ # If the payload is not locally available, the blob data # is not considered available regardless of the PTC vote - if root not in store.payload_states: + if not is_payload_verified(store, root): return False return sum(store.payload_data_availability_vote[root]) > DATA_AVAILABILITY_TIMELY_THRESHOLD @@ -5927,7 +6655,7 @@ - name: is_payload_inclusion_list_satisfied#heze sources: [] spec: | - + def is_payload_inclusion_list_satisfied(store: Store, root: Root) -> bool: """ Return whether the execution payload for the beacon block with root ``root`` @@ -5938,7 +6666,7 @@ # If the payload is not locally available, the payload # is not considered to satisfy the inclusion list constraints - if root not in store.payload_states: + if not is_payload_verified(store, root): return False return store.payload_inclusion_list_satisfaction[root] @@ -5947,7 +6675,7 @@ - name: is_payload_timely#gloas sources: [] spec: | - + def is_payload_timely(store: Store, root: Root) -> bool: """ Return whether the execution payload for the beacon block with root ``root`` @@ -5958,12 +6686,25 @@ # If the payload is not locally available, the payload # is not considered available regardless of the PTC vote - if root not in store.payload_states: + if not is_payload_verified(store, root): return False return sum(store.payload_timeliness_vote[root]) > PAYLOAD_TIMELY_THRESHOLD +- name: is_payload_verified#gloas + sources: [] + spec: | + + def is_payload_verified(store: Store, root: Root) -> bool: + """ + Return whether the execution payload envelope for the beacon block with + root ``root`` has been locally delivered and verified via + ``on_execution_payload_envelope``. + """ + return root in store.payloads + + - name: is_pending_validator#gloas sources: - file: packages/state-transition/src/block/processDepositRequest.ts @@ -6017,12 +6758,11 @@ - file: packages/fork-choice/src/forkChoice/forkChoice.ts search: "* https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time" spec: | - + def is_proposing_on_time(store: Store) -> bool: seconds_since_genesis = store.time - store.genesis_time time_into_slot_ms = seconds_to_milliseconds(seconds_since_genesis) % SLOT_DURATION_MS - epoch = get_current_store_epoch(store) - proposer_reorg_cutoff_ms = get_proposer_reorg_cutoff_ms(epoch) + proposer_reorg_cutoff_ms = get_proposer_reorg_cutoff_ms() return time_into_slot_ms <= proposer_reorg_cutoff_ms @@ -6070,10 +6810,21 @@ ) +- name: is_start_slot_at_epoch#phase0 + sources: [] + spec: | + + def is_start_slot_at_epoch(slot: Slot) -> bool: + """ + Return ``True`` if ``slot`` is the start slot of an epoch. + """ + return compute_slots_since_epoch_start(slot) == 0 + + - name: is_supporting_vote#gloas sources: [] spec: | - + def is_supporting_vote(store: Store, node: ForkChoiceNode, message: LatestMessage) -> bool: """ Returns whether the vote ``message`` supports the chain containing the @@ -6083,7 +6834,8 @@ if node.root == message.root: if node.payload_status == PAYLOAD_STATUS_PENDING: return True - if message.slot <= block.slot: + assert message.slot >= block.slot + if message.slot == block.slot: return False if message.payload_present: return node.payload_status == PAYLOAD_STATUS_FULL @@ -6851,7 +7603,7 @@ - name: on_block#gloas sources: [] spec: | - + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: """ Run ``on_block`` upon receiving a new block. @@ -6860,17 +7612,10 @@ # Parent block must be known assert block.parent_root in store.block_states - # Check if this blocks builds on empty or full parent block - parent_block = store.blocks[block.parent_root] - bid = block.body.signed_execution_payload_bid.message - parent_bid = parent_block.body.signed_execution_payload_bid.message - # Make a copy of the state to avoid mutability issues + # If this block builds on the parent's full payload, that payload must + # have been verified by on_execution_payload_envelope if is_parent_node_full(store, block): - assert block.parent_root in store.payload_states - state = copy(store.payload_states[block.parent_root]) - else: - assert bid.parent_block_hash == parent_bid.parent_block_hash - state = copy(store.block_states[block.parent_root]) + assert is_payload_verified(store, block.parent_root) # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. current_slot = get_current_slot(store) @@ -6887,6 +7632,9 @@ ) assert store.finalized_checkpoint.root == finalized_checkpoint_block + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[block.parent_root]) + # Check the block is valid and compute the post-state block_root = hash_tree_root(block) state_transition(state, signed_block, True) @@ -6912,13 +7660,15 @@ compute_pulled_up_tip(store, block_root) -- name: on_execution_payload#gloas +- name: on_execution_payload_envelope#gloas sources: [] spec: | - - def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None: + + def on_execution_payload_envelope( + store: Store, signed_envelope: SignedExecutionPayloadEnvelope + ) -> None: """ - Run ``on_execution_payload`` upon receiving a new execution payload. + Run ``on_execution_payload_envelope`` upon receiving a new execution payload envelope. """ envelope = signed_envelope.message # The corresponding beacon block root needs to be known @@ -6928,23 +7678,24 @@ # If not, this payload MAY be queued and subsequently considered when blob data becomes available assert is_data_available(envelope.beacon_block_root) - # Make a copy of the state to avoid mutability issues - state = copy(store.block_states[envelope.beacon_block_root]) + state = store.block_states[envelope.beacon_block_root] - # Process the execution payload - process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) + # Verify the execution payload envelope + verify_execution_payload_envelope(state, signed_envelope, EXECUTION_ENGINE) - # Add new state for this payload to the store - store.payload_states[envelope.beacon_block_root] = state + # Add execution payload envelope to the store + store.payloads[envelope.beacon_block_root] = envelope -- name: on_execution_payload#heze +- name: on_execution_payload_envelope#heze sources: [] spec: | - - def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None: + + def on_execution_payload_envelope( + store: Store, signed_envelope: SignedExecutionPayloadEnvelope + ) -> None: """ - Run ``on_execution_payload`` upon receiving a new execution payload. + Run ``on_execution_payload_envelope`` upon receiving a new execution payload envelope. """ envelope = signed_envelope.message # The corresponding beacon block root needs to be known @@ -6954,11 +7705,10 @@ # If not, this payload MAY be queued and subsequently considered when blob data becomes available assert is_data_available(envelope.beacon_block_root) - # Make a copy of the state to avoid mutability issues - state = copy(store.block_states[envelope.beacon_block_root]) + state = store.block_states[envelope.beacon_block_root] - # Process the execution payload - process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) + # Verify the execution payload envelope + verify_execution_payload_envelope(state, signed_envelope, EXECUTION_ENGINE) # [New in Heze:EIP7805] # Check if this payload satisfies the inclusion list constraints @@ -6967,14 +7717,23 @@ store, state, envelope.beacon_block_root, envelope.payload, EXECUTION_ENGINE ) - # Add new state for this payload to the store - store.payload_states[envelope.beacon_block_root] = state + # Add execution payload envelope to the store + store.payloads[envelope.beacon_block_root] = envelope + + +- name: on_fast_confirmation#phase0 + sources: [] + spec: | + + def on_fast_confirmation(fcr_store: FastConfirmationStore) -> None: + update_fast_confirmation_variables(fcr_store) + fcr_store.confirmed_root = get_latest_confirmed(fcr_store) - name: on_inclusion_list#heze sources: [] spec: | - + def on_inclusion_list(store: Store, signed_inclusion_list: SignedInclusionList) -> None: """ Run ``on_inclusion_list`` upon receiving a new inclusion list. @@ -6983,8 +7742,7 @@ seconds_since_genesis = store.time - store.genesis_time time_into_slot_ms = seconds_to_milliseconds(seconds_since_genesis) % SLOT_DURATION_MS - epoch = get_current_store_epoch(store) - view_freeze_cutoff_ms = get_view_freeze_cutoff_ms(epoch) + view_freeze_cutoff_ms = get_view_freeze_cutoff_ms() is_before_view_freeze_cutoff = time_into_slot_ms < view_freeze_cutoff_ms process_inclusion_list(get_inclusion_list_store(), inclusion_list, is_before_view_freeze_cutoff) @@ -7285,25 +8043,45 @@ - name: prepare_execution_payload#gloas sources: [] spec: | - + def prepare_execution_payload( + # [New in Gloas:EIP7732] + store: Store, state: BeaconState, safe_block_hash: Hash32, finalized_block_hash: Hash32, suggested_fee_recipient: ExecutionAddress, execution_engine: ExecutionEngine, ) -> Optional[PayloadId]: + # [New in Gloas:EIP7732] + parent_bid = state.latest_execution_payload_bid + parent_root = hash_tree_root(state.latest_block_header) + if should_extend_payload(store, parent_root): + envelope = store.payloads[parent_root] + # Make a copy of the state to avoid mutability issues + state = copy(state) + # Apply parent payload before computing withdrawals + apply_parent_execution_payload(state, parent_bid, envelope.execution_requests) + withdrawals = get_expected_withdrawals(state).withdrawals + head_block_hash = parent_bid.block_hash + else: + withdrawals = state.payload_expected_withdrawals + head_block_hash = parent_bid.parent_block_hash + # Set the forkchoice head and initiate the payload build process payload_attributes = PayloadAttributes( timestamp=compute_time_at_slot(state, state.slot), prev_randao=get_randao_mix(state, get_current_epoch(state)), suggested_fee_recipient=suggested_fee_recipient, - withdrawals=get_expected_withdrawals(state).withdrawals, + # [Modified in Gloas:EIP7732] + withdrawals=withdrawals, parent_beacon_block_root=hash_tree_root(state.latest_block_header), + # [New in Gloas:EIP7843] + slot_number=state.slot, ) return execution_engine.notify_forkchoice_updated( # [Modified in Gloas:EIP7732] - head_block_hash=state.latest_block_hash, + head_block_hash=head_block_hash, safe_block_hash=safe_block_hash, finalized_block_hash=finalized_block_hash, payload_attributes=payload_attributes, @@ -7313,28 +8091,44 @@ - name: prepare_execution_payload#heze sources: [] spec: | - + def prepare_execution_payload( + store: Store, state: BeaconState, safe_block_hash: Hash32, finalized_block_hash: Hash32, suggested_fee_recipient: ExecutionAddress, execution_engine: ExecutionEngine, ) -> Optional[PayloadId]: + parent_bid = state.latest_execution_payload_bid + parent_root = hash_tree_root(state.latest_block_header) + if should_extend_payload(store, parent_root): + envelope = store.payloads[parent_root] + # Make a copy of the state to avoid mutability issues + state = copy(state) + # Apply parent payload before computing withdrawals + apply_parent_execution_payload(state, parent_bid, envelope.execution_requests) + withdrawals = get_expected_withdrawals(state).withdrawals + head_block_hash = parent_bid.block_hash + else: + withdrawals = state.payload_expected_withdrawals + head_block_hash = parent_bid.parent_block_hash + # Set the forkchoice head and initiate the payload build process payload_attributes = PayloadAttributes( timestamp=compute_time_at_slot(state, state.slot), prev_randao=get_randao_mix(state, get_current_epoch(state)), suggested_fee_recipient=suggested_fee_recipient, - withdrawals=get_expected_withdrawals(state).withdrawals, + withdrawals=withdrawals, parent_beacon_block_root=hash_tree_root(state.latest_block_header), + slot_number=state.slot, # [New in Heze:EIP7805] inclusion_list_transactions=get_inclusion_list_transactions( get_inclusion_list_store(), state, Slot(state.slot - 1) ), ) return execution_engine.notify_forkchoice_updated( - head_block_hash=state.latest_block_hash, + head_block_hash=head_block_hash, safe_block_hash=safe_block_hash, finalized_block_hash=finalized_block_hash, payload_attributes=payload_attributes, @@ -7729,8 +8523,10 @@ - name: process_block#gloas sources: [] spec: | - + def process_block(state: BeaconState, block: BeaconBlock) -> None: + # [New in Gloas:EIP7732] + process_parent_execution_payload(state, block) process_block_header(state, block) # [Modified in Gloas:EIP7732] process_withdrawals(state) @@ -8102,7 +8898,7 @@ - file: packages/state-transition/src/epoch/index.ts search: export function processEpoch( spec: | - + def process_epoch(state: BeaconState) -> None: # [Modified in Altair] process_justification_and_finalization(state) @@ -8118,6 +8914,8 @@ process_slashings_reset(state) process_randao_mixes_reset(state) process_historical_roots_update(state) + # [Modified in Altair] + # Removed `process_participation_record_updates` # [New in Altair] process_participation_flag_updates(state) # [New in Altair] @@ -8129,7 +8927,7 @@ - file: packages/state-transition/src/epoch/index.ts search: export function processEpoch( spec: | - + def process_epoch(state: BeaconState) -> None: process_justification_and_finalization(state) process_inactivity_updates(state) @@ -8140,7 +8938,9 @@ process_effective_balance_updates(state) process_slashings_reset(state) process_randao_mixes_reset(state) - # [Modified in Capella] + # [Modified in Altair] + # Removed `process_historical_roots_update` + # [New in Capella] process_historical_summaries_update(state) process_participation_flag_updates(state) process_sync_committee_updates(state) @@ -8522,89 +9322,60 @@ - name: process_execution_payload#gloas sources: [] spec: | - + def process_execution_payload( - state: BeaconState, - # [Modified in Gloas:EIP7732] - # Removed `body` - # [New in Gloas:EIP7732] - signed_envelope: SignedExecutionPayloadEnvelope, - execution_engine: ExecutionEngine, - # [New in Gloas:EIP7732] - verify: bool = True, + state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine ) -> None: - envelope = signed_envelope.message - payload = envelope.payload - - # Verify signature - if verify: - assert verify_execution_payload_envelope_signature(state, signed_envelope) - - # Cache latest block header state root - previous_state_root = hash_tree_root(state) - if state.latest_block_header.state_root == Root(): - state.latest_block_header.state_root = previous_state_root - - # Verify consistency with the beacon block - assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header) - assert envelope.slot == state.slot - - # Verify consistency with the committed bid - committed_bid = state.latest_execution_payload_bid - assert envelope.builder_index == committed_bid.builder_index - assert committed_bid.prev_randao == payload.prev_randao - - # Verify consistency with expected withdrawals - assert hash_tree_root(payload.withdrawals) == hash_tree_root(state.payload_expected_withdrawals) + payload = body.execution_payload - # Verify the gas_limit - assert committed_bid.gas_limit == payload.gas_limit - # Verify the block hash - assert committed_bid.block_hash == payload.block_hash - # Verify consistency of the parent hash with respect to the previous execution payload - assert payload.parent_hash == state.latest_block_hash + # Verify consistency of the parent hash with respect to the previous execution payload header + assert payload.parent_hash == state.latest_execution_payload_header.block_hash + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) # Verify timestamp assert payload.timestamp == compute_time_at_slot(state, state.slot) - # Verify the execution payload is valid + # [Modified in Fulu:EIP7892] + # Verify commitments are under limit + assert ( + len(body.blob_kzg_commitments) + <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block + ) + + # Compute list of versioned hashes versioned_hashes = [ - kzg_commitment_to_versioned_hash(commitment) - # [Modified in Gloas:EIP7732] - for commitment in committed_bid.blob_kzg_commitments + kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments ] - requests = envelope.execution_requests + + # Verify the execution payload is valid assert execution_engine.verify_and_notify_new_payload( NewPayloadRequest( execution_payload=payload, versioned_hashes=versioned_hashes, parent_beacon_block_root=state.latest_block_header.parent_root, - execution_requests=requests, + execution_requests=body.execution_requests, ) ) - def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: - for operation in operations: - fn(state, operation) - - for_ops(requests.deposits, process_deposit_request) - for_ops(requests.withdrawals, process_withdrawal_request) - for_ops(requests.consolidations, process_consolidation_request) - - # Queue the builder payment - payment = state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] - amount = payment.withdrawal.amount - if amount > 0: - state.builder_pending_withdrawals.append(payment.withdrawal) - state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] = ( - BuilderPendingPayment() + # Cache execution payload header + state.latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + withdrawals_root=hash_tree_root(payload.withdrawals), + blob_gas_used=payload.blob_gas_used, + excess_blob_gas=payload.excess_blob_gas, ) - - # Cache the execution payload hash - state.execution_payload_availability[state.slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1 - state.latest_block_hash = payload.block_hash - - # Verify the state root - if verify: - assert envelope.state_root == hash_tree_root(state) - name: process_execution_payload_bid#gloas @@ -9040,6 +9811,27 @@ for_ops(body.payload_attestations, process_payload_attestation) +- name: process_parent_execution_payload#gloas + sources: [] + spec: | + + def process_parent_execution_payload(state: BeaconState, block: BeaconBlock) -> None: + bid = block.body.signed_execution_payload_bid.message + parent_bid = state.latest_execution_payload_bid + requests = block.body.parent_execution_requests + + is_genesis_block = parent_bid.block_hash == Hash32() + is_parent_block_empty = bid.parent_block_hash != parent_bid.block_hash + if is_genesis_block or is_parent_block_empty: + # Parent was EMPTY -- no execution requests expected + assert requests == ExecutionRequests() + return + + # Parent was FULL -- verify the bid commitment and apply the payload + assert hash_tree_root(requests) == parent_bid.execution_requests_root + apply_parent_execution_payload(state, parent_bid, requests) + + - name: process_participation_flag_updates#altair sources: - file: packages/state-transition/src/epoch/processParticipationFlagUpdates.ts @@ -9986,7 +10778,7 @@ - name: process_withdrawals#gloas sources: [] spec: | - + def process_withdrawals( state: BeaconState, # [Modified in Gloas:EIP7732] @@ -9994,7 +10786,9 @@ ) -> None: # [New in Gloas:EIP7732] # Return early if the parent block is empty - if not is_parent_block_full(state): + is_genesis_block = state.latest_block_hash == Hash32() + is_parent_block_empty = state.latest_block_hash != state.latest_execution_payload_bid.block_hash + if is_genesis_block or is_parent_block_empty: return # Get expected withdrawals @@ -10043,13 +10837,12 @@ - name: record_block_timeliness#phase0 sources: [] spec: | - + def record_block_timeliness(store: Store, root: Root) -> None: block = store.blocks[root] seconds_since_genesis = store.time - store.genesis_time time_into_slot_ms = seconds_to_milliseconds(seconds_since_genesis) % SLOT_DURATION_MS - epoch = get_current_store_epoch(store) - attestation_threshold_ms = get_attestation_due_ms(epoch) + attestation_threshold_ms = get_attestation_due_ms() is_before_attesting_interval = time_into_slot_ms < attestation_threshold_ms is_timely = get_current_slot(store) == block.slot and is_before_attesting_interval store.block_timeliness[root] = is_timely @@ -10058,16 +10851,15 @@ - name: record_block_timeliness#gloas sources: [] spec: | - + def record_block_timeliness(store: Store, root: Root) -> None: block = store.blocks[root] seconds_since_genesis = store.time - store.genesis_time time_into_slot_ms = seconds_to_milliseconds(seconds_since_genesis) % SLOT_DURATION_MS - epoch = get_current_store_epoch(store) - attestation_threshold_ms = get_attestation_due_ms(epoch) + attestation_threshold_ms = get_attestation_due_ms() # [New in Gloas:EIP7732] is_current_slot = get_current_slot(store) == block.slot - ptc_threshold_ms = get_payload_attestation_due_ms(epoch) + ptc_threshold_ms = get_payload_attestation_due_ms() # [Modified in Gloas:EIP7732] store.block_timeliness[root] = [ is_current_slot and time_into_slot_ms < threshold @@ -10127,17 +10919,6 @@ return matrix -- name: saturating_sub#phase0 - sources: [] - spec: | - - def saturating_sub(a: int, b: int) -> int: - """ - Computes a - b, saturating at numeric bounds. - """ - return a - b if a > b else 0 - - - name: seconds_to_milliseconds#phase0 sources: [] spec: | @@ -10163,6 +10944,18 @@ list[index] = value +- name: settle_builder_payment#gloas + sources: [] + spec: | + + def settle_builder_payment(state: BeaconState, payment_index: uint64) -> None: + assert payment_index < len(state.builder_pending_payments) + payment = state.builder_pending_payments[payment_index] + if payment.withdrawal.amount > 0: + state.builder_pending_withdrawals.append(payment.withdrawal) + state.builder_pending_payments[payment_index] = BuilderPendingPayment() + + - name: should_apply_proposer_boost#gloas sources: [] spec: | @@ -10203,8 +10996,10 @@ - name: should_extend_payload#gloas sources: [] spec: | - + def should_extend_payload(store: Store, root: Root) -> bool: + if not is_payload_verified(store, root): + return False proposer_root = store.proposer_boost_root return ( (is_payload_timely(store, root) and is_payload_data_available(store, root)) @@ -10551,6 +11346,32 @@ store.finalized_checkpoint = finalized_checkpoint +- name: update_fast_confirmation_variables#phase0 + sources: [] + spec: | + + def update_fast_confirmation_variables(fcr_store: FastConfirmationStore) -> None: + # Update prev and curr slot head + store = fcr_store.store + fcr_store.previous_slot_head = fcr_store.current_slot_head + fcr_store.current_slot_head = get_head(store) + + # Update greatest unrealized justified checkpoint at the last slot of an epoch + if is_start_slot_at_epoch(Slot(get_current_slot(store) + 1)): + fcr_store.previous_epoch_greatest_unrealized_checkpoint = ( + store.unrealized_justified_checkpoint + ) + + # Update observed justified checkpoints at the start of an epoch + if is_start_slot_at_epoch(get_current_slot(store)): + fcr_store.previous_epoch_observed_justified_checkpoint = ( + fcr_store.current_epoch_observed_justified_checkpoint + ) + fcr_store.current_epoch_observed_justified_checkpoint = ( + fcr_store.previous_epoch_greatest_unrealized_checkpoint + ) + + - name: update_latest_messages#phase0 sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts @@ -11462,7 +12283,7 @@ - file: packages/state-transition/src/slot/upgradeStateToGloas.ts search: export function upgradeStateToGloas( spec: | - + def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: epoch = fulu.get_current_epoch(pre) @@ -11499,9 +12320,7 @@ # [Modified in Gloas:EIP7732] # Removed `latest_execution_payload_header` # [New in Gloas:EIP7732] - latest_execution_payload_bid=ExecutionPayloadBid( - block_hash=pre.latest_execution_payload_header.block_hash, - ), + latest_block_hash=pre.latest_execution_payload_header.block_hash, next_withdrawal_index=pre.next_withdrawal_index, next_withdrawal_validator_index=pre.next_withdrawal_validator_index, historical_summaries=pre.historical_summaries, @@ -11526,7 +12345,10 @@ # [New in Gloas:EIP7732] builder_pending_withdrawals=[], # [New in Gloas:EIP7732] - latest_block_hash=pre.latest_execution_payload_header.block_hash, + latest_execution_payload_bid=ExecutionPayloadBid( + block_hash=pre.latest_execution_payload_header.block_hash, + execution_requests_root=hash_tree_root(ExecutionRequests()), + ), # [New in Gloas:EIP7732] payload_expected_withdrawals=[], # [New in Gloas:EIP7732] @@ -11542,7 +12364,7 @@ - name: upgrade_to_heze#heze sources: [] spec: | - + def upgrade_to_heze(pre: gloas.BeaconState) -> BeaconState: epoch = gloas.get_current_epoch(pre) latest_execution_payload_bid = ExecutionPayloadBid( @@ -11557,6 +12379,7 @@ value=pre.latest_execution_payload_bid.value, execution_payment=pre.latest_execution_payload_bid.execution_payment, blob_kzg_commitments=pre.latest_execution_payload_bid.blob_kzg_commitments, + execution_requests_root=pre.latest_execution_payload_bid.execution_requests_root, # [New in Heze:EIP7805] inclusion_list_bits=Bitvector[INCLUSION_LIST_COMMITTEE_SIZE](), ) @@ -11591,8 +12414,7 @@ inactivity_scores=pre.inactivity_scores, current_sync_committee=pre.current_sync_committee, next_sync_committee=pre.next_sync_committee, - # [Modified in Heze:EIP7805] - latest_execution_payload_bid=latest_execution_payload_bid, + latest_block_hash=pre.latest_block_hash, next_withdrawal_index=pre.next_withdrawal_index, next_withdrawal_validator_index=pre.next_withdrawal_validator_index, historical_summaries=pre.historical_summaries, @@ -11611,7 +12433,8 @@ execution_payload_availability=pre.execution_payload_availability, builder_pending_payments=pre.builder_pending_payments, builder_pending_withdrawals=pre.builder_pending_withdrawals, - latest_block_hash=pre.latest_block_hash, + # [Modified in Heze:EIP7805] + latest_execution_payload_bid=latest_execution_payload_bid, payload_expected_withdrawals=pre.payload_expected_withdrawals, ptc_window=pre.ptc_window, ) @@ -11802,7 +12625,7 @@ - name: validate_on_attestation#gloas sources: [] spec: | - + def validate_on_attestation(store: Store, attestation: Attestation, is_from_block: bool) -> None: target = attestation.data.target @@ -11833,7 +12656,7 @@ # [New in Gloas:EIP7732] # If attesting for a full node, the payload must be known if attestation.data.index == 1: - assert attestation.data.beacon_block_root in store.payload_states + assert is_payload_verified(store, attestation.data.beacon_block_root) # LMD vote must be consistent with FFG vote target assert target.root == get_checkpoint_block( @@ -12042,6 +12865,52 @@ return bls.Verify(builder.pubkey, signing_root, signed_bid.signature) +- name: verify_execution_payload_envelope#gloas + sources: [] + spec: | + + def verify_execution_payload_envelope( + state: BeaconState, + signed_envelope: SignedExecutionPayloadEnvelope, + execution_engine: ExecutionEngine, + ) -> None: + envelope = signed_envelope.message + payload = envelope.payload + + # Verify signature + assert verify_execution_payload_envelope_signature(state, signed_envelope) + + # Verify consistency with the beacon block + header = copy(state.latest_block_header) + header.state_root = hash_tree_root(state) + assert envelope.beacon_block_root == hash_tree_root(header) + + # Verify consistency with the committed bid + bid = state.latest_execution_payload_bid + assert envelope.builder_index == bid.builder_index + assert payload.prev_randao == bid.prev_randao + assert payload.gas_limit == bid.gas_limit + assert payload.block_hash == bid.block_hash + assert hash_tree_root(envelope.execution_requests) == bid.execution_requests_root + + # Verify the execution payload is valid + assert payload.slot_number == state.slot + assert payload.parent_hash == state.latest_block_hash + assert payload.timestamp == compute_time_at_slot(state, state.slot) + assert hash_tree_root(payload.withdrawals) == hash_tree_root(state.payload_expected_withdrawals) + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=[ + kzg_commitment_to_versioned_hash(commitment) + for commitment in bid.blob_kzg_commitments + ], + parent_beacon_block_root=state.latest_block_header.parent_root, + execution_requests=envelope.execution_requests, + ) + ) + + - name: verify_execution_payload_envelope_signature#gloas sources: - file: packages/state-transition/src/block/processExecutionPayloadEnvelope.ts @@ -12123,6 +12992,39 @@ state.finalized_checkpoint = old_current_justified_checkpoint +- name: will_current_target_be_justified#phase0 + sources: [] + spec: | + + def will_current_target_be_justified(store: Store) -> bool: + """ + Return ``True`` if and only if the current target will eventually be justified. + """ + state = get_pulled_up_head_state(store) + total_active_balance = get_total_active_balance(state) + honest_ffg_support = compute_honest_ffg_support_for_current_target(store) + return 3 * honest_ffg_support >= 2 * total_active_balance + + +- name: will_no_conflicting_checkpoint_be_justified#phase0 + sources: [] + spec: | + + def will_no_conflicting_checkpoint_be_justified(store: Store) -> bool: + """ + Return ``True`` if and only if no checkpoint conflicting with the current target can ever be justified. + """ + + # If the target is unrealized justified then no conflicting checkpoint can be justified + if get_current_target(store) == store.unrealized_justified_checkpoint: + return True + + state = get_pulled_up_head_state(store) + total_active_balance = get_total_active_balance(state) + honest_ffg_support = compute_honest_ffg_support_for_current_target(store) + return 3 * honest_ffg_support > 1 * total_active_balance + + - name: xor#phase0 sources: - file: packages/utils/src/bytes/browser.ts diff --git a/specrefs/types.yml b/specrefs/types.yml index c5d28cf5881d..b0f7623bad52 100644 --- a/specrefs/types.yml +++ b/specrefs/types.yml @@ -34,6 +34,13 @@ BlobIndex = uint64 +- name: BlockAccessList#gloas + sources: [] + spec: | + + BlockAccessList = ByteList[MAX_BYTES_PER_TRANSACTION] + + - name: BuilderIndex#gloas sources: - file: packages/types/src/primitive/sszTypes.ts From e49b6991ad85059b1805ae26e4291a8993b682f7 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:40:15 -0700 Subject: [PATCH 08/61] lint --- .../src/block/processExecutionPayloadEnvelope.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts index b410c0a87fa3..30f3788358c5 100644 --- a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts +++ b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts @@ -24,7 +24,7 @@ export function processExecutionPayloadEnvelope( signedEnvelope: gloas.SignedExecutionPayloadEnvelope, opts?: ProcessExecutionPayloadEnvelopeOpts ): CachedBeaconStateGloas { - const {verifySignature = true, verifyStateRoot = true} = opts ?? {}; + const {verifySignature = true} = opts ?? {}; const envelope = signedEnvelope.message; const payload = envelope.payload; const fork = state.config.getForkSeq(payload.slotNumber); @@ -74,7 +74,6 @@ export function processExecutionPayloadEnvelope( postState.commit(); - return postState; } From 813cb524643dfa4b6eed6a6351a9230dd0dfa8e6 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:53:28 -0700 Subject: [PATCH 09/61] ethspecify --- specrefs/.ethspecify.yml | 55 ++++++++++++++++++++++++++++++++++++++++ specrefs/containers.yml | 4 ++- specrefs/dataclasses.yml | 4 ++- specrefs/types.yml | 4 ++- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml index e40c5a2a1e9b..4f0f00f8b4bd 100644 --- a/specrefs/.ethspecify.yml +++ b/specrefs/.ethspecify.yml @@ -51,6 +51,9 @@ exceptions: # heze (not implemented) - DOMAIN_INCLUSION_LIST_COMMITTEE#heze + # phase0 fast confirmation / not implemented + - COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR#phase0 + containers: # gloas - ForkChoiceNode#gloas @@ -87,6 +90,9 @@ exceptions: - LatestMessage#gloas - Store#gloas + # phase0 fast confirmation / not implemented + - FastConfirmationStore#phase0 + # heze (not implemented) - GetInclusionListResponse#heze - InclusionListStore#heze @@ -368,7 +374,56 @@ exceptions: - should_extend_payload#heze - upgrade_to_heze#heze + # phase0 fast confirmation / not implemented + - adjust_committee_weight_estimate_to_ensure_safety#phase0 + - compute_adversarial_weight#phase0 + - compute_empty_slot_support_discount#phase0 + - compute_honest_ffg_support_for_current_target#phase0 + - compute_safety_threshold#phase0 + - compute_shuffled_permutation#phase0 + - estimate_committee_weight_between_slots#phase0 + - find_latest_confirmed_descendant#phase0 + - get_adversarial_weight#phase0 + - get_ancestor_roots#phase0 + - get_block_epoch#phase0 + - get_block_slot#phase0 + - get_block_support_between_slots#phase0 + - get_checkpoint_for_block#phase0 + - get_current_balance_source#phase0 + - get_current_target#phase0 + - get_current_target_score#phase0 + - get_equivocation_score#phase0 + - get_fast_confirmation_store#phase0 + - get_latest_confirmed#phase0 + - get_latest_message_epoch#phase0 + - get_latest_message_epoch#gloas + - get_previous_balance_source#phase0 + - get_pulled_up_head_state#phase0 + - get_slot_committee#phase0 + - get_support_discount#phase0 + - is_ancestor#phase0 + - is_confirmed_chain_safe#phase0 + - is_full_validator_set_covered#phase0 + - is_one_confirmed#phase0 + - is_start_slot_at_epoch#phase0 + - on_fast_confirmation#phase0 + - update_fast_confirmation_variables#phase0 + - will_current_target_be_justified#phase0 + - will_no_conflicting_checkpoint_be_justified#phase0 + + # gloas / heze empty sources to skip + - apply_parent_execution_payload#gloas + - is_payload_verified#gloas + - on_execution_payload_envelope#gloas + - on_execution_payload_envelope#heze + - process_parent_execution_payload#gloas + - settle_builder_payment#gloas + - verify_execution_payload_envelope#gloas + configs: + # phase0 fast confirmation / not implemented + - CONFIRMATION_BYZANTINE_THRESHOLD#phase0 + # heze (not implemented) - HEZE_FORK_EPOCH#heze - HEZE_FORK_VERSION#heze diff --git a/specrefs/containers.yml b/specrefs/containers.yml index e674c487259b..7968f72e000b 100644 --- a/specrefs/containers.yml +++ b/specrefs/containers.yml @@ -987,7 +987,9 @@ - name: ExecutionPayload#gloas - sources: [] + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const ExecutionPayload = spec: | class ExecutionPayload(Container): diff --git a/specrefs/dataclasses.yml b/specrefs/dataclasses.yml index 816b1612943c..5b95a996a98c 100644 --- a/specrefs/dataclasses.yml +++ b/specrefs/dataclasses.yml @@ -325,7 +325,9 @@ - name: PayloadAttributes#gloas - sources: [] + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const PayloadAttributes = spec: | class PayloadAttributes(object): diff --git a/specrefs/types.yml b/specrefs/types.yml index b0f7623bad52..6cecebadd0cd 100644 --- a/specrefs/types.yml +++ b/specrefs/types.yml @@ -35,7 +35,9 @@ - name: BlockAccessList#gloas - sources: [] + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const BlockAccessList = spec: | BlockAccessList = ByteList[MAX_BYTES_PER_TRANSACTION] From 845bec97bbc53929b90a85db096d91c89c5b7e64 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:20:01 -0700 Subject: [PATCH 10/61] fix unit test --- packages/api/test/unit/beacon/oapiSpec.test.ts | 2 ++ packages/api/test/unit/beacon/testData/events.ts | 1 + packages/beacon-node/src/util/sszBytes.ts | 8 ++++---- packages/beacon-node/test/unit/util/sszBytes.test.ts | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/api/test/unit/beacon/oapiSpec.test.ts b/packages/api/test/unit/beacon/oapiSpec.test.ts index 19d1432aaf2c..f4aae5dd52af 100644 --- a/packages/api/test/unit/beacon/oapiSpec.test.ts +++ b/packages/api/test/unit/beacon/oapiSpec.test.ts @@ -62,6 +62,7 @@ const ignoredOperations = [ "getPtcDuties", "producePayloadAttestationData", "getExecutionPayloadBid", + "getSignedExecutionPayloadEnvelope", ]; const ignoredProperties: Record = { @@ -78,6 +79,7 @@ runTestCheckAgainstSpec(openApiJson, definitions, testDatas, ignoredOperations, const ignoredTopics: string[] = [ // TODO GLOAS: required by v5.0.0-alpha.1 "payload_attestation_message", + "execution_payload_bid", ]; // eventstream types are defined as comments in the description of "examples". diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index a6c8c06ad055..9eebe475e13c 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -308,6 +308,7 @@ export const eventTestData: EventData = { blob_kzg_commitments: [ "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2", ], + execution_requests_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", }, signature: "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index bb0d988a0ccb..fa9bf569577a 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -561,7 +561,7 @@ export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Arr * ├─ 8 bytes: builderIndex (offset 108-115) * ├─ 32 bytes: beaconBlockRoot (offset 116-147) * └─ variable: payload data (starts at envelope + 48) - * └─ ExecutionPayload fixed portion includes slotNumber at offset 528 + * └─ ExecutionPayload fixed portion includes slotNumber at offset 532 */ const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET = 4; const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE = 96; @@ -587,15 +587,15 @@ const EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE = // parentHash(32) + feeRecipient(20) + stateRoot(32) + receiptsRoot(32) + logsBloom(256) + // prevRandao(32) + blockNumber(8) + gasLimit(8) + gasUsed(8) + timestamp(8) + // extraData_offset(4) + baseFeePerGas(32) + blockHash(32) + transactions_offset(4) + -// withdrawals_offset(4) + blobGasUsed(8) + excessBlobGas(8) = 528 -const SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD = 528; +// withdrawals_offset(4) + blobGasUsed(8) + excessBlobGas(8) + blockAccessList_offset(4) = 532 +const SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD = 532; // Payload data starts right after the envelope's fixed portion const ENVELOPE_START_IN_SIGNED = SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE; // 100 const SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE = - ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 48 + 528 = 676 + ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 48 + 532 = 680 export function getSlotFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): Slot | null { if (data.length < SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + SLOT_SIZE) { diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index 2fa572d9eca3..6ab13f8df70c 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -581,8 +581,8 @@ describe("SignedExecutionPayloadEnvelope SSZ serialized picking", () => { } it("getSlotFromExecutionPayloadEnvelopeSerialized - invalid data", () => { - // slotNumber is at offset 676 within the serialized payload, need at least 684 bytes - const invalidSizes = [0, 50, 100, 683]; + // slotNumber is at offset 680 within the serialized payload, need at least 688 bytes + const invalidSizes = [0, 50, 100, 687]; for (const size of invalidSizes) { expect(getSlotFromExecutionPayloadEnvelopeSerialized(Buffer.alloc(size))).toBeNull(); } From 1d15ece3b095437435670379e19b12c4f4d0d4d8 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:22:14 -0700 Subject: [PATCH 11/61] fix e2e --- packages/config/test/e2e/ensure-config-is-synced.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/config/test/e2e/ensure-config-is-synced.test.ts b/packages/config/test/e2e/ensure-config-is-synced.test.ts index a37e793f3c55..2c1604f8e268 100644 --- a/packages/config/test/e2e/ensure-config-is-synced.test.ts +++ b/packages/config/test/e2e/ensure-config-is-synced.test.ts @@ -28,6 +28,8 @@ const ignoredRemoteConfigFields: (keyof ChainConfig)[] = [ // Future spec params not yet in Lodestar "EPOCHS_PER_SHUFFLING_PHASE" as keyof ChainConfig, "PROPOSER_SELECTION_GAP" as keyof ChainConfig, + // FCR params - not yet implemented in Lodestar + "CONFIRMATION_BYZANTINE_THRESHOLD" as keyof ChainConfig, // Future forks not yet implemented in Lodestar "HEZE_FORK_VERSION" as keyof ChainConfig, "HEZE_FORK_EPOCH" as keyof ChainConfig, From c3d429dc3db1bb6189a4aedb7b011424923f47d8 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:07:02 -0700 Subject: [PATCH 12/61] address comment --- packages/beacon-node/test/spec/presets/fork_choice.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 6988c9ba279c..c37c4aec1eef 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -393,7 +393,7 @@ const forkChoiceTest = beaconBlockRoot, blockHash, blockNumber, - "0x", + ZERO_HASH_HEX, ExecutionStatus.Valid ); if (!isValid) throw Error("Expect error since this is a negative test"); From afbb9c1aa8a34fdf908bb6571ea2ac7c580f49ff Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:51:41 -0700 Subject: [PATCH 13/61] feat: add processParentExecutionPayload and remove withdrawal early return New function implements deferred parent execution payload processing (consensus-specs#5094). Moves execution requests, builder payment, and availability updates from envelope processing to block processing. Removes the isParentBlockFull early return in processWithdrawals since processParentExecutionPayload now runs before withdrawals. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../block/processParentExecutionPayload.ts | 108 ++++++++++++++++++ .../src/block/processWithdrawals.ts | 13 +-- 2 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 packages/state-transition/src/block/processParentExecutionPayload.ts diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts new file mode 100644 index 000000000000..30149c8fec14 --- /dev/null +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -0,0 +1,108 @@ +import {ForkPostGloas, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import {BeaconBlock, ssz} from "@lodestar/types"; +import {byteArrayEquals, toRootHex} from "@lodestar/utils"; +import {CachedBeaconStateGloas} from "../types.js"; +import {computeEpochAtSlot} from "../util/epoch.js"; +import {processConsolidationRequest} from "./processConsolidationRequest.js"; +import {getPendingValidatorPubkeys, processDepositRequest} from "./processDepositRequest.js"; +import {processWithdrawalRequest} from "./processWithdrawalRequest.js"; + +/** + * Process parent execution payload effects as first step of processBlock. + * + * Spec: consensus-specs#5094 + * https://github.com/ethereum/consensus-specs/blob/26ed32e/specs/gloas/beacon-chain.md + */ +export function processParentExecutionPayload( + state: CachedBeaconStateGloas, + block: BeaconBlock +): void { + const bid = block.body.signedExecutionPayloadBid.message; + const parentBid = state.latestExecutionPayloadBid; + const requests = block.body.parentExecutionRequests; + + // True if this block built on the parent's full payload + const isParentFull = byteArrayEquals(bid.parentBlockHash, parentBid.blockHash); + + if (!isParentFull) { + // Parent was EMPTY -- no execution requests expected + assertEmptyExecutionRequests(requests); + return; + } + + // Parent was FULL -- verify the bid commitment and apply the payload + const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(requests); + if (!byteArrayEquals(requestsRoot, parentBid.executionRequestsRoot)) { + throw new Error( + `Parent execution requests root mismatch actual=${toRootHex(requestsRoot)} expected=${toRootHex(parentBid.executionRequestsRoot)}` + ); + } + + applyParentExecutionPayload(state, parentBid, requests); +} + +/** + * Apply parent execution payload effects to state. + * + * Spec: apply_parent_execution_payload + */ +function applyParentExecutionPayload( + state: CachedBeaconStateGloas, + parentBid: {slot: number; blockHash: Uint8Array; builderIndex: number}, + requests: {deposits: unknown[]; withdrawals: unknown[]; consolidations: unknown[]} +): void { + const fork = state.config.getForkSeq(state.slot); + const parentSlot = parentBid.slot; + const parentEpoch = computeEpochAtSlot(parentSlot); + const currentEpoch = computeEpochAtSlot(state.slot); + + // Process execution requests from parent's payload + // Execution requests are processed at state.slot (child's slot), not parent's slot + if (requests.deposits.length > 0) { + const pendingValidatorPubkeys = getPendingValidatorPubkeys(state.config, state); + for (const deposit of requests.deposits) { + processDepositRequest(fork, state, deposit, pendingValidatorPubkeys); + } + } + + for (const withdrawal of requests.withdrawals) { + processWithdrawalRequest(fork, state, withdrawal); + } + + for (const consolidation of requests.consolidations) { + processConsolidationRequest(state, consolidation); + } + + // Queue the builder payment + let paymentIndex: number | null; + if (parentEpoch === currentEpoch) { + paymentIndex = SLOTS_PER_EPOCH + (parentSlot % SLOTS_PER_EPOCH); + } else if (parentEpoch === currentEpoch - 1) { + paymentIndex = parentSlot % SLOTS_PER_EPOCH; + } else { + // Parent is older than previous epoch — payment already settled/evicted + paymentIndex = null; + } + + if (paymentIndex !== null) { + const payment = state.builderPendingPayments.get(paymentIndex).clone(); + if (payment.withdrawal.amount > 0) { + state.builderPendingWithdrawals.push(payment.withdrawal); + } + state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU()); + } + + // Update parent payload availability and latest block hash + state.executionPayloadAvailability.set(parentSlot % SLOTS_PER_HISTORICAL_ROOT, true); + state.latestBlockHash = parentBid.blockHash; +} + +function assertEmptyExecutionRequests(requests: { + deposits: unknown[]; + withdrawals: unknown[]; + consolidations: unknown[]; +}): void { + if (requests.deposits.length !== 0 || requests.withdrawals.length !== 0 || requests.consolidations.length !== 0) { + throw new Error("Parent execution requests must be empty when parent block is EMPTY"); + } +} diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index e2b54f5e183a..ba731b51d141 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -11,12 +11,7 @@ import { import {BuilderIndex, ValidatorIndex, capella, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex} from "@lodestar/utils"; import {CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; -import { - convertBuilderIndexToValidatorIndex, - convertValidatorIndexToBuilderIndex, - isBuilderIndex, - isParentBlockFull, -} from "../util/gloas.js"; +import {convertBuilderIndexToValidatorIndex, convertValidatorIndexToBuilderIndex, isBuilderIndex} from "../util/gloas.js"; import { decreaseBalance, getMaxEffectiveBalance, @@ -31,10 +26,8 @@ export function processWithdrawals( state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas, payload?: capella.FullOrBlindedExecutionPayload ): void { - // Return early if the parent block is empty - if (fork >= ForkSeq.gloas && !isParentBlockFull(state as CachedBeaconStateGloas)) { - return; - } + // After consensus-specs#5094, processParentExecutionPayload has already handled parent effects + // before processWithdrawals is called, so no early return needed for Gloas. // processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732) // processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) From 311eeca5ddf846b77e1ec9855bfd3e6e7de7a800 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:52:27 -0700 Subject: [PATCH 14/61] refactor: add processParentExecutionPayload as first step in processBlock New ordering for Gloas blocks: 1. processParentExecutionPayload (NEW - before header) 2. processBlockHeader 3. processWithdrawals (no longer has empty-parent early return) 4. processExecutionPayloadBid 5. ... rest unchanged Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/state-transition/src/block/index.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/state-transition/src/block/index.ts b/packages/state-transition/src/block/index.ts index 2dc24d48bd5b..71633419e828 100644 --- a/packages/state-transition/src/block/index.ts +++ b/packages/state-transition/src/block/index.ts @@ -16,6 +16,7 @@ import {processExecutionPayload} from "./processExecutionPayload.js"; import {processExecutionPayloadBid} from "./processExecutionPayloadBid.js"; import {processExecutionPayloadEnvelope} from "./processExecutionPayloadEnvelope.js"; import {processOperations} from "./processOperations.js"; +import {processParentExecutionPayload} from "./processParentExecutionPayload.js"; import {processPayloadAttestation} from "./processPayloadAttestation.js"; import {processRandao} from "./processRandao.js"; import {processSyncAggregate} from "./processSyncCommittee.js"; @@ -33,6 +34,7 @@ export { processExecutionPayloadBid, processPayloadAttestation, processExecutionPayloadEnvelope, + processParentExecutionPayload, }; export * from "./externalData.js"; @@ -51,10 +53,16 @@ export function processBlock( ): void { const {verifySignatures = true} = opts ?? {}; + // Process parent execution payload effects first (consensus-specs#5094) + // Must run before processBlockHeader and processExecutionPayloadBid + if (fork >= ForkSeq.gloas) { + processParentExecutionPayload(state as CachedBeaconStateGloas, block as BeaconBlock); + } + processBlockHeader(state, block); if (fork >= ForkSeq.gloas) { - // After gloas, processWithdrawals does not take a payload parameter + // After consensus-specs#5094, processParentExecutionPayload has already handled parent effects processWithdrawals(fork, state as CachedBeaconStateGloas); } else if (fork >= ForkSeq.capella) { const fullOrBlindedPayload = getFullOrBlindedPayload(block); From e2d3ad22c2515c17b6213f390e3ddb6c65b99eed Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:53:35 -0700 Subject: [PATCH 15/61] refactor: transform processExecutionPayloadEnvelope to pure verification Envelope verification no longer mutates state. All state effects (execution requests, builder payment, availability, latestBlockHash) have moved to processParentExecutionPayload in the next block. Changes: - Remove state cloning, execution request processing, builder payment, availability/latestBlockHash updates, and state root verification - Add executionRequestsRoot check against bid commitment - Return void instead of post-state - Remove verifyStateRoot and dontTransferCache options - Rename stateView method to verifyExecutionPayloadEnvelope Co-Authored-By: Claude Opus 4.6 (1M context) --- .../block/processExecutionPayloadEnvelope.ts | 88 ++++++------------- .../src/stateView/beaconStateView.ts | 13 +-- .../src/stateView/interface.ts | 4 +- 3 files changed, 35 insertions(+), 70 deletions(-) diff --git a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts index 30f3788358c5..1a0de2ea2126 100644 --- a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts +++ b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts @@ -1,4 +1,3 @@ -import {SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import {gloas, ssz} from "@lodestar/types"; import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; import {getExecutionPayloadEnvelopeSignatureSet} from "../signatureSets/executionPayloadEnvelope.js"; @@ -6,75 +5,32 @@ import {BeaconStateView} from "../stateView/beaconStateView.js"; import {CachedBeaconStateGloas} from "../types.js"; import {computeTimeAtSlot} from "../util/index.js"; import {verifySignatureSet} from "../util/signatureSets.js"; -import {processConsolidationRequest} from "./processConsolidationRequest.js"; -import {getPendingValidatorPubkeys, processDepositRequest} from "./processDepositRequest.js"; -import {processWithdrawalRequest} from "./processWithdrawalRequest.js"; export type ProcessExecutionPayloadEnvelopeOpts = { verifySignature?: boolean; - verifyStateRoot?: boolean; - dontTransferCache?: boolean; }; -// Unlike other block processing functions which mutate state in-place, this function -// clones the state and returns the post-state, similar to stateTransition(). -// This function does not call execution engine to verify payload. Need to call it from other place. +/** + * Pure verification of execution payload envelope — no state mutation. + * Payload processing is deferred to the next beacon block via processParentExecutionPayload. + * + * This function does not call the execution engine. That must be done separately. + * + * Spec: consensus-specs#5094 verify_execution_payload_envelope + */ export function processExecutionPayloadEnvelope( state: CachedBeaconStateGloas, signedEnvelope: gloas.SignedExecutionPayloadEnvelope, opts?: ProcessExecutionPayloadEnvelopeOpts -): CachedBeaconStateGloas { +): void { const {verifySignature = true} = opts ?? {}; const envelope = signedEnvelope.message; - const payload = envelope.payload; - const fork = state.config.getForkSeq(payload.slotNumber); if (verifySignature && !verifyExecutionPayloadEnvelopeSignature(state, signedEnvelope)) { throw Error(`Execution payload envelope has invalid signature builderIndex=${envelope.builderIndex}`); } - // .clone() before mutating state, similar to stateTransition() - const postState = state.clone(opts?.dontTransferCache) as CachedBeaconStateGloas; - - validateExecutionPayloadEnvelope(postState, envelope); - - const requests = envelope.executionRequests; - - if (requests.deposits.length > 0) { - // Build cache of pending validator pubkeys once, shared across all deposit requests - const pendingValidatorPubkeys = getPendingValidatorPubkeys(postState.config, postState); - - for (const deposit of requests.deposits) { - processDepositRequest(fork, postState, deposit, pendingValidatorPubkeys); - } - } - - for (const withdrawal of requests.withdrawals) { - processWithdrawalRequest(fork, postState, withdrawal); - } - - for (const consolidation of requests.consolidations) { - processConsolidationRequest(postState, consolidation); - } - - // Queue the builder payment - const paymentIndex = SLOTS_PER_EPOCH + (postState.slot % SLOTS_PER_EPOCH); - const payment = postState.builderPendingPayments.get(paymentIndex).clone(); - const amount = payment.withdrawal.amount; - - if (amount > 0) { - postState.builderPendingWithdrawals.push(payment.withdrawal); - } - - postState.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU()); - - // Cache the execution payload hash - postState.executionPayloadAvailability.set(postState.slot % SLOTS_PER_HISTORICAL_ROOT, true); - postState.latestBlockHash = payload.blockHash; - - postState.commit(); - - return postState; + validateExecutionPayloadEnvelope(state, envelope); } function validateExecutionPayloadEnvelope( @@ -84,15 +40,21 @@ function validateExecutionPayloadEnvelope( const payload = envelope.payload; // Cache latest block header state root + // Note: we read but do NOT mutate state — we compute the header root on a copy + let headerRoot: Uint8Array; if (byteArrayEquals(state.latestBlockHeader.stateRoot, ssz.Root.defaultValue())) { - const previousStateRoot = state.hashTreeRoot(); - state.latestBlockHeader.stateRoot = previousStateRoot; + // Compute what the header root would be with the state root filled in + const header = ssz.phase0.BeaconBlockHeader.toViewDU(state.latestBlockHeader.toValue()); + header.stateRoot = state.hashTreeRoot(); + headerRoot = header.hashTreeRoot(); + } else { + headerRoot = state.latestBlockHeader.hashTreeRoot(); } // Verify consistency with the beacon block - if (!byteArrayEquals(envelope.beaconBlockRoot, state.latestBlockHeader.hashTreeRoot())) { + if (!byteArrayEquals(envelope.beaconBlockRoot, headerRoot)) { throw new Error( - `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(state.latestBlockHeader.hashTreeRoot())}` + `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(headerRoot)}` ); } @@ -137,6 +99,14 @@ function validateExecutionPayloadEnvelope( ); } + // Verify execution_requests_root matches bid commitment (consensus-specs#5094) + const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests); + if (!byteArrayEquals(requestsRoot, committedBid.executionRequestsRoot)) { + throw new Error( + `Execution requests root mismatch envelope=${toRootHex(requestsRoot)} committedBid=${toRootHex(committedBid.executionRequestsRoot)}` + ); + } + // Verify consistency of the parent hash with respect to the previous execution payload if (!byteArrayEquals(payload.parentHash, state.latestBlockHash)) { throw new Error( @@ -151,7 +121,7 @@ function validateExecutionPayloadEnvelope( ); } - // Skipped: Verify the execution payload is valid + // Execution engine verification (verify_and_notify_new_payload) is done externally } function verifyExecutionPayloadEnvelopeSignature( diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 578d223741ff..5a881d3cb840 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -784,19 +784,14 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { return new BeaconStateView(newState); } - processExecutionPayloadEnvelope( + verifyExecutionPayloadEnvelope( signedEnvelope: gloas.SignedExecutionPayloadEnvelope, opts?: ProcessExecutionPayloadEnvelopeOpts - ): BeaconStateView { + ): void { const fork = this.config.getForkName(this.cachedState.slot); if (!isForkPostGloas(fork)) { - throw Error(`processExecutionPayloadEnvelope is only available for gloas+ forks, got fork=${fork}`); + throw Error(`verifyExecutionPayloadEnvelope is only available for gloas+ forks, got fork=${fork}`); } - const postPayloadState = processExecutionPayloadEnvelope( - this.cachedState as CachedBeaconStateGloas, - signedEnvelope, - opts - ); - return new BeaconStateView(postPayloadState); + processExecutionPayloadEnvelope(this.cachedState as CachedBeaconStateGloas, signedEnvelope, opts); } } diff --git a/packages/state-transition/src/stateView/interface.ts b/packages/state-transition/src/stateView/interface.ts index 5eee21e3af9a..ad507316aad7 100644 --- a/packages/state-transition/src/stateView/interface.ts +++ b/packages/state-transition/src/stateView/interface.ts @@ -254,10 +254,10 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu { getBuilder(index: BuilderIndex): gloas.Builder; canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean; getIndexInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number; - processExecutionPayloadEnvelope( + verifyExecutionPayloadEnvelope( signedEnvelope: gloas.SignedExecutionPayloadEnvelope, opts?: ProcessExecutionPayloadEnvelopeOpts - ): IBeaconStateView; + ): void; } /** From c3c73b489a5cb59a69b86402d80336f625b235ab Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:54:24 -0700 Subject: [PATCH 16/61] refactor: remove executionPayloadStateRoot from fork choice onExecutionPayload With deferred payload processing, the envelope no longer produces a separate post-state. The FULL variant node shares the same stateRoot as the PENDING node. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/fork-choice/src/forkChoice/forkChoice.ts | 2 -- packages/fork-choice/src/forkChoice/interface.ts | 2 -- packages/fork-choice/src/protoArray/protoArray.ts | 4 ++-- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 49226737b9fe..eeabef013348 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -988,7 +988,6 @@ export class ForkChoice implements IForkChoice { blockRoot: RootHex, executionPayloadBlockHash: RootHex, executionPayloadNumber: number, - executionPayloadStateRoot: RootHex, executionStatus: PayloadExecutionStatus ): void { this.protoArray.onExecutionPayload( @@ -996,7 +995,6 @@ export class ForkChoice implements IForkChoice { this.fcStore.currentSlot, executionPayloadBlockHash, executionPayloadNumber, - executionPayloadStateRoot, this.proposerBoostRoot, executionStatus ); diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 62bb364d776d..2f8670dd054f 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -198,13 +198,11 @@ export interface IForkChoice { * @param blockRoot - The beacon block root for which the payload arrived * @param executionPayloadBlockHash - The block hash of the execution payload * @param executionPayloadNumber - The block number of the execution payload - * @param executionPayloadStateRoot - The execution payload state root ie. the root of post-state after processExecutionPayloadEnvelope() */ onExecutionPayload( blockRoot: RootHex, executionPayloadBlockHash: RootHex, executionPayloadNumber: number, - executionPayloadStateRoot: RootHex, executionStatus: PayloadExecutionStatus ): void; /** diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index 4d239d52e8db..0aab4d33a1d4 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -572,7 +572,6 @@ export class ProtoArray { currentSlot: Slot, executionPayloadBlockHash: RootHex, executionPayloadNumber: number, - executionPayloadStateRoot: RootHex, proposerBoostRoot: RootHex | null, executionStatus: PayloadExecutionStatus ): void { @@ -617,6 +616,8 @@ export class ProtoArray { } // Create FULL variant as a child of PENDING (sibling to EMPTY) + // With deferred payload processing (consensus-specs#5094), FULL shares the same + // stateRoot as PENDING since envelope no longer produces a separate post-state const fullNode: ProtoNode = { ...pendingNode, parent: pendingIndex, // Points to own PENDING (same as EMPTY) @@ -628,7 +629,6 @@ export class ProtoArray { executionStatus, executionPayloadBlockHash, executionPayloadNumber, - stateRoot: executionPayloadStateRoot, }; const fullIndex = this.nodes.length; From bc621c3cfd860bbfa1dd489b5eb6b0161e1c3e04 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:56:42 -0700 Subject: [PATCH 17/61] refactor: simplify envelope import pipeline for deferred processing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - importExecutionPayload now does pure verification only — no state cloning, no post-payload state computation, no state root check, no regen.processState(), no checkpoint caching - Remove stateRoot from executionPayload and executionPayloadGossip events - Remove stateRoot from envelope construction in validator API - Remove stateRoot from fork choice onExecutionPayload call - Envelope verification runs via verifyExecutionPayloadEnvelope (void) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chain/blocks/importExecutionPayload.ts | 94 +++++++------------ 1 file changed, 36 insertions(+), 58 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index c25f83329a9b..c1b507d4ca8b 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -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 {fromHex, toRootHex} from "@lodestar/utils"; +import {fromHex} from "@lodestar/utils"; +import {fromHex} from "@lodestar/utils"; import {ExecutionPayloadStatus} from "../../execution/index.js"; import {isQueueErrorAborted} from "../../util/queue/index.js"; import {BeaconChain} from "../chain.js"; @@ -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, @@ -138,7 +140,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, @@ -159,35 +161,27 @@ export async function importExecutionPayload( ); 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}` - ); - } - })(), ]); - // 5a. Check signature verification result + // 5a. Pure envelope verification (no state mutation) + try { + blockState.verifyExecutionPayloadEnvelope(signedEnvelope, {verifySignature: false}); + } 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; @@ -213,11 +207,7 @@ export async function importExecutionPayload( }); } - // 5c. Compute post-payload state root - const postPayloadState = postPayloadResult.postPayloadState; - const postPayloadStateRoot = postPayloadState.hashTreeRoot(); - - // 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( @@ -228,23 +218,11 @@ export async function importExecutionPayload( } }); - // 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}); From f4d40219e9d02035485d9ada5a35db6f55cbdb85 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:01:04 -0700 Subject: [PATCH 18/61] feat: block production, gossip validation, and cleanup for deferred processing Block production: - Add executionRequestsRoot to ExecutionPayloadBid construction - Add parentExecutionRequests to BeaconBlockBody via getParentExecutionRequests() - Add getParentExecutionRequests() to BeaconChain (returns parent's execution requests from cached envelope, or empty if EMPTY parent) Gossip validation: - Add executionRequestsRoot check in envelope validation - Add EXECUTION_REQUESTS_ROOT_MISMATCH error code Cleanup: - Remove stateRoot from validator envelope logging - Fix spec test for void-returning processExecutionPayloadEnvelope - Update stale TODO comment in writePayloadEnvelopeInputToDb Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chain/blocks/writePayloadEnvelopeInputToDb.ts | 2 +- packages/beacon-node/src/chain/chain.ts | 15 +++++++++++++++ .../src/chain/errors/executionPayloadEnvelope.ts | 6 ++++++ .../src/chain/produceBlock/produceBlockBody.ts | 4 ++++ .../chain/validation/executionPayloadEnvelope.ts | 14 ++++++++++++-- .../test/spec/presets/operations.test.ts | 5 ++--- 6 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts b/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts index 425bd83adca8..d5c07a563590 100644 --- a/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +++ b/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts @@ -5,7 +5,7 @@ import {writeDataColumnsToDb} from "./writeBlockInputToDb.js"; /** * Persists payload envelope data to DB. This operation must be eventually completed if a payload is imported. * - * TODO GLOAS: Persist envelope metadata (stateRoot, executionRequests, builderIndex, etc.) without the full + * TODO GLOAS: Persist envelope metadata (executionRequests, builderIndex, etc.) without the full * execution payload body — only keep the blockHash reference. The EL already stores the payload. * See https://github.com/ChainSafe/lodestar/issues/5671 */ diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 2ee877c9d7f3..417a65fdf37a 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -39,6 +39,7 @@ import { ValidatorIndex, Wei, deneb, + electra, gloas, isBlindedBeaconBlock, phase0, @@ -870,6 +871,20 @@ export class BeaconChain implements IBeaconChain { ); } + /** + * Get execution requests from parent's payload envelope for block production. + * If parent was FULL, returns the execution requests from the cached envelope. + * If parent was EMPTY or envelope unknown, returns empty execution requests. + */ + getParentExecutionRequests(parentBlockRootHex: RootHex): electra.ExecutionRequests { + const payloadInput = this.seenPayloadEnvelopeInputCache.get(parentBlockRootHex); + if (payloadInput?.hasPayloadEnvelope()) { + return payloadInput.getPayloadEnvelope().message.executionRequests; + } + // Parent was EMPTY or we don't have the envelope — return empty requests + return ssz.electra.ExecutionRequests.defaultValue(); + } + async getExecutionPayloadEnvelope( blockSlot: Slot, blockRootHex: string diff --git a/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts index cf7a0fe4a002..c8a8645d6dc5 100644 --- a/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts @@ -11,6 +11,7 @@ export enum ExecutionPayloadEnvelopeErrorCode { SLOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_SLOT_MISMATCH", BUILDER_INDEX_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BUILDER_INDEX_MISMATCH", BLOCK_HASH_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_HASH_MISMATCH", + EXECUTION_REQUESTS_ROOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_ROOT_MISMATCH", INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE", PAYLOAD_ENVELOPE_INPUT_MISSING = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_PAYLOAD_ENVELOPE_INPUT_MISSING", } @@ -36,6 +37,11 @@ export type ExecutionPayloadEnvelopeErrorType = envelopeBlockHash: RootHex; bidBlockHash: RootHex | null; } + | { + code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_ROOT_MISMATCH; + envelopeRequestsRoot: RootHex; + bidRequestsRoot: RootHex; + } | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE} | {code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING; blockRoot: RootHex}; diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index a2edacdf77b5..2b123355ed5e 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -282,6 +282,10 @@ export async function produceBlockBody( gloasBody.signedExecutionPayloadBid = signedBid; // TODO GLOAS: Get payload attestations from pool for previous slot gloasBody.payloadAttestations = []; + // Determine parent execution requests for deferred processing (consensus-specs#5094) + // If parent was FULL: include execution requests from its envelope + // If parent was EMPTY: include empty execution requests + gloasBody.parentExecutionRequests = this.getParentExecutionRequests(parentBlockRootHex); blockBody = gloasBody as AssembledBodyType; // Store execution payload data required to construct execution payload envelope later diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index fc62d61cca86..831db7d8f0bd 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -4,8 +4,8 @@ import { getExecutionPayloadEnvelopeSignatureSet, isStatePostGloas, } from "@lodestar/state-transition"; -import {gloas} from "@lodestar/types"; -import {toRootHex} from "@lodestar/utils"; +import {gloas, ssz} from "@lodestar/types"; +import {byteArrayEquals, toRootHex} from "@lodestar/utils"; import {ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode, GossipAction} from "../errors/index.js"; import {IBeaconChain} from "../index.js"; import {RegenCaller} from "../regen/index.js"; @@ -98,6 +98,16 @@ async function validateExecutionPayloadEnvelope( }); } + // [REJECT] `hash_tree_root(envelope.execution_requests) == bid.execution_requests_root` + const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests); + if (!byteArrayEquals(requestsRoot, payloadInput.getBid().executionRequestsRoot)) { + throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { + code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_ROOT_MISMATCH, + envelopeRequestsRoot: toRootHex(requestsRoot), + bidRequestsRoot: toRootHex(payloadInput.getBid().executionRequestsRoot), + }); + } + // [REJECT] `payload.block_hash == bid.block_hash` if (toRootHex(payload.blockHash) !== payloadInput.getBlockHashHex()) { throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 48fd66e28c50..26fd54409134 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -78,10 +78,10 @@ const operationFns: Record> = ): CachedBeaconStateAllForks | void => { const fork = state.config.getForkSeq(state.slot); if (fork >= ForkSeq.gloas) { - return blockFns.processExecutionPayloadEnvelope(state as CachedBeaconStateGloas, testCase.signed_envelope, { + blockFns.processExecutionPayloadEnvelope(state as CachedBeaconStateGloas, testCase.signed_envelope, { verifySignature: true, - verifyStateRoot: true, }); + return; } blockFns.processExecutionPayload(fork, state as CachedBeaconStateBellatrix, testCase.body, { executionPayloadStatus: testCase.execution.execution_valid @@ -144,7 +144,6 @@ const operations: TestRunnerFn = (fork, const cachedState = createCachedBeaconStateTest(state, getConfig(fork, epoch)); const postState = operationFn(cachedState, testcase); - // processExecutionPayloadEnvelope returns the postState, other operations mutate the state in-place and return void if (postState !== undefined) { postState.commit(); return postState; From 63e5271ef6548c0184d9fdecd2e879479b3446b1 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:01:14 -0700 Subject: [PATCH 19/61] fix: type errors in processParentExecutionPayload and produceBlockBody - Use electra.ExecutionRequests type instead of unknown[] - Fix parentBlockRootHex -> parentBlock.blockRoot variable name Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/chain/produceBlock/produceBlockBody.ts | 2 +- .../src/block/processParentExecutionPayload.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 2b123355ed5e..db9a57234c84 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -285,7 +285,7 @@ export async function produceBlockBody( // Determine parent execution requests for deferred processing (consensus-specs#5094) // If parent was FULL: include execution requests from its envelope // If parent was EMPTY: include empty execution requests - gloasBody.parentExecutionRequests = this.getParentExecutionRequests(parentBlockRootHex); + gloasBody.parentExecutionRequests = this.getParentExecutionRequests(parentBlock.blockRoot); blockBody = gloasBody as AssembledBodyType; // Store execution payload data required to construct execution payload envelope later diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index 30149c8fec14..0560f75003fc 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -1,5 +1,5 @@ import {ForkPostGloas, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; -import {BeaconBlock, ssz} from "@lodestar/types"; +import {BeaconBlock, electra, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex} from "@lodestar/utils"; import {CachedBeaconStateGloas} from "../types.js"; import {computeEpochAtSlot} from "../util/epoch.js"; @@ -49,7 +49,7 @@ export function processParentExecutionPayload( function applyParentExecutionPayload( state: CachedBeaconStateGloas, parentBid: {slot: number; blockHash: Uint8Array; builderIndex: number}, - requests: {deposits: unknown[]; withdrawals: unknown[]; consolidations: unknown[]} + requests: electra.ExecutionRequests ): void { const fork = state.config.getForkSeq(state.slot); const parentSlot = parentBid.slot; @@ -97,11 +97,7 @@ function applyParentExecutionPayload( state.latestBlockHash = parentBid.blockHash; } -function assertEmptyExecutionRequests(requests: { - deposits: unknown[]; - withdrawals: unknown[]; - consolidations: unknown[]; -}): void { +function assertEmptyExecutionRequests(requests: electra.ExecutionRequests): void { if (requests.deposits.length !== 0 || requests.withdrawals.length !== 0 || requests.consolidations.length !== 0) { throw new Error("Parent execution requests must be empty when parent block is EMPTY"); } From 06ab9c19835fafe17d453276208c23abb4efd995 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:16:16 -0700 Subject: [PATCH 20/61] Fix spec test --- .../test/spec/presets/operations.test.ts | 8 ++++++++ .../src/block/processWithdrawals.ts | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 26fd54409134..d6cb02d5b2f2 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -10,6 +10,7 @@ import { CachedBeaconStateElectra, CachedBeaconStateGloas, ExecutionPayloadStatus, + processSlots, getBlockRootAtSlot, } from "@lodestar/state-transition"; import * as blockFns from "@lodestar/state-transition/block"; @@ -117,6 +118,13 @@ const operationFns: Record> = blockFns.processExecutionPayloadBid(state as CachedBeaconStateGloas, testCase.block); }, + parent_execution_payload: (state, testCase: {block: gloas.BeaconBlock}): CachedBeaconStateAllForks => { + // Spec test calls process_slots then process_parent_execution_payload + const postState = processSlots(state, testCase.block.slot); + blockFns.processParentExecutionPayload(postState as CachedBeaconStateGloas, testCase.block); + return postState; + }, + payload_attestation: (state, testCase: {payload_attestation: gloas.PayloadAttestation}) => { blockFns.processPayloadAttestation(state as CachedBeaconStateGloas, testCase.payload_attestation); }, diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index ba731b51d141..ad4036e0cd13 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -11,7 +11,12 @@ import { import {BuilderIndex, ValidatorIndex, capella, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex} from "@lodestar/utils"; import {CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; -import {convertBuilderIndexToValidatorIndex, convertValidatorIndexToBuilderIndex, isBuilderIndex} from "../util/gloas.js"; +import { + convertBuilderIndexToValidatorIndex, + convertValidatorIndexToBuilderIndex, + isBuilderIndex, + isParentBlockFull, +} from "../util/gloas.js"; import { decreaseBalance, getMaxEffectiveBalance, @@ -26,8 +31,12 @@ export function processWithdrawals( state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas, payload?: capella.FullOrBlindedExecutionPayload ): void { - // After consensus-specs#5094, processParentExecutionPayload has already handled parent effects - // before processWithdrawals is called, so no early return needed for Gloas. + // Return early if the parent block is empty. + // After processParentExecutionPayload runs, latestBlockHash is updated only if parent was FULL. + // If still mismatched, the parent was EMPTY and no withdrawals should be computed. + if (fork >= ForkSeq.gloas && !isParentBlockFull(state as CachedBeaconStateGloas)) { + return; + } // processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732) // processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) From 6552bc8eca15fed01b741cf137462fa769e6f379 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:26:00 -0700 Subject: [PATCH 21/61] Address comments & follow up on spec change --- .../chain/blocks/importExecutionPayload.ts | 55 ++++--- .../blocks/verifyExecutionPayloadEnvelope.ts | 132 +++++++++++++++++ packages/beacon-node/src/chain/chain.ts | 18 ++- .../chain/produceBlock/produceBlockBody.ts | 50 ++++++- .../test/spec/presets/operations.test.ts | 14 +- .../fork-choice/src/forkChoice/forkChoice.ts | 4 + .../fork-choice/src/forkChoice/interface.ts | 6 + packages/state-transition/src/block/index.ts | 2 - .../block/processExecutionPayloadEnvelope.ts | 139 ------------------ .../block/processParentExecutionPayload.ts | 44 +++--- .../src/block/processWithdrawals.ts | 2 +- .../src/stateView/beaconStateView.ts | 19 ++- .../src/stateView/interface.ts | 11 +- 13 files changed, 273 insertions(+), 223 deletions(-) create mode 100644 packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts delete mode 100644 packages/state-transition/src/block/processExecutionPayloadEnvelope.ts diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index c1b507d4ca8b..84b033c6d1b2 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -1,7 +1,6 @@ import {routes} from "@lodestar/api"; import {ExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice"; -import {getExecutionPayloadEnvelopeSignatureSet, isStatePostGloas} from "@lodestar/state-transition"; -import {fromHex} from "@lodestar/utils"; +import {isStatePostGloas} from "@lodestar/state-transition"; import {fromHex} from "@lodestar/utils"; import {ExecutionPayloadStatus} from "../../execution/index.js"; import {isQueueErrorAborted} from "../../util/queue/index.js"; @@ -9,6 +8,7 @@ import {BeaconChain} from "../chain.js"; import {RegenCaller} from "../regen/interface.js"; import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js"; import {ImportPayloadOpts} from "./types.js"; +import {verifyExecutionPayloadEnvelope, verifyExecutionPayloadEnvelopeSignature} from "./verifyExecutionPayloadEnvelope.js"; import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js"; const EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS = 64; @@ -94,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.payload.slotNumber); + const fork = this.config.getForkName(envelope.slot); - // 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.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { + // 1. Emit `execution_payload_available` event at the start of import + if (this.clock.currentSlot - envelope.slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { this.emitter.emit(routes.events.EventType.executionPayloadAvailable, { - slot: envelope.payload.slotNumber, + slot: envelope.slot, blockRoot: blockRootHex, }); } @@ -124,8 +121,8 @@ export async function importExecutionPayload( // The actual DB write is deferred until after verification succeeds. await this.unfinalizedPayloadEnvelopeWrites.waitForSpace(); - // 5. Get pre-state for processExecutionPayloadEnvelope - // We need the block state (post-block, pre-payload) to process the envelope + // 5. Get pre-state for envelope verification + // We need the block state (post-block, pre-payload) to verify the envelope const blockState = await this.regen.getBlockSlotState( protoBlock, protoBlock.slot, @@ -151,21 +148,23 @@ 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]); - })(), + : verifyExecutionPayloadEnvelopeSignature( + this.config, + blockState, + this.pubkeyCache, + signedEnvelope, + payloadInput.proposerIndex, + this.bls + ), ]); - // 5a. Pure envelope verification (no state mutation) + // 5a. Verify envelope fields against state (spec: verify_execution_payload_envelope) try { - blockState.verifyExecutionPayloadEnvelope(signedEnvelope, {verifySignature: false}); + // 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( { @@ -212,7 +211,7 @@ export async function importExecutionPayload( if (!isQueueErrorAborted(e)) { this.logger.error( "Error pushing payload envelope to unfinalized write queue", - {slot: envelope.payload.slotNumber, blockRoot: blockRootHex}, + {slot: envelope.slot, blockRoot: blockRootHex}, e as Error ); } @@ -228,10 +227,10 @@ export async function importExecutionPayload( this.metrics?.importPayload.columnsBySource.inc({source}); } - // 10. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads - if (this.clock.currentSlot - envelope.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { + // 9. Emit event after payload is fully verified and imported to fork choice + if (this.clock.currentSlot - envelope.slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { this.emitter.emit(routes.events.EventType.executionPayload, { - slot: envelope.payload.slotNumber, + slot: envelope.slot, builderIndex: envelope.builderIndex, blockHash: blockHashHex, blockRoot: blockRootHex, @@ -241,7 +240,7 @@ export async function importExecutionPayload( } this.logger.verbose("Execution payload imported", { - slot: envelope.payload.slotNumber, + slot: envelope.slot, builderIndex: envelope.builderIndex, blockRoot: blockRootHex, blockHash: blockHashHex, diff --git a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts new file mode 100644 index 000000000000..428c22188408 --- /dev/null +++ b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts @@ -0,0 +1,132 @@ +import {BeaconConfig} from "@lodestar/config"; +import {gloas, ssz} from "@lodestar/types"; +import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; +import { + type IBeaconStateViewGloas, + type PubkeyCache, + computeTimeAtSlot, + getExecutionPayloadEnvelopeSignatureSet, +} from "@lodestar/state-transition"; +import {IBlsVerifier} from "../bls/index.js"; + +export type VerifyExecutionPayloadEnvelopeOpts = { + verifyExecutionRequestsRoot?: boolean; +}; + +/** + * Verify execution payload envelope fields against the post-block state. + * Does NOT verify signature (done separately) or call the execution engine. + * + * Spec: gloas/fork-choice.md — verify_execution_payload_envelope + */ +export function verifyExecutionPayloadEnvelope( + config: BeaconConfig, + state: IBeaconStateViewGloas, + envelope: gloas.ExecutionPayloadEnvelope, + opts?: VerifyExecutionPayloadEnvelopeOpts +): void { + const {verifyExecutionRequestsRoot = true} = opts ?? {}; + const payload = envelope.payload; + + // Compute header root without mutating state + const headerValue = {...state.latestBlockHeader}; + if (byteArrayEquals(headerValue.stateRoot, ssz.Root.defaultValue())) { + headerValue.stateRoot = state.hashTreeRoot(); + } + const headerRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(headerValue); + + // Verify consistency with the beacon block + if (!byteArrayEquals(envelope.beaconBlockRoot, headerRoot)) { + throw new Error( + `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(headerRoot)}` + ); + } + + if (envelope.slot !== state.slot) { + throw new Error(`Slot mismatch between envelope and state envelope=${envelope.slot} state=${state.slot}`); + } + + // Verify consistency with the committed bid + const bid = state.latestExecutionPayloadBid; + if (envelope.builderIndex !== bid.builderIndex) { + throw new Error( + `Builder index mismatch between envelope and committed bid envelope=${envelope.builderIndex} bid=${bid.builderIndex}` + ); + } + + if (!byteArrayEquals(bid.prevRandao, payload.prevRandao)) { + throw new Error( + `Prev randao mismatch between bid and payload bid=${toHex(bid.prevRandao)} payload=${toHex(payload.prevRandao)}` + ); + } + + if (Number(bid.gasLimit) !== payload.gasLimit) { + throw new Error( + `Gas limit mismatch between payload and bid payload=${payload.gasLimit} bid=${Number(bid.gasLimit)}` + ); + } + + if (!byteArrayEquals(bid.blockHash, payload.blockHash)) { + throw new Error( + `Block hash mismatch between payload and bid payload=${toRootHex(payload.blockHash)} bid=${toRootHex(bid.blockHash)}` + ); + } + + // Verify execution_requests_root matches bid commitment + // Can be skipped if already verified during gossip validation + if (verifyExecutionRequestsRoot) { + const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests); + if (!byteArrayEquals(requestsRoot, bid.executionRequestsRoot)) { + throw new Error( + `Execution requests root mismatch envelope=${toRootHex(requestsRoot)} bid=${toRootHex(bid.executionRequestsRoot)}` + ); + } + } + + // Verify the execution payload is valid + if (!byteArrayEquals(payload.parentHash, state.latestBlockHash)) { + throw new Error( + `Parent hash mismatch between payload and state payload=${toRootHex(payload.parentHash)} state=${toRootHex(state.latestBlockHash)}` + ); + } + + if (payload.timestamp !== computeTimeAtSlot(config, state.slot, state.genesisTime)) { + throw new Error( + `Timestamp mismatch between payload and state payload=${payload.timestamp} state=${computeTimeAtSlot(config, state.slot, state.genesisTime)}` + ); + } + + // Verify consistency with expected withdrawals + const payloadWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(payload.withdrawals); + const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(state.payloadExpectedWithdrawals); + if (!byteArrayEquals(payloadWithdrawalsRoot, expectedWithdrawalsRoot)) { + throw new Error( + `Withdrawals mismatch between payload and expected payload=${toRootHex(payloadWithdrawalsRoot)} expected=${toRootHex(expectedWithdrawalsRoot)}` + ); + } + + // Execution engine verification (verify_and_notify_new_payload) is done externally +} + +/** + * Verify the BLS signature of an execution payload envelope. + * + * Spec: gloas/fork-choice.md — verify_execution_payload_envelope_signature + */ +export async function verifyExecutionPayloadEnvelopeSignature( + config: BeaconConfig, + state: IBeaconStateViewGloas, + pubkeyCache: PubkeyCache, + signedEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: number, + bls: IBlsVerifier +): Promise { + const signatureSet = getExecutionPayloadEnvelopeSignatureSet( + config, + pubkeyCache, + state, + signedEnvelope, + proposerIndex + ); + return bls.verifySignatureSets([signatureSet]); +} diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 417a65fdf37a..5b9562973121 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -873,15 +873,21 @@ export class BeaconChain implements IBeaconChain { /** * Get execution requests from parent's payload envelope for block production. - * If parent was FULL, returns the execution requests from the cached envelope. - * If parent was EMPTY or envelope unknown, returns empty execution requests. + * Uses is_payload_verified AND should_extend_payload per spec's prepare_execution_payload. + * If parent was FULL and PTC voted timely, returns execution requests from the cached envelope. + * Otherwise returns empty execution requests (build on EMPTY variant). */ getParentExecutionRequests(parentBlockRootHex: RootHex): electra.ExecutionRequests { - const payloadInput = this.seenPayloadEnvelopeInputCache.get(parentBlockRootHex); - if (payloadInput?.hasPayloadEnvelope()) { - return payloadInput.getPayloadEnvelope().message.executionRequests; + if ( + this.forkChoice.hasPayloadHexUnsafe(parentBlockRootHex) && + this.forkChoice.shouldExtendPayload(parentBlockRootHex) + ) { + const payloadInput = this.seenPayloadEnvelopeInputCache.get(parentBlockRootHex); + if (payloadInput?.hasPayloadEnvelope()) { + return payloadInput.getPayloadEnvelope().message.executionRequests; + } } - // Parent was EMPTY or we don't have the envelope — return empty requests + // Parent was EMPTY, payload not verified, or PTC didn't vote timely — return empty requests return ssz.electra.ExecutionRequests.defaultValue(); } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index db9a57234c84..a756b900fe9a 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -18,6 +18,7 @@ import { G2_POINT_AT_INFINITY, IBeaconStateView, type IBeaconStateViewBellatrix, + type IBeaconStateViewGloas, computeTimeAtSlot, isStatePostBellatrix, isStatePostCapella, @@ -614,6 +615,8 @@ export async function prepareExecutionPayload( chain: { executionEngine: IExecutionEngine; config: ChainForkConfig; + forkChoice?: IForkChoice; + seenPayloadEnvelopeInputCache?: {get(rootHex: string): {hasPayloadEnvelope(): boolean; getPayloadEnvelope(): gloas.SignedExecutionPayloadEnvelope} | undefined}; }, logger: Logger, fork: ForkPostBellatrix, @@ -624,11 +627,44 @@ export async function prepareExecutionPayload( state: IBeaconStateViewBellatrix, suggestedFeeRecipient: string ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { + let parentHash = state.latestBlockHash; + let withdrawalsOverride: capella.Withdrawal[] | undefined; + + // For Gloas: determine FULL vs EMPTY parent per spec's prepare_execution_payload + // If extending FULL parent: apply parent payload to get correct withdrawals and use bid.blockHash + // If EMPTY parent: use state.payloadExpectedWithdrawals and bid.parentBlockHash + if (isForkPostGloas(fork) && chain.forkChoice && chain.seenPayloadEnvelopeInputCache) { + const gloasState = state as unknown as { + latestExecutionPayloadBid: { + slot: number; + blockHash: Uint8Array; + parentBlockHash: Uint8Array; + builderIndex: number; + value: number; + feeRecipient: Uint8Array; + }; + }; + const parentRootHex = toRootHex(parentBlockRoot); + + if (chain.forkChoice.shouldExtendPayload(parentRootHex)) { + const payloadInput = chain.seenPayloadEnvelopeInputCache.get(parentRootHex); + if (payloadInput?.hasPayloadEnvelope()) { + // Build on FULL variant: apply parent payload to compute correct withdrawals, + // use bid.blockHash as EL head (per spec's prepare_execution_payload) + const gloasView = state as unknown as IBeaconStateViewGloas; + withdrawalsOverride = gloasView.getExpectedWithdrawalsForFullParent(payloadInput.getPayloadEnvelope()); + parentHash = gloasState.latestExecutionPayloadBid.blockHash; + } + } else { + // EMPTY parent: use bid.parentBlockHash as the EL head + parentHash = gloasState.latestExecutionPayloadBid.parentBlockHash; + } + } const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime); const prevRandao = state.getRandaoMix(state.epoch); const payloadIdCached = chain.executionEngine.payloadIdCache.get({ - headBlockHash: toRootHex(parentBlockHash), + headBlockHash: toRootHex(parentHash), finalizedBlockHash, timestamp: numToQuantity(timestamp), prevRandao: toHex(prevRandao), @@ -659,11 +695,12 @@ export async function prepareExecutionPayload( parentBlockRoot, parentBlockHash, feeRecipient: suggestedFeeRecipient, + withdrawalsOverride, }); payloadId = await chain.executionEngine.notifyForkchoiceUpdate( fork, - toRootHex(parentBlockHash), + toRootHex(parentHash), safeBlockHash, finalizedBlockHash, attributes @@ -769,12 +806,14 @@ function preparePayloadAttributes( parentBlockRoot, parentBlockHash, feeRecipient, + withdrawalsOverride, }: { prepareState: IBeaconStateViewBellatrix; prepareSlot: Slot; parentBlockRoot: Root; parentBlockHash: Bytes32; feeRecipient: string; + withdrawalsOverride?: capella.Withdrawal[]; } ): SSEPayloadAttributes["payloadAttributes"] { const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime); @@ -790,7 +829,10 @@ function preparePayloadAttributes( throw new Error("Expected Capella state for withdrawals"); } - if (isStatePostGloas(prepareState)) { + if (withdrawalsOverride) { + // FULL parent: withdrawals computed from state with parent payload applied + (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = withdrawalsOverride; + } else if (isStatePostGloas(prepareState)) { const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash); // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch // already deducted from CL balances but never credited on the EL (the envelope @@ -800,7 +842,7 @@ function preparePayloadAttributes( ? prepareState.getExpectedWithdrawals().expectedWithdrawals : prepareState.payloadExpectedWithdrawals; } else { - // withdrawals logic is now fork aware as it changes on electra fork post capella + // Pre-Gloas or Gloas with full parent but no override (shouldn't happen in normal flow) (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals; } diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index d6cb02d5b2f2..4f97fd26d79f 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; import {getConfig} from "@lodestar/config/test-utils"; -import {ACTIVE_PRESET, ForkName, ForkSeq} from "@lodestar/params"; +import {ACTIVE_PRESET, ForkName} from "@lodestar/params"; import {InputType} from "@lodestar/spec-test-util"; import { BeaconStateAllForks, @@ -72,18 +72,11 @@ const operationFns: Record> = execution_payload: ( state, testCase: { - body: bellatrix.BeaconBlockBody | gloas.BeaconBlockBody; - signed_envelope: gloas.SignedExecutionPayloadEnvelope; + body: bellatrix.BeaconBlockBody; execution: {execution_valid: boolean}; } - ): CachedBeaconStateAllForks | void => { + ) => { const fork = state.config.getForkSeq(state.slot); - if (fork >= ForkSeq.gloas) { - blockFns.processExecutionPayloadEnvelope(state as CachedBeaconStateGloas, testCase.signed_envelope, { - verifySignature: true, - }); - return; - } blockFns.processExecutionPayload(fork, state as CachedBeaconStateBellatrix, testCase.body, { executionPayloadStatus: testCase.execution.execution_valid ? ExecutionPayloadStatus.valid @@ -185,7 +178,6 @@ const operations: TestRunnerFn = (fork, deposit_request: ssz.electra.DepositRequest, consolidation_request: ssz.electra.ConsolidationRequest, payload_attestation: ssz.gloas.PayloadAttestation, - signed_envelope: ssz.gloas.SignedExecutionPayloadEnvelope, }, shouldError: (testCase) => testCase.post === undefined, getExpected: (testCase) => testCase.post, diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index eeabef013348..73544259d832 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -1082,6 +1082,10 @@ export class ForkChoice implements IForkChoice { return this.protoArray.hasPayload(blockRoot); } + shouldExtendPayload(blockRoot: RootHex): boolean { + return this.protoArray.shouldExtendPayload(blockRoot, this.proposerBoostRoot); + } + /** * Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root. */ diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 2f8670dd054f..5b1a8f8328f7 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -230,6 +230,12 @@ export interface IForkChoice { */ hasPayloadUnsafe(blockRoot: Root): boolean; hasPayloadHexUnsafe(blockRoot: RootHex): boolean; + /** + * Whether to extend the payload for a given block root. + * Checks PTC timeliness and data availability, with fallback logic. + * Spec: gloas/fork-choice.md#should_extend_payload + */ + shouldExtendPayload(blockRoot: RootHex): boolean; getSlotsPresent(windowStart: number): number; /** * Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. diff --git a/packages/state-transition/src/block/index.ts b/packages/state-transition/src/block/index.ts index 71633419e828..0822024a04ce 100644 --- a/packages/state-transition/src/block/index.ts +++ b/packages/state-transition/src/block/index.ts @@ -14,7 +14,6 @@ import {processBlockHeader} from "./processBlockHeader.js"; import {processEth1Data} from "./processEth1Data.js"; import {processExecutionPayload} from "./processExecutionPayload.js"; import {processExecutionPayloadBid} from "./processExecutionPayloadBid.js"; -import {processExecutionPayloadEnvelope} from "./processExecutionPayloadEnvelope.js"; import {processOperations} from "./processOperations.js"; import {processParentExecutionPayload} from "./processParentExecutionPayload.js"; import {processPayloadAttestation} from "./processPayloadAttestation.js"; @@ -33,7 +32,6 @@ export { processWithdrawals, processExecutionPayloadBid, processPayloadAttestation, - processExecutionPayloadEnvelope, processParentExecutionPayload, }; diff --git a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts deleted file mode 100644 index 1a0de2ea2126..000000000000 --- a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts +++ /dev/null @@ -1,139 +0,0 @@ -import {gloas, ssz} from "@lodestar/types"; -import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; -import {getExecutionPayloadEnvelopeSignatureSet} from "../signatureSets/executionPayloadEnvelope.js"; -import {BeaconStateView} from "../stateView/beaconStateView.js"; -import {CachedBeaconStateGloas} from "../types.js"; -import {computeTimeAtSlot} from "../util/index.js"; -import {verifySignatureSet} from "../util/signatureSets.js"; - -export type ProcessExecutionPayloadEnvelopeOpts = { - verifySignature?: boolean; -}; - -/** - * Pure verification of execution payload envelope — no state mutation. - * Payload processing is deferred to the next beacon block via processParentExecutionPayload. - * - * This function does not call the execution engine. That must be done separately. - * - * Spec: consensus-specs#5094 verify_execution_payload_envelope - */ -export function processExecutionPayloadEnvelope( - state: CachedBeaconStateGloas, - signedEnvelope: gloas.SignedExecutionPayloadEnvelope, - opts?: ProcessExecutionPayloadEnvelopeOpts -): void { - const {verifySignature = true} = opts ?? {}; - const envelope = signedEnvelope.message; - - if (verifySignature && !verifyExecutionPayloadEnvelopeSignature(state, signedEnvelope)) { - throw Error(`Execution payload envelope has invalid signature builderIndex=${envelope.builderIndex}`); - } - - validateExecutionPayloadEnvelope(state, envelope); -} - -function validateExecutionPayloadEnvelope( - state: CachedBeaconStateGloas, - envelope: gloas.ExecutionPayloadEnvelope -): void { - const payload = envelope.payload; - - // Cache latest block header state root - // Note: we read but do NOT mutate state — we compute the header root on a copy - let headerRoot: Uint8Array; - if (byteArrayEquals(state.latestBlockHeader.stateRoot, ssz.Root.defaultValue())) { - // Compute what the header root would be with the state root filled in - const header = ssz.phase0.BeaconBlockHeader.toViewDU(state.latestBlockHeader.toValue()); - header.stateRoot = state.hashTreeRoot(); - headerRoot = header.hashTreeRoot(); - } else { - headerRoot = state.latestBlockHeader.hashTreeRoot(); - } - - // Verify consistency with the beacon block - if (!byteArrayEquals(envelope.beaconBlockRoot, headerRoot)) { - throw new Error( - `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(headerRoot)}` - ); - } - - if (payload.slotNumber !== state.slot) { - throw new Error(`Slot mismatch between payload and state payload=${payload.slotNumber} state=${state.slot}`); - } - - // Verify consistency with the committed bid - const committedBid = state.latestExecutionPayloadBid; - if (envelope.builderIndex !== committedBid.builderIndex) { - throw new Error( - `Builder index mismatch between envelope and committed bid envelope=${envelope.builderIndex} committedBid=${committedBid.builderIndex}` - ); - } - - if (!byteArrayEquals(committedBid.prevRandao, payload.prevRandao)) { - throw new Error( - `Prev randao mismatch between committed bid and payload committedBid=${toHex(committedBid.prevRandao)} payload=${toHex(payload.prevRandao)}` - ); - } - - // Verify consistency with expected withdrawals - const payloadWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(payload.withdrawals); - const expectedWithdrawalsRoot = state.payloadExpectedWithdrawals.hashTreeRoot(); - if (!byteArrayEquals(payloadWithdrawalsRoot, expectedWithdrawalsRoot)) { - throw new Error( - `Withdrawals mismatch between payload and expected withdrawals payload=${toRootHex(payloadWithdrawalsRoot)} expected=${toRootHex(expectedWithdrawalsRoot)}` - ); - } - - // Verify the gas_limit - if (Number(committedBid.gasLimit) !== payload.gasLimit) { - throw new Error( - `Gas limit mismatch between envelope's payload and committed bid envelope=${payload.gasLimit} committedBid=${Number(committedBid.gasLimit)}` - ); - } - - // Verify the block hash - if (!byteArrayEquals(committedBid.blockHash, payload.blockHash)) { - throw new Error( - `Block hash mismatch between envelope's payload and committed bid envelope=${toRootHex(payload.blockHash)} committedBid=${toRootHex(committedBid.blockHash)}` - ); - } - - // Verify execution_requests_root matches bid commitment (consensus-specs#5094) - const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests); - if (!byteArrayEquals(requestsRoot, committedBid.executionRequestsRoot)) { - throw new Error( - `Execution requests root mismatch envelope=${toRootHex(requestsRoot)} committedBid=${toRootHex(committedBid.executionRequestsRoot)}` - ); - } - - // Verify consistency of the parent hash with respect to the previous execution payload - if (!byteArrayEquals(payload.parentHash, state.latestBlockHash)) { - throw new Error( - `Parent hash mismatch between envelope's payload and state envelope=${toRootHex(payload.parentHash)} state=${toRootHex(state.latestBlockHash)}` - ); - } - - // Verify timestamp - if (payload.timestamp !== computeTimeAtSlot(state.config, state.slot, state.genesisTime)) { - throw new Error( - `Timestamp mismatch between envelope's payload and state envelope=${payload.timestamp} state=${computeTimeAtSlot(state.config, state.slot, state.genesisTime)}` - ); - } - - // Execution engine verification (verify_and_notify_new_payload) is done externally -} - -function verifyExecutionPayloadEnvelopeSignature( - state: CachedBeaconStateGloas, - signedEnvelope: gloas.SignedExecutionPayloadEnvelope -): boolean { - const signatureSet = getExecutionPayloadEnvelopeSignatureSet( - state.config, - state.epochCtx.pubkeyCache, - new BeaconStateView(state), - signedEnvelope, - state.latestBlockHeader.proposerIndex - ); - return verifySignatureSet(signatureSet); -} diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index 0560f75003fc..b639dca10b3d 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -46,9 +46,21 @@ export function processParentExecutionPayload( * * Spec: apply_parent_execution_payload */ -function applyParentExecutionPayload( +/** + * Settle a builder payment at the given index. + * Spec: settle_builder_payment + */ +function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: number): void { + const payment = state.builderPendingPayments.get(paymentIndex).clone(); + if (payment.withdrawal.amount > 0) { + state.builderPendingWithdrawals.push(payment.withdrawal); + } + state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU()); +} + +export function applyParentExecutionPayload( state: CachedBeaconStateGloas, - parentBid: {slot: number; blockHash: Uint8Array; builderIndex: number}, + parentBid: {slot: number; blockHash: Uint8Array; builderIndex: number; value: number; feeRecipient: Uint8Array}, requests: electra.ExecutionRequests ): void { const fork = state.config.getForkSeq(state.slot); @@ -73,23 +85,21 @@ function applyParentExecutionPayload( processConsolidationRequest(state, consolidation); } - // Queue the builder payment - let paymentIndex: number | null; + // Settle the builder payment if (parentEpoch === currentEpoch) { - paymentIndex = SLOTS_PER_EPOCH + (parentSlot % SLOTS_PER_EPOCH); + settleBuilderPayment(state, SLOTS_PER_EPOCH + (parentSlot % SLOTS_PER_EPOCH)); } else if (parentEpoch === currentEpoch - 1) { - paymentIndex = parentSlot % SLOTS_PER_EPOCH; - } else { - // Parent is older than previous epoch — payment already settled/evicted - paymentIndex = null; - } - - if (paymentIndex !== null) { - const payment = state.builderPendingPayments.get(paymentIndex).clone(); - if (payment.withdrawal.amount > 0) { - state.builderPendingWithdrawals.push(payment.withdrawal); - } - state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU()); + settleBuilderPayment(state, parentSlot % SLOTS_PER_EPOCH); + } else if (parentBid.value > 0) { + // Parent is older than previous epoch — payment entry already settled/evicted. + // Directly append the withdrawal to ensure the builder gets paid. + state.builderPendingWithdrawals.push( + ssz.gloas.BuilderPendingWithdrawal.toViewDU({ + feeRecipient: parentBid.feeRecipient, + amount: parentBid.value, + builderIndex: parentBid.builderIndex, + }) + ); } // Update parent payload availability and latest block hash diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index ad4036e0cd13..c30faf880fe9 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -50,7 +50,7 @@ export function processWithdrawals( } = getExpectedWithdrawals(fork, state); const numWithdrawals = expectedWithdrawals.length; - // After gloas, withdrawals are verified later in processExecutionPayloadEnvelope + // After gloas, withdrawals are verified later in verifyExecutionPayloadEnvelope if (fork < ForkSeq.gloas) { if (payload === undefined) { throw Error("payload is required for pre-gloas processWithdrawals"); diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 5a881d3cb840..c9eed49599b7 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -28,8 +28,7 @@ import { rewards, } from "@lodestar/types"; import {Checkpoint, Fork} from "@lodestar/types/phase0"; -import {processExecutionPayloadEnvelope} from "../block/index.js"; -import {ProcessExecutionPayloadEnvelopeOpts} from "../block/processExecutionPayloadEnvelope.js"; +import {applyParentExecutionPayload} from "../block/processParentExecutionPayload.js"; import {VoluntaryExitValidity, getVoluntaryExitValidity} from "../block/processVoluntaryExit.js"; import {getExpectedWithdrawals} from "../block/processWithdrawals.js"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; @@ -784,14 +783,14 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { return new BeaconStateView(newState); } - verifyExecutionPayloadEnvelope( - signedEnvelope: gloas.SignedExecutionPayloadEnvelope, - opts?: ProcessExecutionPayloadEnvelopeOpts - ): void { - const fork = this.config.getForkName(this.cachedState.slot); - if (!isForkPostGloas(fork)) { - throw Error(`verifyExecutionPayloadEnvelope is only available for gloas+ forks, got fork=${fork}`); + getExpectedWithdrawalsForFullParent(envelope: gloas.SignedExecutionPayloadEnvelope): capella.Withdrawal[] { + const fork = this.config.getForkSeq(this.cachedState.slot); + if (!isForkPostGloas(this.config.getForkName(this.cachedState.slot))) { + throw Error("getExpectedWithdrawalsForFullParent is only available for gloas+ forks"); } - processExecutionPayloadEnvelope(this.cachedState as CachedBeaconStateGloas, signedEnvelope, opts); + const stateCopy = this.cachedState.clone(true) as CachedBeaconStateGloas; + applyParentExecutionPayload(stateCopy, stateCopy.latestExecutionPayloadBid, envelope.message.executionRequests); + const {expectedWithdrawals} = getExpectedWithdrawals(fork, stateCopy); + return expectedWithdrawals; } } diff --git a/packages/state-transition/src/stateView/interface.ts b/packages/state-transition/src/stateView/interface.ts index ad507316aad7..45672a11aa24 100644 --- a/packages/state-transition/src/stateView/interface.ts +++ b/packages/state-transition/src/stateView/interface.ts @@ -41,7 +41,6 @@ import { rewards, } from "@lodestar/types"; import {Checkpoint, Fork} from "@lodestar/types/phase0"; -import {ProcessExecutionPayloadEnvelopeOpts} from "../block/processExecutionPayloadEnvelope.js"; import {VoluntaryExitValidity} from "../block/processVoluntaryExit.js"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; import {EpochTransitionCacheOpts} from "../cache/epochTransitionCache.js"; @@ -254,10 +253,12 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu { getBuilder(index: BuilderIndex): gloas.Builder; canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean; getIndexInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number; - verifyExecutionPayloadEnvelope( - signedEnvelope: gloas.SignedExecutionPayloadEnvelope, - opts?: ProcessExecutionPayloadEnvelopeOpts - ): void; + /** + * Compute expected withdrawals as if the parent was FULL. + * Clones the state, applies parent payload effects, then computes withdrawals. + * Used by prepare_execution_payload when building on FULL parent. + */ + getExpectedWithdrawalsForFullParent(envelope: gloas.SignedExecutionPayloadEnvelope): capella.Withdrawal[]; } /** From e10d287956b63d0d34c82da016d7c1a1b2e44f6d Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:19:22 -0700 Subject: [PATCH 22/61] fix upgrade state --- packages/state-transition/src/slot/upgradeStateToGloas.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/state-transition/src/slot/upgradeStateToGloas.ts b/packages/state-transition/src/slot/upgradeStateToGloas.ts index a269dba199c1..9c29884ff353 100644 --- a/packages/state-transition/src/slot/upgradeStateToGloas.ts +++ b/packages/state-transition/src/slot/upgradeStateToGloas.ts @@ -48,6 +48,8 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee; stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee; stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash; + stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = + ssz.electra.ExecutionRequests.hashTreeRoot(ssz.electra.ExecutionRequests.defaultValue()); stateGloasView.nextWithdrawalIndex = stateGloasCloned.nextWithdrawalIndex; stateGloasView.nextWithdrawalValidatorIndex = stateGloasCloned.nextWithdrawalValidatorIndex; stateGloasView.historicalSummaries = stateGloasCloned.historicalSummaries; From 5421487df576d33d086e05b366202426abecd0d9 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:42:49 -0700 Subject: [PATCH 23/61] fix spec test --- .../beacon-node/src/chain/forkChoice/index.ts | 4 +- .../test/spec/presets/fork_choice.test.ts | 46 ++++++++++++++----- .../fork-choice/src/protoArray/protoArray.ts | 19 +------- .../src/block/processWithdrawals.ts | 14 ++++-- .../src/util/computeAnchorCheckpoint.ts | 21 +++------ 5 files changed, 53 insertions(+), 51 deletions(-) diff --git a/packages/beacon-node/src/chain/forkChoice/index.ts b/packages/beacon-node/src/chain/forkChoice/index.ts index 6e274acd2175..8f07db7a6c31 100644 --- a/packages/beacon-node/src/chain/forkChoice/index.ts +++ b/packages/beacon-node/src/chain/forkChoice/index.ts @@ -161,7 +161,7 @@ export function initializeForkChoiceFromFinalizedState( : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}), dataAvailabilityStatus: DataAvailabilityStatus.PreData, - payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY? + payloadStatus: isForkPostGloas ? PayloadStatus.EMPTY : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY? parentBlockHash: isStatePostGloas(state) ? toRootHex(state.latestBlockHash) : null, }, currentSlot @@ -260,7 +260,7 @@ export function initializeForkChoiceFromUnfinalizedState( : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}), dataAvailabilityStatus: DataAvailabilityStatus.PreData, - payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY? + payloadStatus: isForkPostGloas ? PayloadStatus.EMPTY : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY? parentBlockHash: isStatePostGloas(unfinalizedState) ? toRootHex(unfinalizedState.latestBlockHash) : null, }; diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index c37c4aec1eef..4538b95d4a6f 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -20,6 +20,7 @@ import {InputType} from "@lodestar/spec-test-util"; import { BeaconStateAllForks, BeaconStateView, + IBeaconStateViewGloas, createCachedBeaconState, createPubkeyCache, isExecutionStateType, @@ -47,8 +48,13 @@ import { BlockInputPreData, BlockInputSource, } from "../../../src/chain/blocks/blockInput/index.js"; +import { + verifyExecutionPayloadEnvelope, + verifyExecutionPayloadEnvelopeSignature, +} from "../../../src/chain/blocks/verifyExecutionPayloadEnvelope.js"; import {AttestationImportOpt, BlobSidecarValidation} from "../../../src/chain/blocks/types.js"; import {BeaconChain, ChainEvent} from "../../../src/chain/index.js"; +import {RegenCaller} from "../../../src/chain/regen/index.js"; import {defaultChainOptions} from "../../../src/chain/options.js"; import {validateFuluBlockDataColumnSidecars} from "../../../src/chain/validation/dataColumnSidecar.js"; import {ZERO_HASH_HEX} from "../../../src/constants/constants.js"; @@ -382,6 +388,32 @@ const forkChoiceTest = const blockHash = toHex(envelope.message.payload.blockHash); const blockNumber = envelope.message.payload.blockNumber; + // Verify envelope against the post-block state (spec: verify_execution_payload_envelope) + const protoBlock = (chain.forkChoice as ForkChoice).getBlockHexDefaultStatus(beaconBlockRoot); + if (!protoBlock) throw Error(`Block not found for root ${beaconBlockRoot}`); + const envelopeState = await chain.regen.getBlockSlotState( + protoBlock, + protoBlock.slot, + {dontTransferCache: true}, + RegenCaller.restApi + ); + verifyExecutionPayloadEnvelope( + beaconConfig, + envelopeState as IBeaconStateViewGloas, + envelope.message + ); + + // Verify signature + const sigValid = await verifyExecutionPayloadEnvelopeSignature( + beaconConfig, + envelopeState as IBeaconStateViewGloas, + pubkeyCache, + envelope, + envelopeState.latestBlockHeader.proposerIndex, + chain.bls + ); + if (!sigValid) throw Error("Invalid execution payload envelope signature"); + // Add predefined VALID status for the payload's block hash so the EL mock accepts it executionEngineBackend.addPredefinedPayloadStatus(blockHash, { status: ExecutionPayloadStatus.VALID, @@ -587,17 +619,9 @@ const forkChoiceTest = name.includes("voting_source_beyond_two_epoch") || name.includes("justified_update_always_if_better") || name.includes("justified_update_not_realized_finality") || - // TODO GLOAS: These tests will be unskipped by https://github.com/ChainSafe/lodestar/pull/9233 - (name.includes("gloas") && - (name.includes("simple_attempted_reorg_without_enough_ffg_votes") || - name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch") || - name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))) || - // TODO GLOAS: These two tests are affected by the wrong proposer boost cutoff time from the - // consensus-specs and thus have wrong expectation of proposer boost. Our implementation - // should pass these two tests after https://github.com/ethereum/consensus-specs/pull/5095 - // is included the spec release. - (name.includes("gloas/fork_choice/on_block") && - (name.endsWith("proposer_boost") || name.endsWith("proposer_boost_is_first_block"))), + // Spec test fixture bug in v1.7.0-alpha.5: wrong_withdrawals envelope SSZ data is + // byte-for-byte identical to the valid envelope, making it impossible to reject + name.endsWith("on_execution_payload_envelope__wrong_withdrawals"), }, }; }; diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index 0aab4d33a1d4..5b162c14b644 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -111,26 +111,9 @@ export class ProtoArray { // Anchor block PTC votes must be all-true per spec get_forkchoice_store: // payload_timeliness_vote={anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE))} - // Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.4/specs/gloas/fork-choice.md#modified-get_forkchoice_store + // Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/fork-choice.md#modified-get_forkchoice_store if (protoArray.ptcVotes.has(block.blockRoot)) { protoArray.ptcVotes.set(block.blockRoot, BitArray.fromBoolArray(Array.from({length: PTC_SIZE}, () => true))); - - // In the spec, we have payload_states = {anchor_root: anchor_state.copy()} - // which means the anchor's "payload" is considered received - // Without FULL, blocks extending FULL from the anchor would be orphaned. - // TODO GLOAS: This is a bug in the spec. Keep this to pass the current spec test - // for now. Need to remove this when we work on v1.7.0-alpha.5 - if (block.executionPayloadBlockHash !== null) { - protoArray.onExecutionPayload( - block.blockRoot, - currentSlot, - block.executionPayloadBlockHash, - (block as {executionPayloadNumber?: number}).executionPayloadNumber ?? 0, - block.stateRoot, - null, - ExecutionStatus.Valid - ); - } } return protoArray; diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index c30faf880fe9..e57041d1a1c4 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -10,6 +10,7 @@ import { } from "@lodestar/params"; import {BuilderIndex, ValidatorIndex, capella, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex} from "@lodestar/utils"; +import {ZERO_HASH} from "../constants/index.js"; import {CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; import { convertBuilderIndexToValidatorIndex, @@ -31,11 +32,14 @@ export function processWithdrawals( state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas, payload?: capella.FullOrBlindedExecutionPayload ): void { - // Return early if the parent block is empty. - // After processParentExecutionPayload runs, latestBlockHash is updated only if parent was FULL. - // If still mismatched, the parent was EMPTY and no withdrawals should be computed. - if (fork >= ForkSeq.gloas && !isParentBlockFull(state as CachedBeaconStateGloas)) { - return; + // Return early if this is genesis block or the parent block is empty. + if (fork >= ForkSeq.gloas) { + const stateGloas = state as CachedBeaconStateGloas; + const isGenesisBlock = byteArrayEquals(stateGloas.latestBlockHash, ZERO_HASH); + const isParentBlockEmpty = !isParentBlockFull(stateGloas); + if (isGenesisBlock || isParentBlockEmpty) { + return; + } } // processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732) diff --git a/packages/state-transition/src/util/computeAnchorCheckpoint.ts b/packages/state-transition/src/util/computeAnchorCheckpoint.ts index 1edb2ac57ca2..8bfe9a88efea 100644 --- a/packages/state-transition/src/util/computeAnchorCheckpoint.ts +++ b/packages/state-transition/src/util/computeAnchorCheckpoint.ts @@ -1,30 +1,21 @@ import {ChainForkConfig} from "@lodestar/config"; -import {GENESIS_SLOT, ZERO_HASH} from "@lodestar/params"; +import {ZERO_HASH} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; import {BeaconStateAllForks} from "../types.js"; -import {blockToHeader} from "./blockRoot.js"; import {computeCheckpointEpochAtStateSlot} from "./epoch.js"; export function computeAnchorCheckpoint( - config: ChainForkConfig, + _config: ChainForkConfig, anchorState: BeaconStateAllForks ): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { let blockHeader: phase0.BeaconBlockHeader; let root: Uint8Array; - const blockTypes = config.getForkTypes(anchorState.latestBlockHeader.slot); - if (anchorState.latestBlockHeader.slot === GENESIS_SLOT) { - const block = blockTypes.BeaconBlock.defaultValue(); - block.stateRoot = anchorState.hashTreeRoot(); - blockHeader = blockToHeader(config, block); - root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); - } else { - blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader); - if (ssz.Root.equals(blockHeader.stateRoot, ZERO_HASH)) { - blockHeader.stateRoot = anchorState.hashTreeRoot(); - } - root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); + blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader); + if (ssz.Root.equals(blockHeader.stateRoot, ZERO_HASH)) { + blockHeader.stateRoot = anchorState.hashTreeRoot(); } + root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); return { checkpoint: { From b5937ecf5b223d95d6adcaead9fa9aeed3affe14 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Wed, 22 Apr 2026 00:08:03 -0700 Subject: [PATCH 24/61] Harmonize --- .../chain/blocks/importExecutionPayload.ts | 19 +++++++++++-------- .../blocks/verifyExecutionPayloadEnvelope.ts | 8 ++++---- .../chain/produceBlock/produceBlockBody.ts | 13 +++++++++---- .../test/spec/presets/fork_choice.test.ts | 5 +++++ .../test/spec/presets/operations.test.ts | 4 ++-- .../test/spec/utils/specTestIterator.ts | 8 -------- .../fork-choice/src/forkChoice/forkChoice.ts | 4 ---- .../fork-choice/src/forkChoice/interface.ts | 6 ------ .../block/processParentExecutionPayload.ts | 5 +---- .../src/slot/upgradeStateToGloas.ts | 5 +++-- .../src/util/computeAnchorCheckpoint.ts | 4 +--- 11 files changed, 36 insertions(+), 45 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index 84b033c6d1b2..7653032a1c07 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -8,7 +8,10 @@ import {BeaconChain} from "../chain.js"; import {RegenCaller} from "../regen/interface.js"; import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js"; import {ImportPayloadOpts} from "./types.js"; -import {verifyExecutionPayloadEnvelope, verifyExecutionPayloadEnvelopeSignature} from "./verifyExecutionPayloadEnvelope.js"; +import { + verifyExecutionPayloadEnvelope, + verifyExecutionPayloadEnvelopeSignature, +} from "./verifyExecutionPayloadEnvelope.js"; import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js"; const EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS = 64; @@ -94,12 +97,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 - if (this.clock.currentSlot - envelope.slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { + 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, }); } @@ -211,7 +214,7 @@ export async function importExecutionPayload( 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 ); } @@ -228,9 +231,9 @@ export async function importExecutionPayload( } // 9. Emit event after payload is fully verified and imported to fork choice - if (this.clock.currentSlot - envelope.slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { + 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, @@ -240,7 +243,7 @@ export async function importExecutionPayload( } this.logger.verbose("Execution payload imported", { - slot: envelope.slot, + slot: envelope.payload.slotNumber, builderIndex: envelope.builderIndex, blockRoot: blockRootHex, blockHash: blockHashHex, diff --git a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts index 428c22188408..67cd6dfa8849 100644 --- a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts @@ -1,12 +1,12 @@ import {BeaconConfig} from "@lodestar/config"; -import {gloas, ssz} from "@lodestar/types"; -import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; import { type IBeaconStateViewGloas, type PubkeyCache, computeTimeAtSlot, getExecutionPayloadEnvelopeSignatureSet, } from "@lodestar/state-transition"; +import {gloas, ssz} from "@lodestar/types"; +import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; import {IBlsVerifier} from "../bls/index.js"; export type VerifyExecutionPayloadEnvelopeOpts = { @@ -42,8 +42,8 @@ export function verifyExecutionPayloadEnvelope( ); } - if (envelope.slot !== state.slot) { - throw new Error(`Slot mismatch between envelope and state envelope=${envelope.slot} state=${state.slot}`); + if (payload.slotNumber !== state.slot) { + throw new Error(`Slot mismatch between payload and state payload=${payload.slotNumber} state=${state.slot}`); } // Verify consistency with the committed bid diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index a756b900fe9a..9dd1e7175723 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -18,7 +18,6 @@ import { G2_POINT_AT_INFINITY, IBeaconStateView, type IBeaconStateViewBellatrix, - type IBeaconStateViewGloas, computeTimeAtSlot, isStatePostBellatrix, isStatePostCapella, @@ -616,7 +615,11 @@ export async function prepareExecutionPayload( executionEngine: IExecutionEngine; config: ChainForkConfig; forkChoice?: IForkChoice; - seenPayloadEnvelopeInputCache?: {get(rootHex: string): {hasPayloadEnvelope(): boolean; getPayloadEnvelope(): gloas.SignedExecutionPayloadEnvelope} | undefined}; + seenPayloadEnvelopeInputCache?: { + get( + rootHex: string + ): {hasPayloadEnvelope(): boolean; getPayloadEnvelope(): gloas.SignedExecutionPayloadEnvelope} | undefined; + }; }, logger: Logger, fork: ForkPostBellatrix, @@ -627,7 +630,7 @@ export async function prepareExecutionPayload( state: IBeaconStateViewBellatrix, suggestedFeeRecipient: string ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { - let parentHash = state.latestBlockHash; + let parentHash = parentBlockHash; let withdrawalsOverride: capella.Withdrawal[] | undefined; // For Gloas: determine FULL vs EMPTY parent per spec's prepare_execution_payload @@ -651,7 +654,9 @@ export async function prepareExecutionPayload( if (payloadInput?.hasPayloadEnvelope()) { // Build on FULL variant: apply parent payload to compute correct withdrawals, // use bid.blockHash as EL head (per spec's prepare_execution_payload) - const gloasView = state as unknown as IBeaconStateViewGloas; + const gloasView = state as unknown as { + getExpectedWithdrawalsForFullParent(envelope: gloas.SignedExecutionPayloadEnvelope): capella.Withdrawal[]; + }; withdrawalsOverride = gloasView.getExpectedWithdrawalsForFullParent(payloadInput.getPayloadEnvelope()); parentHash = gloasState.latestExecutionPayloadBid.blockHash; } diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 4538b95d4a6f..9975fe8a5d64 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -619,6 +619,11 @@ const forkChoiceTest = name.includes("voting_source_beyond_two_epoch") || name.includes("justified_update_always_if_better") || name.includes("justified_update_not_realized_finality") || + // TODO GLOAS: These tests will be unskipped by https://github.com/ChainSafe/lodestar/pull/9233 + (name.includes("gloas") && + (name.includes("simple_attempted_reorg_without_enough_ffg_votes") || + name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch") || + name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))) || // Spec test fixture bug in v1.7.0-alpha.5: wrong_withdrawals envelope SSZ data is // byte-for-byte identical to the valid envelope, making it impossible to reject name.endsWith("on_execution_payload_envelope__wrong_withdrawals"), diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 4f97fd26d79f..8faeeb543492 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -10,11 +10,11 @@ import { CachedBeaconStateElectra, CachedBeaconStateGloas, ExecutionPayloadStatus, - processSlots, getBlockRootAtSlot, + processSlots, } from "@lodestar/state-transition"; -import * as blockFns from "@lodestar/state-transition/block"; import {AttesterSlashing, altair, bellatrix, capella, electra, gloas, phase0, ssz, sszTypesFor} from "@lodestar/types"; +import * as blockFns from "../../../../state-transition/src/block/index.js"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {ethereumConsensusSpecsTests} from "../specTestVersioning.js"; import {expectEqualBeaconState, inputTypeSszTreeViewDU} from "../utils/expectEqualBeaconState.js"; diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 0dd85ca17d18..803f41432d55 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -77,16 +77,8 @@ export const defaultSkipOpts: SkipOpts = { // cell level DAS is ready /^fulu\/ssz_static\/PartialDataColumn(Header|PartsMetadata|Sidecar)\/.*$/, /^gloas\/ssz_static\/PartialDataColumn(Header|PartsMetadata|Sidecar)\/.*$/, - // TODO GLOAS: Unskip these tests as we get closer to alpha.5 ready - /^gloas\/fork_choice\/.*$/, - /^gloas\/fork\/.*$/, - /^gloas\/transition\/.*$/, - /^gloas\/operations\/parent_execution_payload\/.*$/, ], skippedTests: [ - // TODO GLOAS: Unskip these tests as we get closer to alpha.5 ready - /^gloas\/sanity\/blocks\/pyspec_tests\/builder_payment_after_missed_epochs$/, - /^gloas\/operations\/withdrawals\/pyspec_tests\/zero_hash_genesis_skips_withdrawals$/, ], // TODO GLOAS: Investigate why networking tests are failing since alpha.5 skippedRunners: ["fast_confirmation", "networking"], diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 73544259d832..eeabef013348 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -1082,10 +1082,6 @@ export class ForkChoice implements IForkChoice { return this.protoArray.hasPayload(blockRoot); } - shouldExtendPayload(blockRoot: RootHex): boolean { - return this.protoArray.shouldExtendPayload(blockRoot, this.proposerBoostRoot); - } - /** * Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root. */ diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 5b1a8f8328f7..2f8670dd054f 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -230,12 +230,6 @@ export interface IForkChoice { */ hasPayloadUnsafe(blockRoot: Root): boolean; hasPayloadHexUnsafe(blockRoot: RootHex): boolean; - /** - * Whether to extend the payload for a given block root. - * Checks PTC timeliness and data availability, with fallback logic. - * Spec: gloas/fork-choice.md#should_extend_payload - */ - shouldExtendPayload(blockRoot: RootHex): boolean; getSlotsPresent(windowStart: number): number; /** * Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index b639dca10b3d..b88781923115 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -13,10 +13,7 @@ import {processWithdrawalRequest} from "./processWithdrawalRequest.js"; * Spec: consensus-specs#5094 * https://github.com/ethereum/consensus-specs/blob/26ed32e/specs/gloas/beacon-chain.md */ -export function processParentExecutionPayload( - state: CachedBeaconStateGloas, - block: BeaconBlock -): void { +export function processParentExecutionPayload(state: CachedBeaconStateGloas, block: BeaconBlock): void { const bid = block.body.signedExecutionPayloadBid.message; const parentBid = state.latestExecutionPayloadBid; const requests = block.body.parentExecutionRequests; diff --git a/packages/state-transition/src/slot/upgradeStateToGloas.ts b/packages/state-transition/src/slot/upgradeStateToGloas.ts index 9c29884ff353..f4bc7ad3ec55 100644 --- a/packages/state-transition/src/slot/upgradeStateToGloas.ts +++ b/packages/state-transition/src/slot/upgradeStateToGloas.ts @@ -48,8 +48,9 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee; stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee; stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash; - stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = - ssz.electra.ExecutionRequests.hashTreeRoot(ssz.electra.ExecutionRequests.defaultValue()); + stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot( + ssz.electra.ExecutionRequests.defaultValue() + ); stateGloasView.nextWithdrawalIndex = stateGloasCloned.nextWithdrawalIndex; stateGloasView.nextWithdrawalValidatorIndex = stateGloasCloned.nextWithdrawalValidatorIndex; stateGloasView.historicalSummaries = stateGloasCloned.historicalSummaries; diff --git a/packages/state-transition/src/util/computeAnchorCheckpoint.ts b/packages/state-transition/src/util/computeAnchorCheckpoint.ts index 8bfe9a88efea..d262aa4500df 100644 --- a/packages/state-transition/src/util/computeAnchorCheckpoint.ts +++ b/packages/state-transition/src/util/computeAnchorCheckpoint.ts @@ -9,17 +9,15 @@ export function computeAnchorCheckpoint( anchorState: BeaconStateAllForks ): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { let blockHeader: phase0.BeaconBlockHeader; - let root: Uint8Array; blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader); if (ssz.Root.equals(blockHeader.stateRoot, ZERO_HASH)) { blockHeader.stateRoot = anchorState.hashTreeRoot(); } - root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); return { checkpoint: { - root, + root: ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader), // the checkpoint epoch = computeEpochAtSlot(anchorState.slot) + 1 if slot is not at epoch boundary // this is similar to a process_slots() call epoch: computeCheckpointEpochAtStateSlot(anchorState.slot), From 941a7108b0c429545d3d0b0b4bc54df69642c4ee Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Wed, 22 Apr 2026 15:29:29 +0100 Subject: [PATCH 25/61] review --- .../chain/blocks/importExecutionPayload.ts | 11 ++-- .../beacon-node/src/chain/blocks/index.ts | 1 - .../beacon-node/src/chain/blocks/types.ts | 34 ++++-------- .../blocks/verifyExecutionPayloadEnvelope.ts | 30 ++++------- packages/beacon-node/src/chain/chain.ts | 2 +- .../chain/produceBlock/produceBlockBody.ts | 3 +- .../fork-choice/src/protoArray/protoArray.ts | 2 - packages/state-transition/src/block/index.ts | 10 ++-- .../block/processParentExecutionPayload.ts | 54 ++++++++++--------- packages/types/src/gloas/sszTypes.ts | 2 +- 10 files changed, 65 insertions(+), 84 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index 7653032a1c07..d76c9106d241 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -72,9 +72,8 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe /** * Import an execution payload envelope after all data is available. * - * 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. + * The envelope is purely verified here, no state mutation. State effects from the + * payload are applied on the next block via processParentExecutionPayload. * * Steps: * 1. Emit `execution_payload_available` for payload attestation @@ -83,7 +82,7 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe * 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) + * 7. Update fork choice (transitions PENDING -> FULL) * 8. Record metrics * 9. Emit `execution_payload` event */ @@ -164,7 +163,7 @@ export async function importExecutionPayload( // 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 + // signature and executionRequestsRoot were already verified, skip re-hashing verifyExecutionPayloadEnvelope(this.config, blockState, envelope, { verifyExecutionRequestsRoot: !opts.validSignature, }); @@ -220,7 +219,7 @@ export async function importExecutionPayload( } }); - // 7. Update fork choice — no separate stateRoot since envelope doesn't produce post-state + // 7. Update fork choice, transitions the block's PENDING variant to FULL const execStatus = toForkChoiceExecutionStatus(execResult.status); this.forkChoice.onExecutionPayload(blockRootHex, blockHashHex, envelope.payload.blockNumber, execStatus); diff --git a/packages/beacon-node/src/chain/blocks/index.ts b/packages/beacon-node/src/chain/blocks/index.ts index 787d283f758b..b9120acae350 100644 --- a/packages/beacon-node/src/chain/blocks/index.ts +++ b/packages/beacon-node/src/chain/blocks/index.ts @@ -89,7 +89,6 @@ export async function processBlocks( (block, i): FullyVerifiedBlock => ({ blockInput: block, postState: postStates[i], - postPayloadState: null, parentBlockSlot: parentSlots[i], executionStatus: executionStatuses[i], // start supporting optimistic syncing/processing diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index 83e8a023dc10..03d37ccb526c 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -1,5 +1,5 @@ import type {ChainForkConfig} from "@lodestar/config"; -import {BlockExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice"; +import {BlockExecutionStatus} from "@lodestar/fork-choice"; import {ForkSeq} from "@lodestar/params"; import {DataAvailabilityStatus, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition"; import type {IndexedAttestation, Slot, fulu} from "@lodestar/types"; @@ -88,7 +88,14 @@ export type ImportBlockOpts = { seenTimestampSec?: number; }; -type FullyVerifiedBlockBase = { +/** + * A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import. + * + * `executionStatus` reflects the outcome of execution payload verification at block-import time: + * - pre-gloas: Valid | Syncing | PreMerge (from EL notifyNewPayload against the in-block payload) + * - post-gloas: PayloadSeparated (payload arrives separately as an envelope and is imported later) + */ +export type FullyVerifiedBlock = { blockInput: IBlockInput; postState: IBeaconStateView; parentBlockSlot: Slot; @@ -98,25 +105,6 @@ type FullyVerifiedBlockBase = { indexedAttestations: IndexedAttestation[]; /** Seen timestamp seconds */ seenTimestampSec: number; + /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync or for merge block */ + executionStatus: BlockExecutionStatus; }; - -/** - * A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import. - * - * Discriminated union on `postPayloadState`: - * - `null` → block has no pre-verified envelope; `executionStatus` is any `BlockExecutionStatus` - * - non-null → envelope was pre-verified during state transition; `executionStatus` is narrowed to - * `Valid | Syncing` (matching what `forkChoice.onExecutionPayload` expects) - */ -export type FullyVerifiedBlock = FullyVerifiedBlockBase & - ( - | { - postPayloadState: null; - /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync or for merge block */ - executionStatus: BlockExecutionStatus; - } - | { - postPayloadState: IBeaconStateView; - executionStatus: PayloadExecutionStatus; - } - ); diff --git a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts index 67cd6dfa8849..fafc5e60980e 100644 --- a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts @@ -15,9 +15,12 @@ export type VerifyExecutionPayloadEnvelopeOpts = { /** * Verify execution payload envelope fields against the post-block state. - * Does NOT verify signature (done separately) or call the execution engine. * - * Spec: gloas/fork-choice.md — verify_execution_payload_envelope + * Signature verification and the execution engine call (`verify_and_notify_new_payload`) are + * performed outside this function, see `verifyExecutionPayloadEnvelopeSignature` and + * `importExecutionPayload` which run both in parallel with this check. + * + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope */ export function verifyExecutionPayloadEnvelope( config: BeaconConfig, @@ -28,24 +31,18 @@ export function verifyExecutionPayloadEnvelope( const {verifyExecutionRequestsRoot = true} = opts ?? {}; const payload = envelope.payload; - // Compute header root without mutating state + // Verify consistency with the beacon block const headerValue = {...state.latestBlockHeader}; if (byteArrayEquals(headerValue.stateRoot, ssz.Root.defaultValue())) { headerValue.stateRoot = state.hashTreeRoot(); } const headerRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(headerValue); - - // Verify consistency with the beacon block if (!byteArrayEquals(envelope.beaconBlockRoot, headerRoot)) { throw new Error( `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(headerRoot)}` ); } - if (payload.slotNumber !== state.slot) { - throw new Error(`Slot mismatch between payload and state payload=${payload.slotNumber} state=${state.slot}`); - } - // Verify consistency with the committed bid const bid = state.latestExecutionPayloadBid; if (envelope.builderIndex !== bid.builderIndex) { @@ -53,26 +50,21 @@ export function verifyExecutionPayloadEnvelope( `Builder index mismatch between envelope and committed bid envelope=${envelope.builderIndex} bid=${bid.builderIndex}` ); } - if (!byteArrayEquals(bid.prevRandao, payload.prevRandao)) { throw new Error( `Prev randao mismatch between bid and payload bid=${toHex(bid.prevRandao)} payload=${toHex(payload.prevRandao)}` ); } - if (Number(bid.gasLimit) !== payload.gasLimit) { throw new Error( `Gas limit mismatch between payload and bid payload=${payload.gasLimit} bid=${Number(bid.gasLimit)}` ); } - if (!byteArrayEquals(bid.blockHash, payload.blockHash)) { throw new Error( `Block hash mismatch between payload and bid payload=${toRootHex(payload.blockHash)} bid=${toRootHex(bid.blockHash)}` ); } - - // Verify execution_requests_root matches bid commitment // Can be skipped if already verified during gossip validation if (verifyExecutionRequestsRoot) { const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests); @@ -84,19 +76,19 @@ export function verifyExecutionPayloadEnvelope( } // Verify the execution payload is valid + if (payload.slotNumber !== state.slot) { + throw new Error(`Slot mismatch between payload and state payload=${payload.slotNumber} state=${state.slot}`); + } if (!byteArrayEquals(payload.parentHash, state.latestBlockHash)) { throw new Error( `Parent hash mismatch between payload and state payload=${toRootHex(payload.parentHash)} state=${toRootHex(state.latestBlockHash)}` ); } - if (payload.timestamp !== computeTimeAtSlot(config, state.slot, state.genesisTime)) { throw new Error( `Timestamp mismatch between payload and state payload=${payload.timestamp} state=${computeTimeAtSlot(config, state.slot, state.genesisTime)}` ); } - - // Verify consistency with expected withdrawals const payloadWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(payload.withdrawals); const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(state.payloadExpectedWithdrawals); if (!byteArrayEquals(payloadWithdrawalsRoot, expectedWithdrawalsRoot)) { @@ -104,14 +96,12 @@ export function verifyExecutionPayloadEnvelope( `Withdrawals mismatch between payload and expected payload=${toRootHex(payloadWithdrawalsRoot)} expected=${toRootHex(expectedWithdrawalsRoot)}` ); } - - // Execution engine verification (verify_and_notify_new_payload) is done externally } /** * Verify the BLS signature of an execution payload envelope. * - * Spec: gloas/fork-choice.md — verify_execution_payload_envelope_signature + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope_signature */ export async function verifyExecutionPayloadEnvelopeSignature( config: BeaconConfig, diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 5b9562973121..310bae691894 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -887,7 +887,7 @@ export class BeaconChain implements IBeaconChain { return payloadInput.getPayloadEnvelope().message.executionRequests; } } - // Parent was EMPTY, payload not verified, or PTC didn't vote timely — return empty requests + // Parent was EMPTY, payload not verified, or PTC didn't vote timely, return empty requests return ssz.electra.ExecutionRequests.defaultValue(); } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 9dd1e7175723..59fe704f664b 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -282,7 +282,8 @@ export async function produceBlockBody( gloasBody.signedExecutionPayloadBid = signedBid; // TODO GLOAS: Get payload attestations from pool for previous slot gloasBody.payloadAttestations = []; - // Determine parent execution requests for deferred processing (consensus-specs#5094) + // Parent's execution requests are applied when this block is processed (via + // processParentExecutionPayload), so the proposer must commit to them here. // If parent was FULL: include execution requests from its envelope // If parent was EMPTY: include empty execution requests gloasBody.parentExecutionRequests = this.getParentExecutionRequests(parentBlock.blockRoot); diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index 5b162c14b644..de70078a5e4d 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -599,8 +599,6 @@ export class ProtoArray { } // Create FULL variant as a child of PENDING (sibling to EMPTY) - // With deferred payload processing (consensus-specs#5094), FULL shares the same - // stateRoot as PENDING since envelope no longer produces a separate post-state const fullNode: ProtoNode = { ...pendingNode, parent: pendingIndex, // Points to own PENDING (same as EMPTY) diff --git a/packages/state-transition/src/block/index.ts b/packages/state-transition/src/block/index.ts index 0822024a04ce..84fbc929c091 100644 --- a/packages/state-transition/src/block/index.ts +++ b/packages/state-transition/src/block/index.ts @@ -51,8 +51,8 @@ export function processBlock( ): void { const {verifySignatures = true} = opts ?? {}; - // Process parent execution payload effects first (consensus-specs#5094) - // Must run before processBlockHeader and processExecutionPayloadBid + // Apply the parent's deferred payload effects before everything else. Must run before + // processBlockHeader and processExecutionPayloadBid so subsequent steps see the updated state. if (fork >= ForkSeq.gloas) { processParentExecutionPayload(state as CachedBeaconStateGloas, block as BeaconBlock); } @@ -60,7 +60,7 @@ export function processBlock( processBlockHeader(state, block); if (fork >= ForkSeq.gloas) { - // After consensus-specs#5094, processParentExecutionPayload has already handled parent effects + // Parent payload's execution requests were already applied by processParentExecutionPayload above processWithdrawals(fork, state as CachedBeaconStateGloas); } else if (fork >= ForkSeq.capella) { const fullOrBlindedPayload = getFullOrBlindedPayload(block); @@ -73,7 +73,9 @@ export function processBlock( // The call to the process_execution_payload must happen before the call to the process_randao as the former depends // on the randao_mix computed with the reveal of the previous block. - // TODO GLOAS: We call processExecutionPayload somewhere else post-gloas + // Post-gloas: process_execution_payload is not part of block processing. The parent's payload + // effects are applied earlier via processParentExecutionPayload, and each envelope is verified + // out-of-band via verifyExecutionPayloadEnvelope when it arrives. if ( fork < ForkSeq.gloas && fork >= ForkSeq.bellatrix && diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index b88781923115..176aea949fa8 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -8,20 +8,18 @@ import {getPendingValidatorPubkeys, processDepositRequest} from "./processDeposi import {processWithdrawalRequest} from "./processWithdrawalRequest.js"; /** - * Process parent execution payload effects as first step of processBlock. + * Process parent execution payload effects as the first step of processBlock. * - * Spec: consensus-specs#5094 - * https://github.com/ethereum/consensus-specs/blob/26ed32e/specs/gloas/beacon-chain.md + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/beacon-chain.md#new-process_parent_execution_payload */ export function processParentExecutionPayload(state: CachedBeaconStateGloas, block: BeaconBlock): void { const bid = block.body.signedExecutionPayloadBid.message; const parentBid = state.latestExecutionPayloadBid; const requests = block.body.parentExecutionRequests; - // True if this block built on the parent's full payload - const isParentFull = byteArrayEquals(bid.parentBlockHash, parentBid.blockHash); - - if (!isParentFull) { + const isGenesisBlock = byteArrayEquals(parentBid.blockHash, ssz.Root.defaultValue()); + const isParentBlockEmpty = !byteArrayEquals(bid.parentBlockHash, parentBid.blockHash); + if (isGenesisBlock || isParentBlockEmpty) { // Parent was EMPTY -- no execution requests expected assertEmptyExecutionRequests(requests); return; @@ -39,22 +37,14 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo } /** - * Apply parent execution payload effects to state. + * Process the parent's execution requests, queue the builder payment, update payload availability, + * and update the latest block hash. * - * Spec: apply_parent_execution_payload - */ -/** - * Settle a builder payment at the given index. - * Spec: settle_builder_payment + * Called from processParentExecutionPayload during block processing, and from the validator during + * block production before computing withdrawals. + * + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/beacon-chain.md#new-apply_parent_execution_payload */ -function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: number): void { - const payment = state.builderPendingPayments.get(paymentIndex).clone(); - if (payment.withdrawal.amount > 0) { - state.builderPendingWithdrawals.push(payment.withdrawal); - } - state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU()); -} - export function applyParentExecutionPayload( state: CachedBeaconStateGloas, parentBid: {slot: number; blockHash: Uint8Array; builderIndex: number; value: number; feeRecipient: Uint8Array}, @@ -65,8 +55,8 @@ export function applyParentExecutionPayload( const parentEpoch = computeEpochAtSlot(parentSlot); const currentEpoch = computeEpochAtSlot(state.slot); - // Process execution requests from parent's payload - // Execution requests are processed at state.slot (child's slot), not parent's slot + // Process execution requests from parent's payload. The execution + // requests are processed at state.slot (child's slot), not the parent's slot. if (requests.deposits.length > 0) { const pendingValidatorPubkeys = getPendingValidatorPubkeys(state.config, state); for (const deposit of requests.deposits) { @@ -88,8 +78,8 @@ export function applyParentExecutionPayload( } else if (parentEpoch === currentEpoch - 1) { settleBuilderPayment(state, parentSlot % SLOTS_PER_EPOCH); } else if (parentBid.value > 0) { - // Parent is older than previous epoch — payment entry already settled/evicted. - // Directly append the withdrawal to ensure the builder gets paid. + // Parent is older than the previous epoch, its payment entry has already been settled or + // evicted, so append the withdrawal directly to ensure the builder still gets paid state.builderPendingWithdrawals.push( ssz.gloas.BuilderPendingWithdrawal.toViewDU({ feeRecipient: parentBid.feeRecipient, @@ -104,6 +94,20 @@ export function applyParentExecutionPayload( state.latestBlockHash = parentBid.blockHash; } +/** + * Settle a builder payment at the given index: move its withdrawal (if any) to the + * pending withdrawals list and clear the payment slot. + * + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/beacon-chain.md#new-settle_builder_payment + */ +function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: number): void { + const payment = state.builderPendingPayments.get(paymentIndex).clone(); + if (payment.withdrawal.amount > 0) { + state.builderPendingWithdrawals.push(payment.withdrawal); + } + state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU()); +} + function assertEmptyExecutionRequests(requests: electra.ExecutionRequests): void { if (requests.deposits.length !== 0 || requests.withdrawals.length !== 0 || requests.consolidations.length !== 0) { throw new Error("Parent execution requests must be empty when parent block is EMPTY"); diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index 59fd84ebb1ec..c88815351308 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -294,7 +294,7 @@ export const BeaconState = new ContainerType( ptcWindow: PtcWindow, // New in GLOAS:EIP7732 }, {typeName: "BeaconState", jsonCase: "eth2"} -); // New in GLOAS:EIP7732 +); export const DataColumnSidecar = new ContainerType( { From 532ffccc6f57a70fbd9149bfd22f97553de17e50 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Wed, 22 Apr 2026 15:29:39 +0100 Subject: [PATCH 26/61] ethspecify --- specrefs/.ethspecify.yml | 4 ---- specrefs/functions.yml | 20 ++++++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml index 4f0f00f8b4bd..7d89c0abb44f 100644 --- a/specrefs/.ethspecify.yml +++ b/specrefs/.ethspecify.yml @@ -412,13 +412,9 @@ exceptions: - will_no_conflicting_checkpoint_be_justified#phase0 # gloas / heze empty sources to skip - - apply_parent_execution_payload#gloas - is_payload_verified#gloas - on_execution_payload_envelope#gloas - on_execution_payload_envelope#heze - - process_parent_execution_payload#gloas - - settle_builder_payment#gloas - - verify_execution_payload_envelope#gloas configs: # phase0 fast confirmation / not implemented diff --git a/specrefs/functions.yml b/specrefs/functions.yml index 264fb1179eab..ef4d692161e7 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -215,7 +215,9 @@ - name: apply_parent_execution_payload#gloas - sources: [] + sources: + - file: packages/state-transition/src/block/processParentExecutionPayload.ts + search: export function applyParentExecutionPayload( spec: | def apply_parent_execution_payload( @@ -9812,7 +9814,9 @@ - name: process_parent_execution_payload#gloas - sources: [] + sources: + - file: packages/state-transition/src/block/processParentExecutionPayload.ts + search: export function processParentExecutionPayload( spec: | def process_parent_execution_payload(state: BeaconState, block: BeaconBlock) -> None: @@ -10945,7 +10949,9 @@ - name: settle_builder_payment#gloas - sources: [] + sources: + - file: packages/state-transition/src/block/processParentExecutionPayload.ts + search: function settleBuilderPayment( spec: | def settle_builder_payment(state: BeaconState, payment_index: uint64) -> None: @@ -12866,7 +12872,9 @@ - name: verify_execution_payload_envelope#gloas - sources: [] + sources: + - file: packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts + search: export function verifyExecutionPayloadEnvelope( spec: | def verify_execution_payload_envelope( @@ -12913,8 +12921,8 @@ - name: verify_execution_payload_envelope_signature#gloas sources: - - file: packages/state-transition/src/block/processExecutionPayloadEnvelope.ts - search: function verifyExecutionPayloadEnvelopeSignature( + - file: packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts + search: export async function verifyExecutionPayloadEnvelopeSignature( spec: | def verify_execution_payload_envelope_signature( From 16941ffbb9801177e6e38cf9f54c8510c1637a30 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Wed, 22 Apr 2026 15:43:54 +0100 Subject: [PATCH 27/61] restore some comments --- .../src/chain/blocks/verifyExecutionPayloadEnvelope.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts index fafc5e60980e..ed31e4d33769 100644 --- a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts @@ -31,7 +31,8 @@ export function verifyExecutionPayloadEnvelope( const {verifyExecutionRequestsRoot = true} = opts ?? {}; const payload = envelope.payload; - // Verify consistency with the beacon block + // Verify consistency with the beacon block. + // Compute header root on a copy of latestBlockHeader to avoid mutating state. const headerValue = {...state.latestBlockHeader}; if (byteArrayEquals(headerValue.stateRoot, ssz.Root.defaultValue())) { headerValue.stateRoot = state.hashTreeRoot(); @@ -65,7 +66,8 @@ export function verifyExecutionPayloadEnvelope( `Block hash mismatch between payload and bid payload=${toRootHex(payload.blockHash)} bid=${toRootHex(bid.blockHash)}` ); } - // Can be skipped if already verified during gossip validation + // Verify execution_requests_root matches bid commitment. + // Can be skipped if already verified during gossip validation. if (verifyExecutionRequestsRoot) { const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests); if (!byteArrayEquals(requestsRoot, bid.executionRequestsRoot)) { @@ -89,6 +91,8 @@ export function verifyExecutionPayloadEnvelope( `Timestamp mismatch between payload and state payload=${payload.timestamp} state=${computeTimeAtSlot(config, state.slot, state.genesisTime)}` ); } + + // Verify consistency with expected withdrawals const payloadWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(payload.withdrawals); const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(state.payloadExpectedWithdrawals); if (!byteArrayEquals(payloadWithdrawalsRoot, expectedWithdrawalsRoot)) { @@ -96,6 +100,8 @@ export function verifyExecutionPayloadEnvelope( `Withdrawals mismatch between payload and expected payload=${toRootHex(payloadWithdrawalsRoot)} expected=${toRootHex(expectedWithdrawalsRoot)}` ); } + + // Execution engine verification (verify_and_notify_new_payload) is done externally by the caller } /** From 6b08376dcb15ec64a1a90c433757a1fef53be57d Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:53:39 -0700 Subject: [PATCH 28/61] Remove block production --- packages/beacon-node/src/chain/chain.ts | 21 ------- .../chain/produceBlock/produceBlockBody.ts | 61 ++----------------- 2 files changed, 5 insertions(+), 77 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 310bae691894..2ee877c9d7f3 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -39,7 +39,6 @@ import { ValidatorIndex, Wei, deneb, - electra, gloas, isBlindedBeaconBlock, phase0, @@ -871,26 +870,6 @@ export class BeaconChain implements IBeaconChain { ); } - /** - * Get execution requests from parent's payload envelope for block production. - * Uses is_payload_verified AND should_extend_payload per spec's prepare_execution_payload. - * If parent was FULL and PTC voted timely, returns execution requests from the cached envelope. - * Otherwise returns empty execution requests (build on EMPTY variant). - */ - getParentExecutionRequests(parentBlockRootHex: RootHex): electra.ExecutionRequests { - if ( - this.forkChoice.hasPayloadHexUnsafe(parentBlockRootHex) && - this.forkChoice.shouldExtendPayload(parentBlockRootHex) - ) { - const payloadInput = this.seenPayloadEnvelopeInputCache.get(parentBlockRootHex); - if (payloadInput?.hasPayloadEnvelope()) { - return payloadInput.getPayloadEnvelope().message.executionRequests; - } - } - // Parent was EMPTY, payload not verified, or PTC didn't vote timely, return empty requests - return ssz.electra.ExecutionRequests.defaultValue(); - } - async getExecutionPayloadEnvelope( blockSlot: Slot, blockRootHex: string diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 59fe704f664b..4db6e0c00f68 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -282,11 +282,7 @@ export async function produceBlockBody( gloasBody.signedExecutionPayloadBid = signedBid; // TODO GLOAS: Get payload attestations from pool for previous slot gloasBody.payloadAttestations = []; - // Parent's execution requests are applied when this block is processed (via - // processParentExecutionPayload), so the proposer must commit to them here. - // If parent was FULL: include execution requests from its envelope - // If parent was EMPTY: include empty execution requests - gloasBody.parentExecutionRequests = this.getParentExecutionRequests(parentBlock.blockRoot); + // TODO GLOAS: set parentExecutionRequests in the block body blockBody = gloasBody as AssembledBodyType; // Store execution payload data required to construct execution payload envelope later @@ -615,12 +611,6 @@ export async function prepareExecutionPayload( chain: { executionEngine: IExecutionEngine; config: ChainForkConfig; - forkChoice?: IForkChoice; - seenPayloadEnvelopeInputCache?: { - get( - rootHex: string - ): {hasPayloadEnvelope(): boolean; getPayloadEnvelope(): gloas.SignedExecutionPayloadEnvelope} | undefined; - }; }, logger: Logger, fork: ForkPostBellatrix, @@ -631,46 +621,11 @@ export async function prepareExecutionPayload( state: IBeaconStateViewBellatrix, suggestedFeeRecipient: string ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { - let parentHash = parentBlockHash; - let withdrawalsOverride: capella.Withdrawal[] | undefined; - - // For Gloas: determine FULL vs EMPTY parent per spec's prepare_execution_payload - // If extending FULL parent: apply parent payload to get correct withdrawals and use bid.blockHash - // If EMPTY parent: use state.payloadExpectedWithdrawals and bid.parentBlockHash - if (isForkPostGloas(fork) && chain.forkChoice && chain.seenPayloadEnvelopeInputCache) { - const gloasState = state as unknown as { - latestExecutionPayloadBid: { - slot: number; - blockHash: Uint8Array; - parentBlockHash: Uint8Array; - builderIndex: number; - value: number; - feeRecipient: Uint8Array; - }; - }; - const parentRootHex = toRootHex(parentBlockRoot); - - if (chain.forkChoice.shouldExtendPayload(parentRootHex)) { - const payloadInput = chain.seenPayloadEnvelopeInputCache.get(parentRootHex); - if (payloadInput?.hasPayloadEnvelope()) { - // Build on FULL variant: apply parent payload to compute correct withdrawals, - // use bid.blockHash as EL head (per spec's prepare_execution_payload) - const gloasView = state as unknown as { - getExpectedWithdrawalsForFullParent(envelope: gloas.SignedExecutionPayloadEnvelope): capella.Withdrawal[]; - }; - withdrawalsOverride = gloasView.getExpectedWithdrawalsForFullParent(payloadInput.getPayloadEnvelope()); - parentHash = gloasState.latestExecutionPayloadBid.blockHash; - } - } else { - // EMPTY parent: use bid.parentBlockHash as the EL head - parentHash = gloasState.latestExecutionPayloadBid.parentBlockHash; - } - } const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime); const prevRandao = state.getRandaoMix(state.epoch); const payloadIdCached = chain.executionEngine.payloadIdCache.get({ - headBlockHash: toRootHex(parentHash), + headBlockHash: toRootHex(parentBlockHash), finalizedBlockHash, timestamp: numToQuantity(timestamp), prevRandao: toHex(prevRandao), @@ -701,12 +656,11 @@ export async function prepareExecutionPayload( parentBlockRoot, parentBlockHash, feeRecipient: suggestedFeeRecipient, - withdrawalsOverride, }); payloadId = await chain.executionEngine.notifyForkchoiceUpdate( fork, - toRootHex(parentHash), + toRootHex(parentBlockHash), safeBlockHash, finalizedBlockHash, attributes @@ -812,14 +766,12 @@ function preparePayloadAttributes( parentBlockRoot, parentBlockHash, feeRecipient, - withdrawalsOverride, }: { prepareState: IBeaconStateViewBellatrix; prepareSlot: Slot; parentBlockRoot: Root; parentBlockHash: Bytes32; feeRecipient: string; - withdrawalsOverride?: capella.Withdrawal[]; } ): SSEPayloadAttributes["payloadAttributes"] { const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime); @@ -835,10 +787,7 @@ function preparePayloadAttributes( throw new Error("Expected Capella state for withdrawals"); } - if (withdrawalsOverride) { - // FULL parent: withdrawals computed from state with parent payload applied - (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = withdrawalsOverride; - } else if (isStatePostGloas(prepareState)) { + if (isStatePostGloas(prepareState)) { const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash); // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch // already deducted from CL balances but never credited on the EL (the envelope @@ -848,7 +797,7 @@ function preparePayloadAttributes( ? prepareState.getExpectedWithdrawals().expectedWithdrawals : prepareState.payloadExpectedWithdrawals; } else { - // Pre-Gloas or Gloas with full parent but no override (shouldn't happen in normal flow) + // withdrawals logic is now fork aware as it changes on electra fork post capella (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals; } From 8967ae52c97f501f4b5ae17a9d3b1c8ac44a196c Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 12:03:29 +0100 Subject: [PATCH 29/61] refactor --- .../chain/blocks/importExecutionPayload.ts | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index d76c9106d241..9d06a4965419 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -72,19 +72,20 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe /** * Import an execution payload envelope after all data is available. * - * The envelope is purely verified here, no state mutation. State effects from the - * payload are applied on the next block via processParentExecutionPayload. + * The envelope is only verified here, no state mutation. State effects from the payload + * are applied on the next block via processParentExecutionPayload. * * Steps: - * 1. Emit `execution_payload_available` for payload attestation + * 1. Emit `execution_payload_available` event 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 (transitions PENDING -> FULL) - * 8. Record metrics - * 9. Emit `execution_payload` event + * 3. Wait for data columns to be available + * 4. Apply write-queue backpressure before verification + * 5. Regenerate block state (post-block, pre-payload) for envelope verification + * 6. Run EL verification (notifyNewPayload) and BLS signature verification in parallel + * 7. Verify envelope fields against state (spec: verify_execution_payload_envelope) + * 8. Persist verified payload envelope to hot DB + * 9. Update fork choice (transitions the block's PENDING variant to FULL) + * 10. Record metrics and emit `execution_payload` event */ export async function importExecutionPayload( this: BeaconChain, @@ -94,14 +95,18 @@ export async function importExecutionPayload( ): Promise { const signedEnvelope = payloadInput.getPayloadEnvelope(); const envelope = signedEnvelope.message; + const slot = envelope.payload.slotNumber; const blockRootHex = payloadInput.blockRootHex; const blockHashHex = payloadInput.getBlockHashHex(); - const fork = this.config.getForkName(envelope.payload.slotNumber); + const fork = this.config.getForkName(slot); - // 1. Emit `execution_payload_available` event at the start of import - if (this.clock.currentSlot - envelope.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { + // 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 only signals availability (not validity), so we can emit + // it before getting a response from the EL on whether the payload is valid or not. + if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { this.emitter.emit(routes.events.EventType.executionPayloadAvailable, { - slot: envelope.payload.slotNumber, + slot, blockRoot: blockRootHex, }); } @@ -123,8 +128,8 @@ export async function importExecutionPayload( // The actual DB write is deferred until after verification succeeds. await this.unfinalizedPayloadEnvelopeWrites.waitForSpace(); - // 5. Get pre-state for envelope verification - // We need the block state (post-block, pre-payload) to verify the envelope + // 5. Get pre-state for envelope verification. + // We need the block state (post-block, pre-payload) to verify the envelope. const blockState = await this.regen.getBlockSlotState( protoBlock, protoBlock.slot, @@ -138,7 +143,7 @@ export async function importExecutionPayload( }); } - // 6. Run verification steps in parallel + // 6. Run EL verification and BLS signature verification in parallel const [execResult, signatureValid] = await Promise.all([ this.executionEngine.notifyNewPayload( fork, @@ -160,10 +165,10 @@ export async function importExecutionPayload( ), ]); - // 5a. Verify envelope fields against state (spec: verify_execution_payload_envelope) + // 7. 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 + // signature and executionRequestsRoot were already verified, skip re-hashing. verifyExecutionPayloadEnvelope(this.config, blockState, envelope, { verifyExecutionRequestsRoot: !opts.validSignature, }); @@ -177,12 +182,12 @@ export async function importExecutionPayload( ); } - // 5b. Check signature verification result + // 7a. Check signature verification result if (!signatureValid) { throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE}); } - // 5c. Handle EL response + // 7b. Handle EL response switch (execResult.status) { case ExecutionPayloadStatus.VALID: break; @@ -208,31 +213,31 @@ export async function importExecutionPayload( }); } - // 6. Persist payload envelope to hot DB + // 8. Persist payload envelope to hot DB (performed asynchronously to avoid blocking) this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => { if (!isQueueErrorAborted(e)) { this.logger.error( "Error pushing payload envelope to unfinalized write queue", - {slot: envelope.payload.slotNumber, blockRoot: blockRootHex}, + {slot, blockRoot: blockRootHex}, e as Error ); } }); - // 7. Update fork choice, transitions the block's PENDING variant to FULL + // 9. Update fork choice, transitions the block's PENDING variant to FULL const execStatus = toForkChoiceExecutionStatus(execResult.status); this.forkChoice.onExecutionPayload(blockRootHex, blockHashHex, envelope.payload.blockNumber, execStatus); - // 8. Record metrics for payload envelope and column sources + // 10. Record metrics for payload envelope and column sources, emit `execution_payload` + // event for recent enough payloads after successful import. this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source}); for (const {source} of payloadInput.getSampledColumnsWithSource()) { this.metrics?.importPayload.columnsBySource.inc({source}); } - // 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) { + if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { this.emitter.emit(routes.events.EventType.executionPayload, { - slot: envelope.payload.slotNumber, + slot, builderIndex: envelope.builderIndex, blockHash: blockHashHex, blockRoot: blockRootHex, @@ -242,7 +247,7 @@ export async function importExecutionPayload( } this.logger.verbose("Execution payload imported", { - slot: envelope.payload.slotNumber, + slot, builderIndex: envelope.builderIndex, blockRoot: blockRootHex, blockHash: blockHashHex, From 5b1e34459bc53a01f92eb63f9a4d1cfc3ed4ea60 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 12:25:41 +0100 Subject: [PATCH 30/61] simplify applyParentExecutionPayload function signature --- .../src/block/processParentExecutionPayload.ts | 9 +++------ .../state-transition/src/stateView/beaconStateView.ts | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index 176aea949fa8..a745b63ab2c3 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -33,7 +33,7 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo ); } - applyParentExecutionPayload(state, parentBid, requests); + applyParentExecutionPayload(state, requests); } /** @@ -45,11 +45,8 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo * * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/beacon-chain.md#new-apply_parent_execution_payload */ -export function applyParentExecutionPayload( - state: CachedBeaconStateGloas, - parentBid: {slot: number; blockHash: Uint8Array; builderIndex: number; value: number; feeRecipient: Uint8Array}, - requests: electra.ExecutionRequests -): void { +export function applyParentExecutionPayload(state: CachedBeaconStateGloas, requests: electra.ExecutionRequests): void { + const parentBid = state.latestExecutionPayloadBid; const fork = state.config.getForkSeq(state.slot); const parentSlot = parentBid.slot; const parentEpoch = computeEpochAtSlot(parentSlot); diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index c9eed49599b7..08ed99db0a84 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -789,7 +789,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { throw Error("getExpectedWithdrawalsForFullParent is only available for gloas+ forks"); } const stateCopy = this.cachedState.clone(true) as CachedBeaconStateGloas; - applyParentExecutionPayload(stateCopy, stateCopy.latestExecutionPayloadBid, envelope.message.executionRequests); + applyParentExecutionPayload(stateCopy, envelope.message.executionRequests); const {expectedWithdrawals} = getExpectedWithdrawals(fork, stateCopy); return expectedWithdrawals; } From 9a9fe6b8d8f4ba945b45200f6715d1d3d3b24880 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 12:58:51 +0100 Subject: [PATCH 31/61] remove genesis block handling, will be separate pr --- .../src/block/processParentExecutionPayload.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index a745b63ab2c3..868dc7b18a67 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -17,9 +17,8 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo const parentBid = state.latestExecutionPayloadBid; const requests = block.body.parentExecutionRequests; - const isGenesisBlock = byteArrayEquals(parentBid.blockHash, ssz.Root.defaultValue()); - const isParentBlockEmpty = !byteArrayEquals(bid.parentBlockHash, parentBid.blockHash); - if (isGenesisBlock || isParentBlockEmpty) { + const isParentBlockFull = byteArrayEquals(bid.parentBlockHash, parentBid.blockHash); + if (!isParentBlockFull) { // Parent was EMPTY -- no execution requests expected assertEmptyExecutionRequests(requests); return; From bf7009e5303e61d176a20ea242c6cd585bc7d44e Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 13:05:00 +0100 Subject: [PATCH 32/61] we don't know and it's wrong, removed confusing todo --- packages/beacon-node/src/chain/forkChoice/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/src/chain/forkChoice/index.ts b/packages/beacon-node/src/chain/forkChoice/index.ts index 3adcd5a19210..312d7c270054 100644 --- a/packages/beacon-node/src/chain/forkChoice/index.ts +++ b/packages/beacon-node/src/chain/forkChoice/index.ts @@ -148,7 +148,7 @@ export function initializeForkChoiceFromFinalizedState( : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}), dataAvailabilityStatus: DataAvailabilityStatus.PreData, - payloadStatus: isForkPostGloas ? PayloadStatus.EMPTY : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY? + payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL, parentBlockHash: isStatePostGloas(state) ? toRootHex(state.latestBlockHash) : null, }, currentSlot @@ -240,7 +240,7 @@ export function initializeForkChoiceFromUnfinalizedState( : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}), dataAvailabilityStatus: DataAvailabilityStatus.PreData, - payloadStatus: isForkPostGloas ? PayloadStatus.EMPTY : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY? + payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL, parentBlockHash: isStatePostGloas(unfinalizedState) ? toRootHex(unfinalizedState.latestBlockHash) : null, }; From d2424d522c1f7038b0f9d4e7ed03862f2b5e826e Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 13:36:12 +0100 Subject: [PATCH 33/61] review computeAnchorCheckpoint --- .../chain/opPools/aggregatedAttestationPool.test.ts | 2 +- .../test/unit/chain/forkChoice/forkChoice.test.ts | 8 ++++---- packages/beacon-node/test/utils/node/beacon.ts | 2 +- packages/cli/src/cmds/beacon/initBeaconState.ts | 6 +++--- packages/state-transition/src/cache/epochCache.ts | 6 +++--- .../src/stateView/beaconStateView.ts | 2 +- .../src/util/computeAnchorCheckpoint.ts | 13 +++++-------- .../state-transition/src/util/epochShuffling.ts | 9 ++------- .../test/unit/cachedBeaconState.test.ts | 6 +++--- 9 files changed, 23 insertions(+), 31 deletions(-) diff --git a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts index febf125248db..1972ba501572 100644 --- a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts @@ -35,7 +35,7 @@ describe.skip(`getAttestationsForBlock vc=${vc}`, () => { () => { originalState = generatePerfTestCachedStateElectra({goBackOneSlot: true, vc}); - const {blockHeader, checkpoint} = computeAnchorCheckpoint(originalState.config, originalState); + const {blockHeader, checkpoint} = computeAnchorCheckpoint(originalState); // TODO figure out why getBlockRootAtSlot(originalState, justifiedSlot) is not the same to justifiedCheckpoint.root const finalizedEpoch = originalState.finalizedCheckpoint.epoch; const finalizedCheckpoint = { diff --git a/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts b/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts index d611ef0efcd7..4708158966b6 100644 --- a/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts +++ b/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts @@ -83,7 +83,7 @@ describe("LodestarForkChoice", () => { * parent (37) - child (38) */ it.skip("getHead - should not consider orphaned block as head", () => { - const {blockHeader} = computeAnchorCheckpoint(config, anchorState); + const {blockHeader} = computeAnchorCheckpoint(anchorState); const finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); const targetBlock = generateSignedBlockAtSlot(32); targetBlock.message.parentRoot = finalizedRoot; @@ -159,7 +159,7 @@ describe("LodestarForkChoice", () => { * finalized - slot 8 (finalized 1) - slot 12 - slot 16 (finalized 2) - slot 20 - slot 24 (finalized 3) - slot 28 - slot 32 (finalized 4) */ it("prune - should prune old blocks", () => { - const {blockHeader} = computeAnchorCheckpoint(config, anchorState); + const {blockHeader} = computeAnchorCheckpoint(anchorState); const finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); const block08 = generateSignedBlockAtSlot(8); block08.message.parentRoot = finalizedRoot; @@ -226,7 +226,7 @@ describe("LodestarForkChoice", () => { * parent (34) - child (35) */ it("getAllNonAncestorBlocks - should get non ancestor nodes", () => { - const {blockHeader} = computeAnchorCheckpoint(config, anchorState); + const {blockHeader} = computeAnchorCheckpoint(anchorState); const finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); const targetBlock = generateSignedBlockAtSlot(32); targetBlock.message.parentRoot = finalizedRoot; @@ -301,7 +301,7 @@ describe("LodestarForkChoice", () => { it.skip("should not filter blocks with unrealized checkpoints = store checkpoints", () => { const blockDelaySec = 0; // C9 is the justified/finalized block - const {blockHeader} = computeAnchorCheckpoint(config, anchorState); + const {blockHeader} = computeAnchorCheckpoint(anchorState); const finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); // C10 const blockW = generateSignedBlockAtSlot(8); diff --git a/packages/beacon-node/test/utils/node/beacon.ts b/packages/beacon-node/test/utils/node/beacon.ts index 2748ac284f76..c6660d1227cc 100644 --- a/packages/beacon-node/test/utils/node/beacon.ts +++ b/packages/beacon-node/test/utils/node/beacon.ts @@ -77,7 +77,7 @@ export async function getDevBeaconNode( // derive wsCheckpoint if not provided if (!wsCheckpoint) { - const {checkpoint} = computeAnchorCheckpoint(config, anchorState); + const {checkpoint} = computeAnchorCheckpoint(anchorState); wsCheckpoint = {root: checkpoint.root, epoch: checkpoint.epoch}; logger.debug("Derived wsCheckpoint", {epoch: checkpoint.epoch}); } diff --git a/packages/cli/src/cmds/beacon/initBeaconState.ts b/packages/cli/src/cmds/beacon/initBeaconState.ts index 0f9fabd43098..c3899ad8b9c4 100644 --- a/packages/cli/src/cmds/beacon/initBeaconState.ts +++ b/packages/cli/src/cmds/beacon/initBeaconState.ts @@ -201,7 +201,7 @@ export async function initBeaconState( logger ); - const {checkpoint} = computeAnchorCheckpoint(chainForkConfig, stateAndCp.anchorState); + const {checkpoint} = computeAnchorCheckpoint(stateAndCp.anchorState); logger.info("Initialized checkpoint state", { slot: stateAndCp.anchorState.slot, @@ -230,7 +230,7 @@ export async function initBeaconState( logger ); - const {checkpoint} = computeAnchorCheckpoint(chainForkConfig, stateAndCp.anchorState); + const {checkpoint} = computeAnchorCheckpoint(stateAndCp.anchorState); logger.info("Initialized checkpoint state", { slot: stateAndCp.anchorState.slot, @@ -289,7 +289,7 @@ export async function initBeaconState( ); const lastProcessedSlot = stateAndCp.anchorState.latestBlockHeader.slot; - const {checkpoint} = computeAnchorCheckpoint(chainForkConfig, stateAndCp.anchorState); + const {checkpoint} = computeAnchorCheckpoint(stateAndCp.anchorState); logger.info("Initialized checkpoint state", { slot: stateAndCp.anchorState.slot, diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index cf0805be7bfc..d65086f7536b 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -346,11 +346,11 @@ export class EpochCache { // BeaconChain could provide a shuffling getter to avoid re-computing shuffling every epoch // in that case, we don't need to compute shufflings again const shufflingGetter = opts?.shufflingGetter; - const previousDecisionRoot = calculateShufflingDecisionRoot(config, state, previousEpoch); + const previousDecisionRoot = calculateShufflingDecisionRoot(state, previousEpoch); const cachedPreviousShuffling = shufflingGetter?.(previousEpoch, previousDecisionRoot); - const currentDecisionRoot = calculateShufflingDecisionRoot(config, state, currentEpoch); + const currentDecisionRoot = calculateShufflingDecisionRoot(state, currentEpoch); const cachedCurrentShuffling = shufflingGetter?.(currentEpoch, currentDecisionRoot); - const nextDecisionRoot = calculateShufflingDecisionRoot(config, state, nextEpoch); + const nextDecisionRoot = calculateShufflingDecisionRoot(state, nextEpoch); const cachedNextShuffling = shufflingGetter?.(nextEpoch, nextDecisionRoot); for (let i = 0; i < validatorCount; i++) { diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 08ed99db0a84..62c74471c9e9 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -471,7 +471,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { } computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { - return computeAnchorCheckpoint(this.config, this.cachedState); + return computeAnchorCheckpoint(this.cachedState); } // Sync committees diff --git a/packages/state-transition/src/util/computeAnchorCheckpoint.ts b/packages/state-transition/src/util/computeAnchorCheckpoint.ts index d262aa4500df..92386ce8983a 100644 --- a/packages/state-transition/src/util/computeAnchorCheckpoint.ts +++ b/packages/state-transition/src/util/computeAnchorCheckpoint.ts @@ -1,16 +1,13 @@ -import {ChainForkConfig} from "@lodestar/config"; import {ZERO_HASH} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; import {BeaconStateAllForks} from "../types.js"; import {computeCheckpointEpochAtStateSlot} from "./epoch.js"; -export function computeAnchorCheckpoint( - _config: ChainForkConfig, - anchorState: BeaconStateAllForks -): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { - let blockHeader: phase0.BeaconBlockHeader; - - blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader); +export function computeAnchorCheckpoint(anchorState: BeaconStateAllForks): { + checkpoint: phase0.Checkpoint; + blockHeader: phase0.BeaconBlockHeader; +} { + const blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader); if (ssz.Root.equals(blockHeader.stateRoot, ZERO_HASH)) { blockHeader.stateRoot = anchorState.hashTreeRoot(); } diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index 24880ec83adf..00d0cb367cd2 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -1,5 +1,4 @@ import {asyncUnshuffleList, unshuffleList} from "@chainsafe/swap-or-not-shuffle"; -import {BeaconConfig} from "@lodestar/config"; import { DOMAIN_BEACON_ATTESTER, GENESIS_SLOT, @@ -133,12 +132,8 @@ export function calculateDecisionRoot(state: BeaconStateAllForks, epoch: Epoch): * - Special case close to genesis block, return the genesis block root * - This is similar to forkchoice.getDependentRoot() function, otherwise we cannot get cached shuffing in attestation verification when syncing from genesis. */ -export function calculateShufflingDecisionRoot( - config: BeaconConfig, - state: BeaconStateAllForks, - epoch: Epoch -): RootHex { +export function calculateShufflingDecisionRoot(state: BeaconStateAllForks, epoch: Epoch): RootHex { return state.slot > GENESIS_SLOT ? calculateDecisionRoot(state, epoch) - : toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(computeAnchorCheckpoint(config, state).blockHeader)); + : toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(computeAnchorCheckpoint(state).blockHeader)); } diff --git a/packages/state-transition/test/unit/cachedBeaconState.test.ts b/packages/state-transition/test/unit/cachedBeaconState.test.ts index 9f887634da1d..db4b02d4d250 100644 --- a/packages/state-transition/test/unit/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/cachedBeaconState.test.ts @@ -162,21 +162,21 @@ describe("CachedBeaconState", () => { const shufflingGetter = (shufflingEpoch: Epoch, dependentRoot: RootHex): EpochShuffling | null => { if ( shufflingEpoch === seedState.epochCtx.epoch - 1 && - dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) + dependentRoot === calculateShufflingDecisionRoot(seedState, shufflingEpoch) ) { return seedState.epochCtx.previousShuffling; } if ( shufflingEpoch === seedState.epochCtx.epoch && - dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) + dependentRoot === calculateShufflingDecisionRoot(seedState, shufflingEpoch) ) { return seedState.epochCtx.currentShuffling; } if ( shufflingEpoch === seedState.epochCtx.epoch + 1 && - dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) + dependentRoot === calculateShufflingDecisionRoot(seedState, shufflingEpoch) ) { return seedState.epochCtx.nextShuffling; } From 9bf51039998a1a7e28d2ccb3067b0b936b9df1fe Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 13:40:11 +0100 Subject: [PATCH 34/61] review fork_choice.test.ts --- .../beacon-node/test/spec/presets/fork_choice.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 699de1135891..d632f3167b6a 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -391,21 +391,21 @@ const forkChoiceTest = // Verify envelope against the post-block state (spec: verify_execution_payload_envelope) const protoBlock = (chain.forkChoice as ForkChoice).getBlockHexDefaultStatus(beaconBlockRoot); if (!protoBlock) throw Error(`Block not found for root ${beaconBlockRoot}`); - const envelopeState = await chain.regen.getBlockSlotState( + const blockState = await chain.regen.getBlockSlotState( protoBlock, protoBlock.slot, {dontTransferCache: true}, - RegenCaller.restApi + RegenCaller.processBlock ); - verifyExecutionPayloadEnvelope(beaconConfig, envelopeState as IBeaconStateViewGloas, envelope.message); + verifyExecutionPayloadEnvelope(beaconConfig, blockState as IBeaconStateViewGloas, envelope.message); // Verify signature const sigValid = await verifyExecutionPayloadEnvelopeSignature( beaconConfig, - envelopeState as IBeaconStateViewGloas, + blockState as IBeaconStateViewGloas, pubkeyCache, envelope, - envelopeState.latestBlockHeader.proposerIndex, + blockState.latestBlockHeader.proposerIndex, chain.bls ); if (!sigValid) throw Error("Invalid execution payload envelope signature"); From 08b27127418220c45b192db9843c62afeb6e004f Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 13:48:38 +0100 Subject: [PATCH 35/61] revert changes to computeAnchorCheckpoint --- .../opPools/aggregatedAttestationPool.test.ts | 2 +- .../unit/chain/forkChoice/forkChoice.test.ts | 8 ++--- .../beacon-node/test/utils/node/beacon.ts | 2 +- .../cli/src/cmds/beacon/initBeaconState.ts | 6 ++-- .../state-transition/src/cache/epochCache.ts | 6 ++-- .../src/stateView/beaconStateView.ts | 2 +- .../src/util/computeAnchorCheckpoint.ts | 32 +++++++++++++------ .../src/util/epochShuffling.ts | 9 ++++-- .../test/unit/cachedBeaconState.test.ts | 6 ++-- 9 files changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts index 1972ba501572..febf125248db 100644 --- a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts @@ -35,7 +35,7 @@ describe.skip(`getAttestationsForBlock vc=${vc}`, () => { () => { originalState = generatePerfTestCachedStateElectra({goBackOneSlot: true, vc}); - const {blockHeader, checkpoint} = computeAnchorCheckpoint(originalState); + const {blockHeader, checkpoint} = computeAnchorCheckpoint(originalState.config, originalState); // TODO figure out why getBlockRootAtSlot(originalState, justifiedSlot) is not the same to justifiedCheckpoint.root const finalizedEpoch = originalState.finalizedCheckpoint.epoch; const finalizedCheckpoint = { diff --git a/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts b/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts index 4708158966b6..d611ef0efcd7 100644 --- a/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts +++ b/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts @@ -83,7 +83,7 @@ describe("LodestarForkChoice", () => { * parent (37) - child (38) */ it.skip("getHead - should not consider orphaned block as head", () => { - const {blockHeader} = computeAnchorCheckpoint(anchorState); + const {blockHeader} = computeAnchorCheckpoint(config, anchorState); const finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); const targetBlock = generateSignedBlockAtSlot(32); targetBlock.message.parentRoot = finalizedRoot; @@ -159,7 +159,7 @@ describe("LodestarForkChoice", () => { * finalized - slot 8 (finalized 1) - slot 12 - slot 16 (finalized 2) - slot 20 - slot 24 (finalized 3) - slot 28 - slot 32 (finalized 4) */ it("prune - should prune old blocks", () => { - const {blockHeader} = computeAnchorCheckpoint(anchorState); + const {blockHeader} = computeAnchorCheckpoint(config, anchorState); const finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); const block08 = generateSignedBlockAtSlot(8); block08.message.parentRoot = finalizedRoot; @@ -226,7 +226,7 @@ describe("LodestarForkChoice", () => { * parent (34) - child (35) */ it("getAllNonAncestorBlocks - should get non ancestor nodes", () => { - const {blockHeader} = computeAnchorCheckpoint(anchorState); + const {blockHeader} = computeAnchorCheckpoint(config, anchorState); const finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); const targetBlock = generateSignedBlockAtSlot(32); targetBlock.message.parentRoot = finalizedRoot; @@ -301,7 +301,7 @@ describe("LodestarForkChoice", () => { it.skip("should not filter blocks with unrealized checkpoints = store checkpoints", () => { const blockDelaySec = 0; // C9 is the justified/finalized block - const {blockHeader} = computeAnchorCheckpoint(anchorState); + const {blockHeader} = computeAnchorCheckpoint(config, anchorState); const finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); // C10 const blockW = generateSignedBlockAtSlot(8); diff --git a/packages/beacon-node/test/utils/node/beacon.ts b/packages/beacon-node/test/utils/node/beacon.ts index c6660d1227cc..2748ac284f76 100644 --- a/packages/beacon-node/test/utils/node/beacon.ts +++ b/packages/beacon-node/test/utils/node/beacon.ts @@ -77,7 +77,7 @@ export async function getDevBeaconNode( // derive wsCheckpoint if not provided if (!wsCheckpoint) { - const {checkpoint} = computeAnchorCheckpoint(anchorState); + const {checkpoint} = computeAnchorCheckpoint(config, anchorState); wsCheckpoint = {root: checkpoint.root, epoch: checkpoint.epoch}; logger.debug("Derived wsCheckpoint", {epoch: checkpoint.epoch}); } diff --git a/packages/cli/src/cmds/beacon/initBeaconState.ts b/packages/cli/src/cmds/beacon/initBeaconState.ts index c3899ad8b9c4..0f9fabd43098 100644 --- a/packages/cli/src/cmds/beacon/initBeaconState.ts +++ b/packages/cli/src/cmds/beacon/initBeaconState.ts @@ -201,7 +201,7 @@ export async function initBeaconState( logger ); - const {checkpoint} = computeAnchorCheckpoint(stateAndCp.anchorState); + const {checkpoint} = computeAnchorCheckpoint(chainForkConfig, stateAndCp.anchorState); logger.info("Initialized checkpoint state", { slot: stateAndCp.anchorState.slot, @@ -230,7 +230,7 @@ export async function initBeaconState( logger ); - const {checkpoint} = computeAnchorCheckpoint(stateAndCp.anchorState); + const {checkpoint} = computeAnchorCheckpoint(chainForkConfig, stateAndCp.anchorState); logger.info("Initialized checkpoint state", { slot: stateAndCp.anchorState.slot, @@ -289,7 +289,7 @@ export async function initBeaconState( ); const lastProcessedSlot = stateAndCp.anchorState.latestBlockHeader.slot; - const {checkpoint} = computeAnchorCheckpoint(stateAndCp.anchorState); + const {checkpoint} = computeAnchorCheckpoint(chainForkConfig, stateAndCp.anchorState); logger.info("Initialized checkpoint state", { slot: stateAndCp.anchorState.slot, diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index d65086f7536b..cf0805be7bfc 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -346,11 +346,11 @@ export class EpochCache { // BeaconChain could provide a shuffling getter to avoid re-computing shuffling every epoch // in that case, we don't need to compute shufflings again const shufflingGetter = opts?.shufflingGetter; - const previousDecisionRoot = calculateShufflingDecisionRoot(state, previousEpoch); + const previousDecisionRoot = calculateShufflingDecisionRoot(config, state, previousEpoch); const cachedPreviousShuffling = shufflingGetter?.(previousEpoch, previousDecisionRoot); - const currentDecisionRoot = calculateShufflingDecisionRoot(state, currentEpoch); + const currentDecisionRoot = calculateShufflingDecisionRoot(config, state, currentEpoch); const cachedCurrentShuffling = shufflingGetter?.(currentEpoch, currentDecisionRoot); - const nextDecisionRoot = calculateShufflingDecisionRoot(state, nextEpoch); + const nextDecisionRoot = calculateShufflingDecisionRoot(config, state, nextEpoch); const cachedNextShuffling = shufflingGetter?.(nextEpoch, nextDecisionRoot); for (let i = 0; i < validatorCount; i++) { diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 62c74471c9e9..08ed99db0a84 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -471,7 +471,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { } computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { - return computeAnchorCheckpoint(this.cachedState); + return computeAnchorCheckpoint(this.config, this.cachedState); } // Sync committees diff --git a/packages/state-transition/src/util/computeAnchorCheckpoint.ts b/packages/state-transition/src/util/computeAnchorCheckpoint.ts index 92386ce8983a..1edb2ac57ca2 100644 --- a/packages/state-transition/src/util/computeAnchorCheckpoint.ts +++ b/packages/state-transition/src/util/computeAnchorCheckpoint.ts @@ -1,20 +1,34 @@ -import {ZERO_HASH} from "@lodestar/params"; +import {ChainForkConfig} from "@lodestar/config"; +import {GENESIS_SLOT, ZERO_HASH} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; import {BeaconStateAllForks} from "../types.js"; +import {blockToHeader} from "./blockRoot.js"; import {computeCheckpointEpochAtStateSlot} from "./epoch.js"; -export function computeAnchorCheckpoint(anchorState: BeaconStateAllForks): { - checkpoint: phase0.Checkpoint; - blockHeader: phase0.BeaconBlockHeader; -} { - const blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader); - if (ssz.Root.equals(blockHeader.stateRoot, ZERO_HASH)) { - blockHeader.stateRoot = anchorState.hashTreeRoot(); +export function computeAnchorCheckpoint( + config: ChainForkConfig, + anchorState: BeaconStateAllForks +): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { + let blockHeader: phase0.BeaconBlockHeader; + let root: Uint8Array; + const blockTypes = config.getForkTypes(anchorState.latestBlockHeader.slot); + + if (anchorState.latestBlockHeader.slot === GENESIS_SLOT) { + const block = blockTypes.BeaconBlock.defaultValue(); + block.stateRoot = anchorState.hashTreeRoot(); + blockHeader = blockToHeader(config, block); + root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); + } else { + blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader); + if (ssz.Root.equals(blockHeader.stateRoot, ZERO_HASH)) { + blockHeader.stateRoot = anchorState.hashTreeRoot(); + } + root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); } return { checkpoint: { - root: ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader), + root, // the checkpoint epoch = computeEpochAtSlot(anchorState.slot) + 1 if slot is not at epoch boundary // this is similar to a process_slots() call epoch: computeCheckpointEpochAtStateSlot(anchorState.slot), diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index 00d0cb367cd2..24880ec83adf 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -1,4 +1,5 @@ import {asyncUnshuffleList, unshuffleList} from "@chainsafe/swap-or-not-shuffle"; +import {BeaconConfig} from "@lodestar/config"; import { DOMAIN_BEACON_ATTESTER, GENESIS_SLOT, @@ -132,8 +133,12 @@ export function calculateDecisionRoot(state: BeaconStateAllForks, epoch: Epoch): * - Special case close to genesis block, return the genesis block root * - This is similar to forkchoice.getDependentRoot() function, otherwise we cannot get cached shuffing in attestation verification when syncing from genesis. */ -export function calculateShufflingDecisionRoot(state: BeaconStateAllForks, epoch: Epoch): RootHex { +export function calculateShufflingDecisionRoot( + config: BeaconConfig, + state: BeaconStateAllForks, + epoch: Epoch +): RootHex { return state.slot > GENESIS_SLOT ? calculateDecisionRoot(state, epoch) - : toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(computeAnchorCheckpoint(state).blockHeader)); + : toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(computeAnchorCheckpoint(config, state).blockHeader)); } diff --git a/packages/state-transition/test/unit/cachedBeaconState.test.ts b/packages/state-transition/test/unit/cachedBeaconState.test.ts index db4b02d4d250..9f887634da1d 100644 --- a/packages/state-transition/test/unit/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/cachedBeaconState.test.ts @@ -162,21 +162,21 @@ describe("CachedBeaconState", () => { const shufflingGetter = (shufflingEpoch: Epoch, dependentRoot: RootHex): EpochShuffling | null => { if ( shufflingEpoch === seedState.epochCtx.epoch - 1 && - dependentRoot === calculateShufflingDecisionRoot(seedState, shufflingEpoch) + dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) ) { return seedState.epochCtx.previousShuffling; } if ( shufflingEpoch === seedState.epochCtx.epoch && - dependentRoot === calculateShufflingDecisionRoot(seedState, shufflingEpoch) + dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) ) { return seedState.epochCtx.currentShuffling; } if ( shufflingEpoch === seedState.epochCtx.epoch + 1 && - dependentRoot === calculateShufflingDecisionRoot(seedState, shufflingEpoch) + dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) ) { return seedState.epochCtx.nextShuffling; } From e62a7a15c19fc668d4b6837a9dc4ec29bdfd635d Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 13:51:16 +0100 Subject: [PATCH 36/61] ahhh damn, we need that for spec tests --- .../src/util/computeAnchorCheckpoint.ts | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/state-transition/src/util/computeAnchorCheckpoint.ts b/packages/state-transition/src/util/computeAnchorCheckpoint.ts index 1edb2ac57ca2..c4f97b9939f1 100644 --- a/packages/state-transition/src/util/computeAnchorCheckpoint.ts +++ b/packages/state-transition/src/util/computeAnchorCheckpoint.ts @@ -1,34 +1,21 @@ import {ChainForkConfig} from "@lodestar/config"; -import {GENESIS_SLOT, ZERO_HASH} from "@lodestar/params"; +import {ZERO_HASH} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; import {BeaconStateAllForks} from "../types.js"; -import {blockToHeader} from "./blockRoot.js"; import {computeCheckpointEpochAtStateSlot} from "./epoch.js"; export function computeAnchorCheckpoint( - config: ChainForkConfig, + _config: ChainForkConfig, anchorState: BeaconStateAllForks ): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { - let blockHeader: phase0.BeaconBlockHeader; - let root: Uint8Array; - const blockTypes = config.getForkTypes(anchorState.latestBlockHeader.slot); - - if (anchorState.latestBlockHeader.slot === GENESIS_SLOT) { - const block = blockTypes.BeaconBlock.defaultValue(); - block.stateRoot = anchorState.hashTreeRoot(); - blockHeader = blockToHeader(config, block); - root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); - } else { - blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader); - if (ssz.Root.equals(blockHeader.stateRoot, ZERO_HASH)) { - blockHeader.stateRoot = anchorState.hashTreeRoot(); - } - root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); + const blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader); + if (ssz.Root.equals(blockHeader.stateRoot, ZERO_HASH)) { + blockHeader.stateRoot = anchorState.hashTreeRoot(); } return { checkpoint: { - root, + root: ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader), // the checkpoint epoch = computeEpochAtSlot(anchorState.slot) + 1 if slot is not at epoch boundary // this is similar to a process_slots() call epoch: computeCheckpointEpochAtStateSlot(anchorState.slot), From 8acf7c81ef7a0ef831b4868eca82df84400e7730 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 13:54:21 +0100 Subject: [PATCH 37/61] remove type cast --- packages/beacon-node/test/spec/presets/fork_choice.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index d632f3167b6a..07fcc8103959 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -389,7 +389,7 @@ const forkChoiceTest = const blockNumber = envelope.message.payload.blockNumber; // Verify envelope against the post-block state (spec: verify_execution_payload_envelope) - const protoBlock = (chain.forkChoice as ForkChoice).getBlockHexDefaultStatus(beaconBlockRoot); + const protoBlock = chain.forkChoice.getBlockHexDefaultStatus(beaconBlockRoot); if (!protoBlock) throw Error(`Block not found for root ${beaconBlockRoot}`); const blockState = await chain.regen.getBlockSlotState( protoBlock, From 24a3da57f00416751be7ea507213957b3947ae17 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 14:04:41 +0100 Subject: [PATCH 38/61] clarify withdrawals --- packages/state-transition/src/block/processWithdrawals.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index e57041d1a1c4..55aa3e00b1c6 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -54,7 +54,9 @@ export function processWithdrawals( } = getExpectedWithdrawals(fork, state); const numWithdrawals = expectedWithdrawals.length; - // After gloas, withdrawals are verified later in verifyExecutionPayloadEnvelope + // Pre-gloas verifies the payload's withdrawals against expectedWithdrawals here. + // Post-gloas, the payload arrives later as an envelope and that consistency check + // happens in verifyExecutionPayloadEnvelope against state.payloadExpectedWithdrawals. if (fork < ForkSeq.gloas) { if (payload === undefined) { throw Error("payload is required for pre-gloas processWithdrawals"); From a04a4d0c4f654fd136e115fc46037e8781020f23 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 14:15:34 +0100 Subject: [PATCH 39/61] review getExpectedWithdrawalsForFullParent --- .../src/stateView/beaconStateView.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 08ed99db0a84..5e2b575d7c18 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -1,7 +1,7 @@ import {CompactMultiProof, ProofType, Tree, createProof} from "@chainsafe/persistent-merkle-tree"; import {BitArray, ByteViews} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {ForkName, ForkSeq, SLOTS_PER_HISTORICAL_ROOT, isForkPostGloas} from "@lodestar/params"; +import {ForkName, ForkSeq, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import { BeaconBlock, BeaconState, @@ -783,14 +783,19 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { return new BeaconStateView(newState); } + /** + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/validator.md#executionpayload + */ getExpectedWithdrawalsForFullParent(envelope: gloas.SignedExecutionPayloadEnvelope): capella.Withdrawal[] { const fork = this.config.getForkSeq(this.cachedState.slot); - if (!isForkPostGloas(this.config.getForkName(this.cachedState.slot))) { - throw Error("getExpectedWithdrawalsForFullParent is only available for gloas+ forks"); + if (fork < ForkSeq.gloas) { + throw new Error("getExpectedWithdrawalsForFullParent is not available before Gloas"); } + // Make a copy of the state to avoid mutability issues const stateCopy = this.cachedState.clone(true) as CachedBeaconStateGloas; + // Apply parent payload before computing withdrawals applyParentExecutionPayload(stateCopy, envelope.message.executionRequests); - const {expectedWithdrawals} = getExpectedWithdrawals(fork, stateCopy); - return expectedWithdrawals; + + return getExpectedWithdrawals(fork, stateCopy).expectedWithdrawals; } } From 2fa1c4f33ee434d6f6ab14375f5ce5be4aedc667 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 14:42:17 +0100 Subject: [PATCH 40/61] review processParentExecutionPayload.ts --- .../src/block/processParentExecutionPayload.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index 868dc7b18a67..326157c7e154 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -97,6 +97,11 @@ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, reque * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/beacon-chain.md#new-settle_builder_payment */ function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: number): void { + if (paymentIndex >= state.builderPendingPayments.length) { + throw new Error( + `Invalid builder payment index paymentIndex=${paymentIndex} limit=${state.builderPendingPayments.length}` + ); + } const payment = state.builderPendingPayments.get(paymentIndex).clone(); if (payment.withdrawal.amount > 0) { state.builderPendingWithdrawals.push(payment.withdrawal); From b89af0b8847f22009bda3945cd18bff166166434 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 15:10:43 +0100 Subject: [PATCH 41/61] review operations.test.ts --- .../beacon-node/test/spec/presets/operations.test.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 8faeeb543492..d35e421096ee 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -11,10 +11,9 @@ import { CachedBeaconStateGloas, ExecutionPayloadStatus, getBlockRootAtSlot, - processSlots, } from "@lodestar/state-transition"; +import * as blockFns from "@lodestar/state-transition/block"; import {AttesterSlashing, altair, bellatrix, capella, electra, gloas, phase0, ssz, sszTypesFor} from "@lodestar/types"; -import * as blockFns from "../../../../state-transition/src/block/index.js"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {ethereumConsensusSpecsTests} from "../specTestVersioning.js"; import {expectEqualBeaconState, inputTypeSszTreeViewDU} from "../utils/expectEqualBeaconState.js"; @@ -111,11 +110,8 @@ const operationFns: Record> = blockFns.processExecutionPayloadBid(state as CachedBeaconStateGloas, testCase.block); }, - parent_execution_payload: (state, testCase: {block: gloas.BeaconBlock}): CachedBeaconStateAllForks => { - // Spec test calls process_slots then process_parent_execution_payload - const postState = processSlots(state, testCase.block.slot); - blockFns.processParentExecutionPayload(postState as CachedBeaconStateGloas, testCase.block); - return postState; + parent_execution_payload: (state, testCase: {block: gloas.BeaconBlock}) => { + blockFns.processParentExecutionPayload(state as CachedBeaconStateGloas, testCase.block); }, payload_attestation: (state, testCase: {payload_attestation: gloas.PayloadAttestation}) => { From c2ef8aa46742fc0072c7e28a8cd955581e6f8d89 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 15:24:53 +0100 Subject: [PATCH 42/61] final pass on processParentExecutionPayload --- .../src/block/processParentExecutionPayload.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index 326157c7e154..69d185a7cbcc 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -45,8 +45,8 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/beacon-chain.md#new-apply_parent_execution_payload */ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, requests: electra.ExecutionRequests): void { - const parentBid = state.latestExecutionPayloadBid; const fork = state.config.getForkSeq(state.slot); + const parentBid = state.latestExecutionPayloadBid; const parentSlot = parentBid.slot; const parentEpoch = computeEpochAtSlot(parentSlot); const currentEpoch = computeEpochAtSlot(state.slot); @@ -74,8 +74,8 @@ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, reque } else if (parentEpoch === currentEpoch - 1) { settleBuilderPayment(state, parentSlot % SLOTS_PER_EPOCH); } else if (parentBid.value > 0) { - // Parent is older than the previous epoch, its payment entry has already been settled or - // evicted, so append the withdrawal directly to ensure the builder still gets paid + // Parent is older than the previous epoch, its payment entry has been evicted from + // builder_pending_payments. Append the withdrawal directly. state.builderPendingWithdrawals.push( ssz.gloas.BuilderPendingWithdrawal.toViewDU({ feeRecipient: parentBid.feeRecipient, From 78a72010b5caa0b610659661cfc5642f503b89eb Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 15:34:34 +0100 Subject: [PATCH 43/61] small nit --- .../src/chain/blocks/verifyExecutionPayloadEnvelope.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts index ed31e4d33769..671ed22eb2e7 100644 --- a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts @@ -86,9 +86,10 @@ export function verifyExecutionPayloadEnvelope( `Parent hash mismatch between payload and state payload=${toRootHex(payload.parentHash)} state=${toRootHex(state.latestBlockHash)}` ); } - if (payload.timestamp !== computeTimeAtSlot(config, state.slot, state.genesisTime)) { + const expectedTimestamp = computeTimeAtSlot(config, state.slot, state.genesisTime); + if (payload.timestamp !== expectedTimestamp) { throw new Error( - `Timestamp mismatch between payload and state payload=${payload.timestamp} state=${computeTimeAtSlot(config, state.slot, state.genesisTime)}` + `Timestamp mismatch between payload and state payload=${payload.timestamp} state=${expectedTimestamp}` ); } From f8c73af4c0872401d0dc909a964c823fe78aa769 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 15:54:31 +0100 Subject: [PATCH 44/61] fix order of gossip checks --- .../validation/executionPayloadEnvelope.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index 831db7d8f0bd..862f86ca6784 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -98,6 +98,15 @@ async function validateExecutionPayloadEnvelope( }); } + // [REJECT] `payload.block_hash == bid.block_hash` + if (toRootHex(payload.blockHash) !== payloadInput.getBlockHashHex()) { + throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { + code: ExecutionPayloadEnvelopeErrorCode.BLOCK_HASH_MISMATCH, + envelopeBlockHash: toRootHex(payload.blockHash), + bidBlockHash: payloadInput.getBlockHashHex(), + }); + } + // [REJECT] `hash_tree_root(envelope.execution_requests) == bid.execution_requests_root` const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests); if (!byteArrayEquals(requestsRoot, payloadInput.getBid().executionRequestsRoot)) { @@ -108,15 +117,6 @@ async function validateExecutionPayloadEnvelope( }); } - // [REJECT] `payload.block_hash == bid.block_hash` - if (toRootHex(payload.blockHash) !== payloadInput.getBlockHashHex()) { - throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { - code: ExecutionPayloadEnvelopeErrorCode.BLOCK_HASH_MISMATCH, - envelopeBlockHash: toRootHex(payload.blockHash), - bidBlockHash: payloadInput.getBlockHashHex(), - }); - } - // Get the block state to verify the builder's signature. const blockState = await chain.regen .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope) From e3999215144617a620ac3402c9227b564cfeed18 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 15:55:01 +0100 Subject: [PATCH 45/61] clarify comment --- packages/beacon-node/src/chain/blocks/types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index 03d37ccb526c..5d64e1a9bb4f 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -43,8 +43,9 @@ export enum BlobSidecarValidation { export type ImportPayloadOpts = { /** - * Set to true if envelope signature was already verified (e.g., during gossip/API validation). - * When false/undefined, signature will be verified during import. + * Set to true when the envelope was already validated upstream (e.g., gossip/API validation): + * signature is trusted and execution_requests_root was already verified against the bid. + * When false/undefined, both are verified during import. */ validSignature?: boolean; }; From 56f686ce52e193dfb448454668479c57a4570b47 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:06:41 +0100 Subject: [PATCH 46/61] review import --- .../chain/blocks/importExecutionPayload.ts | 66 +++++++++---------- .../test/spec/presets/fork_choice.test.ts | 2 +- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index 9d06a4965419..1a445370a3ec 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -20,7 +20,7 @@ export enum PayloadErrorCode { EXECUTION_ENGINE_INVALID = "PAYLOAD_ERROR_EXECUTION_ENGINE_INVALID", EXECUTION_ENGINE_ERROR = "PAYLOAD_ERROR_EXECUTION_ENGINE_ERROR", BLOCK_NOT_IN_FORK_CHOICE = "PAYLOAD_ERROR_BLOCK_NOT_IN_FORK_CHOICE", - STATE_TRANSITION_ERROR = "PAYLOAD_ERROR_STATE_TRANSITION_ERROR", + ENVELOPE_VERIFICATION_ERROR = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR", INVALID_SIGNATURE = "PAYLOAD_ERROR_INVALID_SIGNATURE", } @@ -40,7 +40,7 @@ export type PayloadErrorType = blockRootHex: string; } | { - code: PayloadErrorCode.STATE_TRANSITION_ERROR; + code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR; message: string; } | { @@ -80,12 +80,11 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe * 2. Get the ProtoBlock from fork choice * 3. Wait for data columns to be available * 4. Apply write-queue backpressure before verification - * 5. Regenerate block state (post-block, pre-payload) for envelope verification - * 6. Run EL verification (notifyNewPayload) and BLS signature verification in parallel - * 7. Verify envelope fields against state (spec: verify_execution_payload_envelope) - * 8. Persist verified payload envelope to hot DB - * 9. Update fork choice (transitions the block's PENDING variant to FULL) - * 10. Record metrics and emit `execution_payload` event + * 5. Regenerate state for envelope verification + * 6. Verify envelope (fields against state, signature, and EL — parallel where possible) + * 7. Persist verified payload envelope to hot DB + * 8. Update fork choice (transitions the block's PENDING variant to FULL) + * 9. Record metrics and emit `execution_payload` event */ export async function importExecutionPayload( this: BeaconChain, @@ -128,8 +127,7 @@ export async function importExecutionPayload( // The actual DB write is deferred until after verification succeeds. await this.unfinalizedPayloadEnvelopeWrites.waitForSpace(); - // 5. Get pre-state for envelope verification. - // We need the block state (post-block, pre-payload) to verify the envelope. + // 5. Regenerate state for envelope verification const blockState = await this.regen.getBlockSlotState( protoBlock, protoBlock.slot, @@ -138,12 +136,29 @@ export async function importExecutionPayload( ); if (!isStatePostGloas(blockState)) { throw new PayloadError({ - code: PayloadErrorCode.STATE_TRANSITION_ERROR, - message: `Expected gloas+ block state for payload import, got fork=${blockState.forkName}`, + code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR, + message: `Expected gloas+ state for payload import, got fork=${blockState.forkName}`, }); } - // 6. Run EL verification and BLS signature verification in parallel + // 6. Verify envelope against state (spec: verify_execution_payload_envelope). Run the sync + // field checks first to fail fast before starting the EL + BLS work. When validSignature is + // true, the envelope came from gossip/API where both the signature and executionRequestsRoot + // were already verified, skip re-hashing executionRequestsRoot. + try { + verifyExecutionPayloadEnvelope(this.config, blockState, envelope, { + verifyExecutionRequestsRoot: !opts.validSignature, + }); + } catch (e) { + throw new PayloadError( + { + code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR, + message: (e as Error).message, + }, + `Envelope verification error: ${(e as Error).message}` + ); + } + const [execResult, signatureValid] = await Promise.all([ this.executionEngine.notifyNewPayload( fork, @@ -165,29 +180,10 @@ export async function importExecutionPayload( ), ]); - // 7. 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}` - ); - } - - // 7a. Check signature verification result if (!signatureValid) { throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE}); } - // 7b. Handle EL response switch (execResult.status) { case ExecutionPayloadStatus.VALID: break; @@ -213,7 +209,7 @@ export async function importExecutionPayload( }); } - // 8. Persist payload envelope to hot DB (performed asynchronously to avoid blocking) + // 7. Persist payload envelope to hot DB (performed asynchronously to avoid blocking) this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => { if (!isQueueErrorAborted(e)) { this.logger.error( @@ -224,11 +220,11 @@ export async function importExecutionPayload( } }); - // 9. Update fork choice, transitions the block's PENDING variant to FULL + // 8. Update fork choice, transitions the block's PENDING variant to FULL const execStatus = toForkChoiceExecutionStatus(execResult.status); this.forkChoice.onExecutionPayload(blockRootHex, blockHashHex, envelope.payload.blockNumber, execStatus); - // 10. Record metrics for payload envelope and column sources, emit `execution_payload` + // 9. Record metrics for payload envelope and column sources, emit `execution_payload` // event for recent enough payloads after successful import. this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source}); for (const {source} of payloadInput.getSampledColumnsWithSource()) { diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 07fcc8103959..0719e85d19c4 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -388,7 +388,7 @@ const forkChoiceTest = const blockHash = toHex(envelope.message.payload.blockHash); const blockNumber = envelope.message.payload.blockNumber; - // Verify envelope against the post-block state (spec: verify_execution_payload_envelope) + // Verify envelope against the state const protoBlock = chain.forkChoice.getBlockHexDefaultStatus(beaconBlockRoot); if (!protoBlock) throw Error(`Block not found for root ${beaconBlockRoot}`); const blockState = await chain.regen.getBlockSlotState( From 772820469b11b35f3947defdf2a84d40d95b2454 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:18:56 +0100 Subject: [PATCH 47/61] Update packages/beacon-node/src/chain/blocks/types.ts --- packages/beacon-node/src/chain/blocks/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index 5d64e1a9bb4f..3599bd2f0b63 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -106,6 +106,6 @@ export type FullyVerifiedBlock = { indexedAttestations: IndexedAttestation[]; /** Seen timestamp seconds */ seenTimestampSec: number; - /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync or for merge block */ + /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync */ executionStatus: BlockExecutionStatus; }; From 11f9d1672c03975e2603527c6e67013d0a3ad017 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:19:08 +0100 Subject: [PATCH 48/61] Update packages/beacon-node/test/spec/presets/fork_choice.test.ts --- packages/beacon-node/test/spec/presets/fork_choice.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 0719e85d19c4..71ede563c206 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -619,7 +619,7 @@ const forkChoiceTest = (name.includes("simple_attempted_reorg_without_enough_ffg_votes") || name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch") || name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))) || - // Spec test fixture bug in v1.7.0-alpha.5: wrong_withdrawals envelope SSZ data is + // TODO GLOAS: Spec test fixture bug in v1.7.0-alpha.5: wrong_withdrawals envelope SSZ data is // byte-for-byte identical to the valid envelope, making it impossible to reject name.endsWith("on_execution_payload_envelope__wrong_withdrawals"), }, From 2054188911e51822fa369f8775980c9934cdcd54 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:19:20 +0100 Subject: [PATCH 49/61] Update packages/state-transition/src/block/index.ts --- packages/state-transition/src/block/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/state-transition/src/block/index.ts b/packages/state-transition/src/block/index.ts index 84fbc929c091..9914aa76d535 100644 --- a/packages/state-transition/src/block/index.ts +++ b/packages/state-transition/src/block/index.ts @@ -74,8 +74,8 @@ export function processBlock( // The call to the process_execution_payload must happen before the call to the process_randao as the former depends // on the randao_mix computed with the reveal of the previous block. // Post-gloas: process_execution_payload is not part of block processing. The parent's payload - // effects are applied earlier via processParentExecutionPayload, and each envelope is verified - // out-of-band via verifyExecutionPayloadEnvelope when it arrives. + // effects are applied earlier via processParentExecutionPayload, and each execution payload is + // verified out-of-band via verifyExecutionPayloadEnvelope when it arrives. if ( fork < ForkSeq.gloas && fork >= ForkSeq.bellatrix && From 2a7d8e60b4fbbe13bef560acaaf76650bc324393 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:19:32 +0100 Subject: [PATCH 50/61] Update packages/state-transition/src/block/processWithdrawals.ts --- packages/state-transition/src/block/processWithdrawals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index 55aa3e00b1c6..f667645044a7 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -32,7 +32,7 @@ export function processWithdrawals( state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas, payload?: capella.FullOrBlindedExecutionPayload ): void { - // Return early if this is genesis block or the parent block is empty. + // Return early if this is genesis block or the parent block is empty if (fork >= ForkSeq.gloas) { const stateGloas = state as CachedBeaconStateGloas; const isGenesisBlock = byteArrayEquals(stateGloas.latestBlockHash, ZERO_HASH); From 372f4855dfb701c2d3d0bb1ff32b7b84dd1fd327 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:26:17 +0100 Subject: [PATCH 51/61] . --- .../chain/blocks/importExecutionPayload.ts | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index 1a445370a3ec..23e3f2e59713 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -79,12 +79,12 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe * 1. Emit `execution_payload_available` event for payload attestation * 2. Get the ProtoBlock from fork choice * 3. Wait for data columns to be available - * 4. Apply write-queue backpressure before verification - * 5. Regenerate state for envelope verification - * 6. Verify envelope (fields against state, signature, and EL — parallel where possible) - * 7. Persist verified payload envelope to hot DB - * 8. Update fork choice (transitions the block's PENDING variant to FULL) - * 9. Record metrics and emit `execution_payload` event + * 4. Regenerate state for envelope verification + * 5. Verify envelope (fields against state, signature, and EL in parallel where possible) + * 6. Persist verified payload envelope to hot DB (waits for write-queue space for backpressure) + * 7. Update fork choice (transitions the block's PENDING variant to FULL) + * 8. Record metrics for payload envelope and column sources + * 9. Emit `execution_payload` event */ export async function importExecutionPayload( this: BeaconChain, @@ -119,15 +119,11 @@ export async function importExecutionPayload( }); } - // 3. Wait for data columns to be available before claiming a write-queue slot. + // 3. Wait for data columns to be available. // The helper is shared with future gloas sync services; take the single-item batch form here. await verifyPayloadsDataAvailability([payloadInput], signal); - // 4. Apply backpressure from the write queue, before doing verification work. - // The actual DB write is deferred until after verification succeeds. - await this.unfinalizedPayloadEnvelopeWrites.waitForSpace(); - - // 5. Regenerate state for envelope verification + // 4. Regenerate state for envelope verification const blockState = await this.regen.getBlockSlotState( protoBlock, protoBlock.slot, @@ -141,7 +137,7 @@ export async function importExecutionPayload( }); } - // 6. Verify envelope against state (spec: verify_execution_payload_envelope). Run the sync + // 5. Verify envelope against state (spec: verify_execution_payload_envelope). Run the sync // field checks first to fail fast before starting the EL + BLS work. When validSignature is // true, the envelope came from gossip/API where both the signature and executionRequestsRoot // were already verified, skip re-hashing executionRequestsRoot. @@ -209,7 +205,9 @@ export async function importExecutionPayload( }); } - // 7. Persist payload envelope to hot DB (performed asynchronously to avoid blocking) + // 6. Persist payload envelope to hot DB. Wait for write-queue space here to apply backpressure + // on the import pipeline during sync, then perform the write asynchronously to avoid blocking. + await this.unfinalizedPayloadEnvelopeWrites.waitForSpace(); this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => { if (!isQueueErrorAborted(e)) { this.logger.error( @@ -220,17 +218,17 @@ export async function importExecutionPayload( } }); - // 8. Update fork choice, transitions the block's PENDING variant to FULL + // 7. Update fork choice, transitions the block's PENDING variant to FULL const execStatus = toForkChoiceExecutionStatus(execResult.status); this.forkChoice.onExecutionPayload(blockRootHex, blockHashHex, envelope.payload.blockNumber, execStatus); - // 9. Record metrics for payload envelope and column sources, emit `execution_payload` - // event for recent enough payloads after successful import. + // 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}); } + // 9. Emit `execution_payload` event for recent enough payloads after successful import if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { this.emitter.emit(routes.events.EventType.executionPayload, { slot, From 6218a8ebc540e27b094e895d1e39acee975123d8 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:32:22 +0100 Subject: [PATCH 52/61] wording --- .../beacon-node/src/chain/blocks/importExecutionPayload.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index 23e3f2e59713..a057f9dc2acd 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -137,10 +137,9 @@ export async function importExecutionPayload( }); } - // 5. Verify envelope against state (spec: verify_execution_payload_envelope). Run the sync - // field checks first to fail fast before starting the EL + BLS work. When validSignature is - // true, the envelope came from gossip/API where both the signature and executionRequestsRoot - // were already verified, skip re-hashing executionRequestsRoot. + // 5. Verify envelope fields against state first to fail fast before the EL + BLS work. + // When validSignature is true, gossip/API has already verified both the signature and the + // executionRequestsRoot, so we skip those checks here. try { verifyExecutionPayloadEnvelope(this.config, blockState, envelope, { verifyExecutionRequestsRoot: !opts.validSignature, From e450f5295c3472e1ecbffbe6c171f3591e502266 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:37:45 +0100 Subject: [PATCH 53/61] restore some comments --- packages/beacon-node/src/chain/blocks/importExecutionPayload.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index a057f9dc2acd..c63cb38c4528 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -175,10 +175,12 @@ export async function importExecutionPayload( ), ]); + // 5a. Check signature verification result if (!signatureValid) { throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE}); } + // 5b. Handle EL response switch (execResult.status) { case ExecutionPayloadStatus.VALID: break; From 232e294206f40a3b5fb0b3d426eb728a89a8935a Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:39:22 +0100 Subject: [PATCH 54/61] ok that's it --- .../beacon-node/src/chain/blocks/importExecutionPayload.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index c63cb38c4528..9a0abe45281e 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -154,6 +154,7 @@ export async function importExecutionPayload( ); } + // 5a. Run EL notifyNewPayload and signature verification in parallel const [execResult, signatureValid] = await Promise.all([ this.executionEngine.notifyNewPayload( fork, @@ -175,12 +176,12 @@ export async function importExecutionPayload( ), ]); - // 5a. Check signature verification result + // 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; From e7759b00b9430f330fcadd31d49f9e479050213a Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:41:21 +0100 Subject: [PATCH 55/61] not yet --- packages/beacon-node/src/chain/blocks/importExecutionPayload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index 9a0abe45281e..fcad4de12f03 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -154,7 +154,7 @@ export async function importExecutionPayload( ); } - // 5a. Run EL notifyNewPayload and signature verification in parallel + // 5a. Run EL and signature verification in parallel const [execResult, signatureValid] = await Promise.all([ this.executionEngine.notifyNewPayload( fork, From c06c94618283d1b41c7b12726f567ec170eac9f7 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 16:43:56 +0100 Subject: [PATCH 56/61] why is that comment even modified... --- packages/beacon-node/src/chain/blocks/importExecutionPayload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index fcad4de12f03..9334b97158c2 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -230,7 +230,7 @@ export async function importExecutionPayload( this.metrics?.importPayload.columnsBySource.inc({source}); } - // 9. Emit `execution_payload` event for recent enough payloads after successful import + // 9. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { this.emitter.emit(routes.events.EventType.executionPayload, { slot, From 5ae6863380c46b978627303c03d4c3e52c47c3b8 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 21 Apr 2026 18:06:36 +0200 Subject: [PATCH 57/61] feat: cache the last 2 PayloadEnvelopeInputs --- .../blocks/writePayloadEnvelopeInputToDb.ts | 25 ++++------ .../beacon-node/src/chain/prepareNextSlot.ts | 11 +++++ .../seenCache/seenPayloadEnvelopeInput.ts | 47 +++++++++++-------- .../validation/executionPayloadEnvelope.ts | 6 ++- .../src/stateView/beaconStateView.ts | 4 +- .../src/stateView/interface.ts | 2 +- 6 files changed, 53 insertions(+), 42 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts b/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts index d5c07a563590..3421e6bd40ed 100644 --- a/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +++ b/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts @@ -33,23 +33,14 @@ export async function persistPayloadEnvelopeInput( this: BeaconChain, payloadInput: PayloadEnvelopeInput ): Promise { - await writePayloadEnvelopeInputToDb - .call(this, payloadInput) - .catch((e) => { - this.logger.error( - "Error persisting payload envelope in hot db", - { - slot: payloadInput.slot, - root: payloadInput.blockRootHex, - }, - e - ); - }) - .finally(() => { - this.seenPayloadEnvelopeInputCache.prune(payloadInput.blockRootHex); - this.logger.debug("Pruned payload envelope input", { + await writePayloadEnvelopeInputToDb.call(this, payloadInput).catch((e) => { + this.logger.error( + "Error persisting payload envelope in hot db", + { slot: payloadInput.slot, root: payloadInput.blockRootHex, - }); - }); + }, + e + ); + }); } diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index ec3dfc3286c0..0c37118b3524 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -203,6 +203,17 @@ export class PrepareNextSlotScheduler { }); } + if (ForkSeq[fork] >= ForkSeq.gloas) { + // Cutoff = slot of the parent of the block we'll actually build on (post-reorg). + // Steady state: cache holds just 2 entries — head (parent for next-slot production) + // and head.parent (proposer-boost-reorg fallback). Anything older is evicted. + const finalHead = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHeadRoot); + const finalHeadParent = finalHead && this.chain.forkChoice.getBlockHexDefaultStatus(finalHead.parentRoot); + if (finalHeadParent) { + this.chain.seenPayloadEnvelopeInputCache.pruneBelow(finalHeadParent.slot); + } + } + this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition); // If emitPayloadAttributes is true emit a SSE payloadAttributes event diff --git a/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts b/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts index e36147638061..338fa55d0066 100644 --- a/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts +++ b/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts @@ -1,6 +1,6 @@ import {CheckpointWithHex} from "@lodestar/fork-choice"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; -import {RootHex} from "@lodestar/types"; +import {RootHex, Slot} from "@lodestar/types"; import {Logger} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {SerializedCache} from "../../util/serializedCache.js"; @@ -21,8 +21,20 @@ export type SeenPayloadEnvelopeInputModules = { /** * Cache for tracking PayloadEnvelopeInput instances, keyed by beacon block root. * - * Created during block import when a block is processed. - * Pruned on finalization and after payload is written to DB. + * Created during block import when a block is processed. Two pruning paths: + * - `prepareNextSlot` calls `pruneBelow(headParentSlot)` every slot once the head we'll build + * on is known. + * - `onFinalized` calls `pruneBelow(finalizedSlot)` on every finalization for bulk cleanup. + * + * Steady state (linear chain, healthy progression): the cache holds ~2 entries — the head + * (parent for next-slot production) and its parent (proposer-boost-reorg fallback). It can + * transiently hold more during forks, range-sync bursts, or when `prepareNextSlot` skips + * ticks; subsequent ticks settle it back. + * + * Consumers that miss the cache fall back to DB (`chain.getParentExecutionRequests` / + * `getExecutionPayloadEnvelope`). The authoritative view of "does this block / payload exist + * in the canonical chain" is `forkChoice` — this cache is a latency optimisation for the + * synchronous fast path, not a source of truth. */ export class SeenPayloadEnvelopeInput { private readonly chainEvents: ChainEventEmitter; @@ -58,16 +70,7 @@ export class SeenPayloadEnvelopeInput { } private onFinalized = (checkpoint: CheckpointWithHex): void => { - // Prune all entries with slot < finalized slot - const finalizedSlot = computeStartSlotAtEpoch(checkpoint.epoch); - let deletedCount = 0; - for (const [, input] of this.payloadInputs) { - if (input.slot < finalizedSlot) { - this.evictPayloadInput(input); - deletedCount++; - } - } - this.logger?.debug("SeenPayloadEnvelopeInput.onFinalized deleted cached entries", {deletedCount}); + this.pruneBelow(computeStartSlotAtEpoch(checkpoint.epoch)); }; add(props: CreateFromBlockProps): PayloadEnvelopeInput { @@ -88,17 +91,21 @@ export class SeenPayloadEnvelopeInput { return this.payloadInputs.get(blockRootHex)?.hasPayloadEnvelope() ?? false; } - prune(blockRootHex: RootHex): void { - const payloadInput = this.payloadInputs.get(blockRootHex); - if (payloadInput) { - this.evictPayloadInput(payloadInput); - } - } - size(): number { return this.payloadInputs.size; } + pruneBelow(slot: Slot): void { + let deletedCount = 0; + for (const [, input] of this.payloadInputs) { + if (input.slot < slot) { + this.evictPayloadInput(input); + deletedCount++; + } + } + this.logger?.debug("SeenPayloadEnvelopeInput.pruneBelow deleted entries", {slot, deletedCount}); + } + private evictPayloadInput(payloadInput: PayloadEnvelopeInput): void { this.serializedCache.delete(payloadInput.getSerializedCacheKeys()); this.payloadInputs.delete(payloadInput.blockRootHex); diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index 862f86ca6784..1713e82f8f47 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -47,9 +47,10 @@ async function validateExecutionPayloadEnvelope( // [IGNORE] The node has not seen another valid // `SignedExecutionPayloadEnvelope` for this block root from this builder. + // Fork choice flips to PayloadStatus.FULL during envelope import, so it is the authoritative + // duplicate-detection signal. const envelopeBlock = chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL); - const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); - if (envelopeBlock || payloadInput?.hasPayloadEnvelope()) { + if (envelopeBlock) { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN, blockRoot: blockRootHex, @@ -57,6 +58,7 @@ async function validateExecutionPayloadEnvelope( }); } + const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); if (!payloadInput) { // PayloadEnvelopeInput should have been created during block import throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 5e2b575d7c18..f6ee0017823f 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -786,7 +786,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { /** * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/validator.md#executionpayload */ - getExpectedWithdrawalsForFullParent(envelope: gloas.SignedExecutionPayloadEnvelope): capella.Withdrawal[] { + getExpectedWithdrawalsForFullParent(executionRequests: electra.ExecutionRequests): capella.Withdrawal[] { const fork = this.config.getForkSeq(this.cachedState.slot); if (fork < ForkSeq.gloas) { throw new Error("getExpectedWithdrawalsForFullParent is not available before Gloas"); @@ -794,7 +794,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { // Make a copy of the state to avoid mutability issues const stateCopy = this.cachedState.clone(true) as CachedBeaconStateGloas; // Apply parent payload before computing withdrawals - applyParentExecutionPayload(stateCopy, envelope.message.executionRequests); + applyParentExecutionPayload(stateCopy, executionRequests); return getExpectedWithdrawals(fork, stateCopy).expectedWithdrawals; } diff --git a/packages/state-transition/src/stateView/interface.ts b/packages/state-transition/src/stateView/interface.ts index 45672a11aa24..324fc69e10a6 100644 --- a/packages/state-transition/src/stateView/interface.ts +++ b/packages/state-transition/src/stateView/interface.ts @@ -258,7 +258,7 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu { * Clones the state, applies parent payload effects, then computes withdrawals. * Used by prepare_execution_payload when building on FULL parent. */ - getExpectedWithdrawalsForFullParent(envelope: gloas.SignedExecutionPayloadEnvelope): capella.Withdrawal[]; + getExpectedWithdrawalsForFullParent(executionRequests: electra.ExecutionRequests): capella.Withdrawal[]; } /** From 783d8d35012f7999b09d18ccd883b8e47aab2857 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Apr 2026 12:24:39 +0200 Subject: [PATCH 58/61] refactor: rename finalHead -> updatedHead --- packages/beacon-node/src/chain/prepareNextSlot.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index 0c37118b3524..d2ed2b55d03e 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -207,10 +207,11 @@ export class PrepareNextSlotScheduler { // Cutoff = slot of the parent of the block we'll actually build on (post-reorg). // Steady state: cache holds just 2 entries — head (parent for next-slot production) // and head.parent (proposer-boost-reorg fallback). Anything older is evicted. - const finalHead = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHeadRoot); - const finalHeadParent = finalHead && this.chain.forkChoice.getBlockHexDefaultStatus(finalHead.parentRoot); - if (finalHeadParent) { - this.chain.seenPayloadEnvelopeInputCache.pruneBelow(finalHeadParent.slot); + const updatedHead = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHeadRoot); + const updatedHeadParent = + updatedHead && this.chain.forkChoice.getBlockHexDefaultStatus(updatedHead.parentRoot); + if (updatedHeadParent) { + this.chain.seenPayloadEnvelopeInputCache.pruneBelow(updatedHeadParent.slot); } } From 89a60b7564fa9a41e76601327bbd08818f2a637e Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Apr 2026 14:40:48 +0200 Subject: [PATCH 59/61] chore: revert executionPayloadEnvelope validation --- .../src/chain/validation/executionPayloadEnvelope.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index 1713e82f8f47..862f86ca6784 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -47,10 +47,9 @@ async function validateExecutionPayloadEnvelope( // [IGNORE] The node has not seen another valid // `SignedExecutionPayloadEnvelope` for this block root from this builder. - // Fork choice flips to PayloadStatus.FULL during envelope import, so it is the authoritative - // duplicate-detection signal. const envelopeBlock = chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL); - if (envelopeBlock) { + const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); + if (envelopeBlock || payloadInput?.hasPayloadEnvelope()) { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN, blockRoot: blockRootHex, @@ -58,7 +57,6 @@ async function validateExecutionPayloadEnvelope( }); } - const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); if (!payloadInput) { // PayloadEnvelopeInput should have been created during block import throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { From d7bdb550f83acb6e6590eaf4e1881df852322e8d Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Apr 2026 21:00:01 +0200 Subject: [PATCH 60/61] chore: address comments --- .../src/chain/seenCache/seenPayloadEnvelopeInput.ts | 5 ----- packages/state-transition/src/stateView/beaconStateView.ts | 4 ++-- packages/state-transition/src/stateView/interface.ts | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts b/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts index 338fa55d0066..16610a2b7dd9 100644 --- a/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts +++ b/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts @@ -30,11 +30,6 @@ export type SeenPayloadEnvelopeInputModules = { * (parent for next-slot production) and its parent (proposer-boost-reorg fallback). It can * transiently hold more during forks, range-sync bursts, or when `prepareNextSlot` skips * ticks; subsequent ticks settle it back. - * - * Consumers that miss the cache fall back to DB (`chain.getParentExecutionRequests` / - * `getExecutionPayloadEnvelope`). The authoritative view of "does this block / payload exist - * in the canonical chain" is `forkChoice` — this cache is a latency optimisation for the - * synchronous fast path, not a source of truth. */ export class SeenPayloadEnvelopeInput { private readonly chainEvents: ChainEventEmitter; diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index f6ee0017823f..5e2b575d7c18 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -786,7 +786,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { /** * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/validator.md#executionpayload */ - getExpectedWithdrawalsForFullParent(executionRequests: electra.ExecutionRequests): capella.Withdrawal[] { + getExpectedWithdrawalsForFullParent(envelope: gloas.SignedExecutionPayloadEnvelope): capella.Withdrawal[] { const fork = this.config.getForkSeq(this.cachedState.slot); if (fork < ForkSeq.gloas) { throw new Error("getExpectedWithdrawalsForFullParent is not available before Gloas"); @@ -794,7 +794,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { // Make a copy of the state to avoid mutability issues const stateCopy = this.cachedState.clone(true) as CachedBeaconStateGloas; // Apply parent payload before computing withdrawals - applyParentExecutionPayload(stateCopy, executionRequests); + applyParentExecutionPayload(stateCopy, envelope.message.executionRequests); return getExpectedWithdrawals(fork, stateCopy).expectedWithdrawals; } diff --git a/packages/state-transition/src/stateView/interface.ts b/packages/state-transition/src/stateView/interface.ts index 324fc69e10a6..45672a11aa24 100644 --- a/packages/state-transition/src/stateView/interface.ts +++ b/packages/state-transition/src/stateView/interface.ts @@ -258,7 +258,7 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu { * Clones the state, applies parent payload effects, then computes withdrawals. * Used by prepare_execution_payload when building on FULL parent. */ - getExpectedWithdrawalsForFullParent(executionRequests: electra.ExecutionRequests): capella.Withdrawal[]; + getExpectedWithdrawalsForFullParent(envelope: gloas.SignedExecutionPayloadEnvelope): capella.Withdrawal[]; } /** From 4370934ad9220415e02f473d2acd85e01d620e7f Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 Apr 2026 20:33:30 +0100 Subject: [PATCH 61/61] remove redundant fc call --- packages/beacon-node/src/chain/prepareNextSlot.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index d2ed2b55d03e..0f5e4293934f 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -83,7 +83,7 @@ export class PrepareNextSlotScheduler { const headBlock = this.chain.recomputeForkChoiceHead(ForkchoiceCaller.prepareNextSlot); const {slot: headSlot, blockRoot: headRoot} = headBlock; // may be updated below if we predict a proposer-boost-reorg - let updatedHeadRoot = headRoot; + let updatedHead = headBlock; // PS: previously this was comparing slots, but that gave no leway on the skipped // slots on epoch bounday. Making it more fluid. @@ -148,7 +148,7 @@ export class PrepareNextSlotScheduler { {dontTransferCache: !isEpochTransition}, RegenCaller.predictProposerHead ); - updatedHeadRoot = proposerHeadRoot; + updatedHead = proposerHead; } // Update the builder status, if enabled shoot an api call to check status @@ -166,7 +166,7 @@ export class PrepareNextSlotScheduler { let parentBlockHash: Bytes32; if (isStatePostGloas(updatedPrepareState)) { - parentBlockHash = this.chain.forkChoice.shouldExtendPayload(updatedHeadRoot) + parentBlockHash = this.chain.forkChoice.shouldExtendPayload(updatedHead.blockRoot) ? updatedPrepareState.latestExecutionPayloadBid.blockHash : updatedPrepareState.latestExecutionPayloadBid.parentBlockHash; } else { @@ -189,7 +189,7 @@ export class PrepareNextSlotScheduler { this.chain, this.logger, fork as ForkPostBellatrix, // State is of execution type - fromHex(updatedHeadRoot), + fromHex(updatedHead.blockRoot), parentBlockHash, safeBlockHash, finalizedBlockHash, @@ -207,9 +207,7 @@ export class PrepareNextSlotScheduler { // Cutoff = slot of the parent of the block we'll actually build on (post-reorg). // Steady state: cache holds just 2 entries — head (parent for next-slot production) // and head.parent (proposer-boost-reorg fallback). Anything older is evicted. - const updatedHead = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHeadRoot); - const updatedHeadParent = - updatedHead && this.chain.forkChoice.getBlockHexDefaultStatus(updatedHead.parentRoot); + const updatedHeadParent = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHead.parentRoot); if (updatedHeadParent) { this.chain.seenPayloadEnvelopeInputCache.pruneBelow(updatedHeadParent.slot); }