Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
ValidatorIndex,
Wei,
deneb,
electra,
gloas,
isBlindedBeaconBlock,
phase0,
Expand Down Expand Up @@ -886,6 +887,17 @@ export class BeaconChain implements IBeaconChain {
);
}

async getParentExecutionRequests(
parentBlockSlot: Slot,
parentBlockRootHex: RootHex
): Promise<electra.ExecutionRequests> {
const envelope = await this.getExecutionPayloadEnvelope(parentBlockSlot, parentBlockRootHex);
if (envelope === null) {
throw Error(`Parent execution payload envelope not found slot=${parentBlockSlot}, root=${parentBlockRootHex}`);
}
Comment thread
nflaig marked this conversation as resolved.
return envelope.message.executionRequests;
}
Comment thread
nflaig marked this conversation as resolved.

async getDataColumnSidecars(blockSlot: Slot, blockRootHex: string): Promise<DataColumnSidecar[]> {
const fork = this.config.getForkName(blockSlot);

Expand Down
2 changes: 2 additions & 0 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
altair,
capella,
deneb,
electra,
gloas,
phase0,
rewards,
Expand Down Expand Up @@ -231,6 +232,7 @@ export interface IBeaconChain {
blockSlot: Slot,
blockRootHex: string
): Promise<gloas.SignedExecutionPayloadEnvelope | null>;
getParentExecutionRequests(parentBlockSlot: Slot, parentBlockRootHex: RootHex): Promise<electra.ExecutionRequests>;

produceCommonBlockBody(blockAttributes: BlockAttributes): Promise<CommonBlockBody>;
produceBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise<CommonBlockBody>}): Promise<{
Expand Down
27 changes: 24 additions & 3 deletions packages/beacon-node/src/chain/prepareNextSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -194,7 +206,8 @@ export class PrepareNextSlotScheduler {
safeBlockHash,
finalizedBlockHash,
updatedPrepareState,
feeRecipient
feeRecipient,
parentExecutionRequests
);
this.logger.verbose("PrepareNextSlotScheduler prepared new payload", {
prepareSlot,
Expand All @@ -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});
}
Expand Down
41 changes: 30 additions & 11 deletions packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,13 @@ export async function produceBlockBody<T extends BlockType>(
});

// 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,
Expand All @@ -226,7 +230,8 @@ export async function produceBlockBody<T extends BlockType>(
safeBlockHash,
finalizedBlockHash ?? ZERO_HASH_HEX,
currentState,
feeRecipient
feeRecipient,
parentExecutionRequests
);

const {prepType, payloadId} = prepareRes;
Expand Down Expand Up @@ -282,7 +287,7 @@ export async function produceBlockBody<T extends BlockType>(
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<T>;

// Store execution payload data required to construct execution payload envelope later
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -656,6 +662,7 @@ export async function prepareExecutionPayload(
parentBlockRoot,
parentBlockHash,
feeRecipient: suggestedFeeRecipient,
parentExecutionRequests,
});

payloadId = await chain.executionEngine.notifyForkchoiceUpdate(
Expand Down Expand Up @@ -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, {
Expand All @@ -728,6 +737,7 @@ export function getPayloadAttributesForSSE(
parentBlockRoot,
parentBlockHash,
feeRecipient,
parentExecutionRequests,
});

let parentBlockNumber: number;
Expand Down Expand Up @@ -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);
Expand All @@ -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 =
Expand Down
4 changes: 2 additions & 2 deletions packages/state-transition/src/stateView/beaconStateView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,15 +786,15 @@ 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");
}
// Make a copy of the state to avoid mutability issues
const stateCopy = this.cachedState.clone(true) as CachedBeaconStateGloas;
Comment thread
nflaig marked this conversation as resolved.
// Apply parent payload before computing withdrawals
applyParentExecutionPayload(stateCopy, envelope.message.executionRequests);
applyParentExecutionPayload(stateCopy, executionRequests);

return getExpectedWithdrawals(fork, stateCopy).expectedWithdrawals;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/state-transition/src/stateView/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

/**
Expand Down
Loading