diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 3054d54a0ad8..921ac115c081 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, @@ -886,6 +887,17 @@ export class BeaconChain implements IBeaconChain { ); } + async getParentExecutionRequests( + parentBlockSlot: Slot, + parentBlockRootHex: RootHex + ): Promise { + const envelope = await this.getExecutionPayloadEnvelope(parentBlockSlot, parentBlockRootHex); + if (envelope === null) { + throw Error(`Parent execution payload envelope not found slot=${parentBlockSlot}, root=${parentBlockRootHex}`); + } + return envelope.message.executionRequests; + } + async getDataColumnSidecars(blockSlot: Slot, blockRootHex: string): Promise { const fork = this.config.getForkName(blockSlot); diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 3b21ef24ece7..ea4b19465e80 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -18,6 +18,7 @@ import { altair, capella, deneb, + electra, gloas, phase0, rewards, @@ -231,6 +232,7 @@ export interface IBeaconChain { blockSlot: Slot, blockRootHex: string ): Promise; + getParentExecutionRequests(parentBlockSlot: Slot, parentBlockRootHex: RootHex): Promise; produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; produceBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise}): Promise<{ diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index d7c29e8e227e..a57126faf9bd 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -10,7 +10,7 @@ import { isStatePostBellatrix, isStatePostGloas, } from "@lodestar/state-transition"; -import {Bytes32, Slot} from "@lodestar/types"; +import {Bytes32, Slot, electra} from "@lodestar/types"; import {Logger, fromHex, isErrorAborted, sleep} from "@lodestar/utils"; import {GENESIS_SLOT, ZERO_HASH_HEX} from "../constants/constants.js"; import {BuilderStatus} from "../execution/builder/http.js"; @@ -165,14 +165,19 @@ export class PrepareNextSlotScheduler { } let parentBlockHash: Bytes32; + let isExtendingPayload = false; if (isStatePostGloas(updatedPrepareState)) { - parentBlockHash = this.chain.forkChoice.shouldExtendPayload(updatedHead.blockRoot) + isExtendingPayload = this.chain.forkChoice.shouldExtendPayload(updatedHead.blockRoot); + parentBlockHash = isExtendingPayload ? updatedPrepareState.latestExecutionPayloadBid.blockHash : updatedPrepareState.latestExecutionPayloadBid.parentBlockHash; } else { parentBlockHash = updatedPrepareState.latestExecutionPayloadHeader.blockHash; } + // Reused by the SSE emit below to avoid a second DB lookup on cache miss + let parentExecutionRequests: electra.ExecutionRequests | undefined; + if (feeRecipient) { const preparationTime = computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000; @@ -182,6 +187,13 @@ export class PrepareNextSlotScheduler { const finalizedBlockHash = this.chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; + if (isExtendingPayload) { + parentExecutionRequests = await this.chain.getParentExecutionRequests( + updatedHead.slot, + updatedHead.blockRoot + ); + } + // awaiting here instead of throwing an async call because there is no other task // left for scheduler and this gives nice semantics to catch and log errors in the // try/catch wrapper here. @@ -194,7 +206,8 @@ export class PrepareNextSlotScheduler { safeBlockHash, finalizedBlockHash, updatedPrepareState, - feeRecipient + feeRecipient, + parentExecutionRequests ); this.logger.verbose("PrepareNextSlotScheduler prepared new payload", { prepareSlot, @@ -221,12 +234,20 @@ export class PrepareNextSlotScheduler { (feeRecipient || this.chain.opts.emitPayloadAttributes === true) && this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes) ) { + // if we didn't fetch above (not proposing), SSE still needs it here + if (!parentExecutionRequests && isExtendingPayload) { + parentExecutionRequests = await this.chain.getParentExecutionRequests( + updatedHead.slot, + updatedHead.blockRoot + ); + } const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, { prepareState: updatedPrepareState, prepareSlot, parentBlockRoot: fromHex(updatedHead.blockRoot), parentBlockHash, feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000", + parentExecutionRequests, }); this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork}); } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 4db6e0c00f68..f92922906136 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -214,9 +214,13 @@ export async function produceBlockBody( }); // Get execution payload from EL - const parentBlockHash = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot)) + const isExtendingPayload = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot)); + const parentBlockHash = isExtendingPayload ? currentState.latestExecutionPayloadBid.blockHash : currentState.latestExecutionPayloadBid.parentBlockHash; + const parentExecutionRequests = isExtendingPayload + ? await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot) + : ssz.electra.ExecutionRequests.defaultValue(); const prepareRes = await prepareExecutionPayload( this, this.logger, @@ -226,7 +230,8 @@ export async function produceBlockBody( safeBlockHash, finalizedBlockHash ?? ZERO_HASH_HEX, currentState, - feeRecipient + feeRecipient, + parentExecutionRequests ); const {prepType, payloadId} = prepareRes; @@ -282,7 +287,7 @@ export async function produceBlockBody( gloasBody.signedExecutionPayloadBid = signedBid; // TODO GLOAS: Get payload attestations from pool for previous slot gloasBody.payloadAttestations = []; - // TODO GLOAS: set parentExecutionRequests in the block body + gloasBody.parentExecutionRequests = parentExecutionRequests; blockBody = gloasBody as AssembledBodyType; // Store execution payload data required to construct execution payload envelope later @@ -619,7 +624,8 @@ export async function prepareExecutionPayload( safeBlockHash: RootHex, finalizedBlockHash: RootHex, state: IBeaconStateViewBellatrix, - suggestedFeeRecipient: string + suggestedFeeRecipient: string, + parentExecutionRequests?: electra.ExecutionRequests ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime); const prevRandao = state.getRandaoMix(state.epoch); @@ -656,6 +662,7 @@ export async function prepareExecutionPayload( parentBlockRoot, parentBlockHash, feeRecipient: suggestedFeeRecipient, + parentExecutionRequests, }); payloadId = await chain.executionEngine.notifyForkchoiceUpdate( @@ -714,12 +721,14 @@ export function getPayloadAttributesForSSE( parentBlockRoot, parentBlockHash, feeRecipient, + parentExecutionRequests, }: { prepareState: IBeaconStateViewBellatrix; prepareSlot: Slot; parentBlockRoot: Root; parentBlockHash: Bytes32; feeRecipient: string; + parentExecutionRequests?: electra.ExecutionRequests; } ): SSEPayloadAttributes { const payloadAttributes = preparePayloadAttributes(fork, chain, { @@ -728,6 +737,7 @@ export function getPayloadAttributesForSSE( parentBlockRoot, parentBlockHash, feeRecipient, + parentExecutionRequests, }); let parentBlockNumber: number; @@ -766,12 +776,14 @@ function preparePayloadAttributes( parentBlockRoot, parentBlockHash, feeRecipient, + parentExecutionRequests, }: { prepareState: IBeaconStateViewBellatrix; prepareSlot: Slot; parentBlockRoot: Root; parentBlockHash: Bytes32; feeRecipient: string; + parentExecutionRequests?: electra.ExecutionRequests; } ): SSEPayloadAttributes["payloadAttributes"] { const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime); @@ -789,13 +801,20 @@ function preparePayloadAttributes( 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 - // was not delivered). The next payload must carry those same withdrawals to - // restore CL/EL consistency, otherwise validators permanently lose that balance. - (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = isExtendingPayload - ? prepareState.getExpectedWithdrawals().expectedWithdrawals - : prepareState.payloadExpectedWithdrawals; + if (isExtendingPayload) { + if (parentExecutionRequests === undefined) { + throw new Error("parentExecutionRequests required when extending full parent"); + } + (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = + prepareState.getExpectedWithdrawalsForFullParent(parentExecutionRequests); + } else { + // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch + // already deducted from CL balances but never credited on the EL (the envelope + // was not delivered). The next payload must carry those same withdrawals to + // restore CL/EL consistency, otherwise validators permanently lose that balance. + (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = + prepareState.payloadExpectedWithdrawals; + } } else { // withdrawals logic is now fork aware as it changes on electra fork post capella (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = 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[]; } /**