Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
8 changes: 6 additions & 2 deletions packages/beacon-node/src/chain/forkChoice/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ export function initializeForkChoiceFromFinalizedState(

...(isStatePostBellatrix(state) && state.isExecutionStateType && state.isMergeTransitionComplete
? {
executionPayloadBlockHash: toRootHex(state.latestBlockHash),
executionPayloadBlockHash: isStatePostGloas(state)
? toRootHex(state.latestBlockHash)
: toRootHex(state.latestExecutionPayloadHeader.blockHash),
Comment thread
nflaig marked this conversation as resolved.
Resolved
// TODO GLOAS: executionPayloadNumber is not tracked in BeaconState post-gloas (EIP-7732 removed
// latestExecutionPayloadHeader). Using 0 as unavailable fallback until a solution is found.
executionPayloadNumber: isStatePostGloas(state) ? 0 : state.payloadBlockNumber,
Expand Down Expand Up @@ -247,7 +249,9 @@ export function initializeForkChoiceFromUnfinalizedState(
unfinalizedState.isExecutionStateType &&
unfinalizedState.isMergeTransitionComplete
? {
executionPayloadBlockHash: toRootHex(unfinalizedState.latestBlockHash),
executionPayloadBlockHash: isStatePostGloas(unfinalizedState)
? toRootHex(unfinalizedState.latestBlockHash)
: toRootHex(unfinalizedState.latestExecutionPayloadHeader.blockHash),
// TODO GLOAS: executionPayloadNumber is not tracked in BeaconState post-gloas (EIP-7732 removed
// latestExecutionPayloadHeader). Using 0 as unavailable fallback until a solution is found.
executionPayloadNumber: isStatePostGloas(unfinalizedState) ? 0 : unfinalizedState.payloadBlockNumber,
Expand Down
37 changes: 25 additions & 12 deletions packages/beacon-node/src/chain/prepareNextSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
computeEpochAtSlot,
computeTimeAtSlot,
isStatePostBellatrix,
isStatePostGloas,
} from "@lodestar/state-transition";
import {Slot} from "@lodestar/types";
import {Logger, fromHex, isErrorAborted, sleep} from "@lodestar/utils";
import {Bytes32, Slot} from "@lodestar/types";
import {Logger, fromHex, isErrorAborted, sleep, toRootHex} from "@lodestar/utils";
import {GENESIS_SLOT, ZERO_HASH_HEX} from "../constants/constants.js";
import {BuilderStatus} from "../execution/builder/http.js";
import {Metrics} from "../metrics/index.js";
Expand Down Expand Up @@ -156,25 +157,40 @@ export class PrepareNextSlotScheduler {
this.logger.error("Builder disabled as the check status api failed", {prepareSlot}, e as Error);
});
}
}

if (!isStatePostBellatrix(updatedPrepareState)) {
throw new Error("Expected Bellatrix state for payload attributes");
}

const parentBlockRoot = fromHex(updatedHeadRoot);
let parentBlockHash: Bytes32;
if (isStatePostGloas(updatedPrepareState)) {
parentBlockHash = this.chain.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot))
? updatedPrepareState.latestExecutionPayloadBid.blockHash
: updatedPrepareState.latestExecutionPayloadBid.parentBlockHash;
} else {
parentBlockHash = updatedPrepareState.latestExecutionPayloadHeader.blockHash;
}

if (feeRecipient) {
const preparationTime =
computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime);
if (!isStatePostBellatrix(updatedPrepareState)) {
throw new Error("Expected Bellatrix state for payload preparation");
}

const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice);
const finalizedBlockHash =
this.chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;

// awaiting here instead of throwing an async call because there is no other task
// left for scheduler and this gives nice sematics to catch and log errors in the
// left for scheduler and this gives nice semantics to catch and log errors in the
// try/catch wrapper here.
await prepareExecutionPayload(
this.chain,
this.logger,
fork as ForkPostBellatrix, // State is of execution type
fromHex(updatedHeadRoot),
parentBlockRoot,
parentBlockHash,
safeBlockHash,
finalizedBlockHash,
updatedPrepareState,
Expand All @@ -187,10 +203,6 @@ export class PrepareNextSlotScheduler {
});
}

if (!isStatePostBellatrix(updatedPrepareState)) {
throw new Error("Expected Bellatrix state for payload attributes");
}

this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition);

// If emitPayloadAttributes is true emit a SSE payloadAttributes event
Expand All @@ -201,7 +213,8 @@ export class PrepareNextSlotScheduler {
const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
prepareState: updatedPrepareState,
prepareSlot,
parentBlockRoot: fromHex(headRoot),
parentBlockRoot,
parentBlockHash,
// The likely consumers of this API are builders and will anyway ignore the
// feeRecipient, so just pass zero hash for now till a real use case arises
feeRecipient: "0x0000000000000000000000000000000000000000000000000000000000000000",
Expand Down
48 changes: 34 additions & 14 deletions packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
IBeaconStateView,
type IBeaconStateViewBellatrix,
computeTimeAtSlot,
isParentBlockFull,
isStatePostBellatrix,
isStatePostCapella,
isStatePostGloas,
Expand Down Expand Up @@ -48,7 +47,7 @@ import {
fulu,
gloas,
} from "@lodestar/types";
import {Logger, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
import {ZERO_HASH_HEX} from "../../constants/index.js";
import {numToQuantity} from "../../execution/engine/utils.js";
import {
Expand Down Expand Up @@ -214,11 +213,15 @@ export async function produceBlockBody<T extends BlockType>(
});

// Get execution payload from EL
const parentBlockHash = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot))
? currentState.latestExecutionPayloadBid.blockHash
: currentState.latestExecutionPayloadBid.parentBlockHash;
const prepareRes = await prepareExecutionPayload(
this,
this.logger,
fork,
parentBlockRoot,
parentBlockHash,
safeBlockHash,
finalizedBlockHash ?? ZERO_HASH_HEX,
currentState,
Expand Down Expand Up @@ -255,8 +258,8 @@ export async function produceBlockBody<T extends BlockType>(

// Create self-build execution payload bid
const bid: gloas.ExecutionPayloadBid = {
parentBlockHash: currentState.latestBlockHash,
parentBlockRoot: parentBlockRoot,
parentBlockHash,
parentBlockRoot,
blockHash: executionPayload.blockHash,
prevRandao: currentState.getRandaoMix(currentState.epoch),
feeRecipient: executionPayload.feeRecipient,
Expand Down Expand Up @@ -334,6 +337,7 @@ export async function produceBlockBody<T extends BlockType>(
this.logger,
fork,
parentBlockRoot,
currentState.latestExecutionPayloadHeader.blockHash,
safeBlockHash,
finalizedBlockHash ?? ZERO_HASH_HEX,
currentState,
Expand Down Expand Up @@ -442,6 +446,7 @@ export async function produceBlockBody<T extends BlockType>(
this.logger,
fork,
parentBlockRoot,
currentState.latestExecutionPayloadHeader.blockHash,
safeBlockHash,
finalizedBlockHash ?? ZERO_HASH_HEX,
currentState,
Expand Down Expand Up @@ -607,17 +612,17 @@ export async function prepareExecutionPayload(
logger: Logger,
fork: ForkPostBellatrix,
parentBlockRoot: Root,
parentBlockHash: Bytes32,
safeBlockHash: RootHex,
finalizedBlockHash: RootHex,
state: IBeaconStateViewBellatrix,
suggestedFeeRecipient: string
): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> {
const parentHash = state.latestBlockHash;
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),
Expand Down Expand Up @@ -646,12 +651,13 @@ export async function prepareExecutionPayload(
prepareState: state,
prepareSlot: state.slot,
parentBlockRoot,
parentBlockHash,
feeRecipient: suggestedFeeRecipient,
});

payloadId = await chain.executionEngine.notifyForkchoiceUpdate(
fork,
toRootHex(parentHash),
toRootHex(parentBlockHash),
safeBlockHash,
finalizedBlockHash,
attributes
Expand Down Expand Up @@ -703,20 +709,30 @@ export function getPayloadAttributesForSSE(
prepareState,
prepareSlot,
parentBlockRoot,
parentBlockHash,
feeRecipient,
}: {prepareState: IBeaconStateViewBellatrix; prepareSlot: Slot; parentBlockRoot: Root; feeRecipient: string}
}: {
prepareState: IBeaconStateViewBellatrix;
prepareSlot: Slot;
parentBlockRoot: Root;
parentBlockHash: Bytes32;
feeRecipient: string;
}
): SSEPayloadAttributes {
const parentHash = prepareState.latestBlockHash;
const payloadAttributes = preparePayloadAttributes(fork, chain, {
prepareState,
prepareSlot,
parentBlockRoot,
parentBlockHash,
feeRecipient,
});

let parentBlockNumber: number;
if (isForkPostGloas(fork)) {
const parentBlock = chain.forkChoice.getBlockHexAndBlockHash(toRootHex(parentBlockRoot), toRootHex(parentHash));
const parentBlock = chain.forkChoice.getBlockHexAndBlockHash(
toRootHex(parentBlockRoot),
toRootHex(parentBlockHash)
);
if (parentBlock?.executionPayloadBlockHash == null) {
throw Error(`Parent block not found in fork choice root=${toRootHex(parentBlockRoot)}`);
}
Expand All @@ -730,7 +746,7 @@ export function getPayloadAttributesForSSE(
proposalSlot: prepareSlot,
parentBlockNumber,
parentBlockRoot,
parentBlockHash: parentHash,
parentBlockHash,
payloadAttributes,
};
return ssePayloadAttributes;
Expand All @@ -745,11 +761,13 @@ function preparePayloadAttributes(
prepareState,
prepareSlot,
parentBlockRoot,
parentBlockHash,
feeRecipient,
}: {
prepareState: IBeaconStateViewBellatrix;
prepareSlot: Slot;
parentBlockRoot: Root;
parentBlockHash: Bytes32;
feeRecipient: string;
}
): SSEPayloadAttributes["payloadAttributes"] {
Expand All @@ -766,13 +784,15 @@ function preparePayloadAttributes(
throw new Error("Expected Capella state for withdrawals");
}

if (isStatePostGloas(prepareState) && !isParentBlockFull(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
// 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;
(payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = isExtendingPayload
? prepareState.getExpectedWithdrawals().expectedWithdrawals
: 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
6 changes: 4 additions & 2 deletions packages/beacon-node/src/node/nodejs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {BeaconApiMethods} from "@lodestar/api/beacon/server";
import {BeaconConfig} from "@lodestar/config";
import type {LoggerNode} from "@lodestar/logger/node";
import {ZERO_HASH_HEX} from "@lodestar/params";
import {IBeaconStateView, PubkeyCache, isStatePostBellatrix} from "@lodestar/state-transition";
import {IBeaconStateView, PubkeyCache, isStatePostBellatrix, isStatePostGloas} from "@lodestar/state-transition";
import {phase0} from "@lodestar/types";
import {sleep, toRootHex} from "@lodestar/utils";
import {ProcessShutdownCallback} from "@lodestar/validator";
Expand Down Expand Up @@ -223,7 +223,9 @@ export class BeaconNode {
if (opts.executionEngine.mode === "mock") {
const eth1BlockHash =
isStatePostBellatrix(anchorState) && anchorState.isExecutionStateType
? toRootHex(anchorState.latestBlockHash)
? isStatePostGloas(anchorState)
? toRootHex(anchorState.latestBlockHash)
: toRootHex(anchorState.latestExecutionPayloadHeader.blockHash)
Comment thread
nflaig marked this conversation as resolved.
Resolved
: undefined;
executionEngineOpts = {
...opts.executionEngine,
Expand Down
8 changes: 8 additions & 0 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,14 @@ export class ForkChoice implements IForkChoice {
return this.proposerBoostRoot ?? HEX_ZERO_HASH;
}

/**
* Decides whether to extend an available payload from the previous slot,
* corresponding to the beacon block `blockRoot`.
*/
shouldExtendPayload(blockRoot: RootHex): boolean {
return this.protoArray.shouldExtendPayload(blockRoot, this.proposerBoostRoot);
}

/**
* To predict the proposer head of the next slot. That is, to predict if proposer-boost-reorg could happen.
* Reason why we can't be certain is because information of the head block is not fully available yet
Expand Down
1 change: 1 addition & 0 deletions packages/fork-choice/src/forkChoice/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export interface IForkChoice {
getBlockDefaultStatus(blockRoot: Root): ProtoBlock | null;
getBlockHexDefaultStatus(blockRoot: RootHex): ProtoBlock | null;
getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null;
shouldExtendPayload(blockRoot: RootHex): boolean;
getFinalizedBlock(): ProtoBlock;
getJustifiedBlock(): ProtoBlock;
getFinalizedCheckpointSlot(): Slot;
Expand Down
6 changes: 5 additions & 1 deletion packages/fork-choice/src/protoArray/protoArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ export class ProtoArray {
* Determine if we should extend the payload (prefer FULL over EMPTY)
* Spec: gloas/fork-choice.md#new-should_extend_payload
*
* Returns true if:
* Returns true if payload is verified (FULL variant exists) AND:
* 1. Payload is timely, OR
* 2. No proposer boost root (empty/zero hash), OR
* 3. Proposer boost root's parent is not this block, OR
Expand All @@ -717,6 +717,10 @@ export class ProtoArray {
* @param proposerBoostRoot - Current proposer boost root (from ForkChoice)
*/
shouldExtendPayload(blockRoot: RootHex, proposerBoostRoot: RootHex | null): boolean {
if (!this.hasPayload(blockRoot)) {
return false;
}

// Condition 1: Payload is timely
if (this.isPayloadTimely(blockRoot)) {
return true;
Expand Down
Loading
Loading