diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts index a74e3f033db1..2869cb12e793 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts @@ -8,7 +8,12 @@ import { ProtoBlock, } from "@lodestar/fork-choice"; import {ForkSeq} from "@lodestar/params"; -import {CachedBeaconStateAllForks, isExecutionBlockBodyType, isExecutionStateType} from "@lodestar/state-transition"; +import { + CachedBeaconStateAllForks, + isExecutionBlockBodyType, + isExecutionEnabled, + isExecutionStateType, +} from "@lodestar/state-transition"; import {bellatrix, electra} from "@lodestar/types"; import {ErrorAborted, Logger, toRootHex} from "@lodestar/utils"; import {ExecutionPayloadStatus, IExecutionEngine} from "../../execution/engine/interface.js"; @@ -145,7 +150,9 @@ export async function verifyBlockExecutionPayload( const block = blockInput.getBlock(); /** Not null if execution is enabled */ const executionPayloadEnabled = - isExecutionStateType(preState0) && isExecutionBlockBodyType(block.message.body) + isExecutionStateType(preState0) && + isExecutionBlockBodyType(block.message.body) && + isExecutionEnabled(preState0, block.message) ? block.message.body.executionPayload : null; diff --git a/packages/beacon-node/src/chain/forkChoice/index.ts b/packages/beacon-node/src/chain/forkChoice/index.ts index 56da740ee99b..d83dc9949d7e 100644 --- a/packages/beacon-node/src/chain/forkChoice/index.ts +++ b/packages/beacon-node/src/chain/forkChoice/index.ts @@ -18,6 +18,7 @@ import { getBlockRootAtSlot, getEffectiveBalanceIncrementsZeroInactive, isExecutionStateType, + isMergeTransitionComplete, } from "@lodestar/state-transition"; import {Slot, ssz} from "@lodestar/types"; import {Logger, toRootHex} from "@lodestar/utils"; @@ -134,7 +135,7 @@ export function initializeForkChoiceFromFinalizedState( unrealizedFinalizedEpoch: finalizedCheckpoint.epoch, unrealizedFinalizedRoot: toRootHex(finalizedCheckpoint.root), - ...(isExecutionStateType(state) + ...(isExecutionStateType(state) && isMergeTransitionComplete(state) ? { executionPayloadBlockHash: toRootHex(state.latestExecutionPayloadHeader.blockHash), executionPayloadNumber: state.latestExecutionPayloadHeader.blockNumber, @@ -215,7 +216,7 @@ export function initializeForkChoiceFromUnfinalizedState( unrealizedFinalizedEpoch: finalizedCheckpoint.epoch, unrealizedFinalizedRoot: toRootHex(finalizedCheckpoint.root), - ...(isExecutionStateType(unfinalizedState) + ...(isExecutionStateType(unfinalizedState) && isMergeTransitionComplete(unfinalizedState) ? { executionPayloadBlockHash: toRootHex(unfinalizedState.latestExecutionPayloadHeader.blockHash), executionPayloadNumber: unfinalizedState.latestExecutionPayloadHeader.blockNumber, diff --git a/packages/beacon-node/src/chain/validation/block.ts b/packages/beacon-node/src/chain/validation/block.ts index b68f30d6b0cf..974b2c1e0712 100644 --- a/packages/beacon-node/src/chain/validation/block.ts +++ b/packages/beacon-node/src/chain/validation/block.ts @@ -6,6 +6,7 @@ import { computeTimeAtSlot, getBlockProposerSignatureSet, isExecutionBlockBodyType, + isExecutionEnabled, isExecutionStateType, } from "@lodestar/state-transition"; import {SignedBeaconBlock, deneb} from "@lodestar/types"; @@ -139,7 +140,7 @@ export async function validateGossipBlock( if (fork === ForkName.bellatrix) { if (!isExecutionBlockBodyType(block.body)) throw Error("Not merge block type"); const executionPayload = block.body.executionPayload; - if (isExecutionStateType(blockState)) { + if (isExecutionStateType(blockState) && isExecutionEnabled(blockState, block)) { const expectedTimestamp = computeTimeAtSlot(config, blockSlot, chain.genesisTime); if (executionPayload.timestamp !== computeTimeAtSlot(config, blockSlot, chain.genesisTime)) { throw new BlockGossipError(GossipAction.REJECT, { diff --git a/packages/beacon-node/src/node/notifier.ts b/packages/beacon-node/src/node/notifier.ts index aabb1850a060..f40055ec4e80 100644 --- a/packages/beacon-node/src/node/notifier.ts +++ b/packages/beacon-node/src/node/notifier.ts @@ -6,6 +6,7 @@ import { computeEpochAtSlot, computeStartSlotAtEpoch, isExecutionCachedStateType, + isMergeTransitionComplete, } from "@lodestar/state-transition"; import {Epoch} from "@lodestar/types"; import {ErrorAborted, Logger, prettyBytes, prettyBytesShort, sleep} from "@lodestar/utils"; @@ -171,13 +172,18 @@ function getHeadExecutionInfo( // Add execution status to notifier only if head is on/post bellatrix if (isExecutionCachedStateType(headState)) { - const executionPayloadHashInfo = - headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadBlockHash : "empty"; - const executionPayloadNumberInfo = - headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadNumber : NaN; - return [ - `exec-block: ${executionStatusStr}(${executionPayloadNumberInfo} ${prettyBytesShort(executionPayloadHashInfo)})`, - ]; + if (isMergeTransitionComplete(headState)) { + const executionPayloadHashInfo = + headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadBlockHash : "empty"; + const executionPayloadNumberInfo = + headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadNumber : NaN; + return [ + `exec-block: ${executionStatusStr}(${executionPayloadNumberInfo} ${prettyBytesShort( + executionPayloadHashInfo + )})`, + ]; + } + return [`exec-block: ${executionStatusStr}`]; } return []; diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 428b6602d4f2..3286cc146f0b 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -75,12 +75,7 @@ export const defaultSkipOpts: SkipOpts = { /^gloas\/(finality|fork_choice|networking|sanity|transition)\/.*$/, /^gloas\/ssz_static\/ForkChoiceNode.*$/, ], - skippedTests: [ - // These tests validate "first payload" scenarios where is_execution_enabled was false pre-merge. - // Since we removed merge transition support, these code paths no longer exist. - /^bellatrix\/operations\/execution_payload\/.+\/bad_parent_hash_first_payload$/, - /^bellatrix\/sanity\/blocks\/.+\/is_execution_enabled_false$/, - ], + skippedTests: [], skippedRunners: [], }; diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 02234e8f7367..779583d3f9b4 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -10,6 +10,7 @@ import { computeStartSlotAtEpoch, getAttesterSlashableIndices, isExecutionBlockBodyType, + isExecutionEnabled, isExecutionStateType, } from "@lodestar/state-transition"; import {computeUnrealizedCheckpoints} from "@lodestar/state-transition/epoch"; @@ -741,7 +742,7 @@ export class ForkChoice implements IForkChoice { unrealizedFinalizedEpoch: unrealizedFinalizedCheckpoint.epoch, unrealizedFinalizedRoot: unrealizedFinalizedCheckpoint.rootHex, - ...(isExecutionBlockBodyType(block.body) && isExecutionStateType(state) + ...(isExecutionBlockBodyType(block.body) && isExecutionStateType(state) && isExecutionEnabled(state, block) ? { executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash), executionPayloadNumber: block.body.executionPayload.blockNumber, diff --git a/packages/state-transition/src/block/index.ts b/packages/state-transition/src/block/index.ts index a5d67e4c75aa..c208c7964ffa 100644 --- a/packages/state-transition/src/block/index.ts +++ b/packages/state-transition/src/block/index.ts @@ -7,7 +7,7 @@ import { CachedBeaconStateCapella, CachedBeaconStateGloas, } from "../types.js"; -import {getFullOrBlindedPayload} from "../util/execution.js"; +import {getFullOrBlindedPayload, isExecutionEnabled} from "../util/execution.js"; import {BlockExternalData, DataAvailabilityStatus} from "./externalData.js"; import {processBlobKzgCommitments} from "./processBlobKzgCommitments.js"; import {processBlockHeader} from "./processBlockHeader.js"; @@ -67,7 +67,11 @@ 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 - if (fork >= ForkSeq.bellatrix && fork < ForkSeq.gloas) { + if ( + fork < ForkSeq.gloas && + fork >= ForkSeq.bellatrix && + isExecutionEnabled(state as CachedBeaconStateBellatrix, block) + ) { processExecutionPayload(fork, state as CachedBeaconStateBellatrix, block.body, externalData); } diff --git a/packages/state-transition/src/block/processExecutionPayload.ts b/packages/state-transition/src/block/processExecutionPayload.ts index 65f28822a022..0af784074cdb 100644 --- a/packages/state-transition/src/block/processExecutionPayload.ts +++ b/packages/state-transition/src/block/processExecutionPayload.ts @@ -3,7 +3,11 @@ import {ForkName, ForkSeq, isForkPostDeneb} from "@lodestar/params"; import {BeaconBlockBody, BlindedBeaconBlockBody, deneb, isExecutionPayload} from "@lodestar/types"; import {toHex, toRootHex} from "@lodestar/utils"; import {CachedBeaconStateBellatrix, CachedBeaconStateCapella} from "../types.js"; -import {executionPayloadToPayloadHeader, getFullOrBlindedPayloadFromBody} from "../util/execution.js"; +import { + executionPayloadToPayloadHeader, + getFullOrBlindedPayloadFromBody, + isMergeTransitionComplete, +} from "../util/execution.js"; import {computeEpochAtSlot, computeTimeAtSlot, getRandaoMix} from "../util/index.js"; import {BlockExternalData, ExecutionPayloadStatus} from "./externalData.js"; @@ -17,13 +21,15 @@ export function processExecutionPayload( const forkName = ForkName[ForkSeq[fork] as ForkName]; // Verify consistency of the parent hash, block number, base fee per gas and gas limit // with respect to the previous execution payload header - const {latestExecutionPayloadHeader} = state; - if (!byteArrayEquals(payload.parentHash, latestExecutionPayloadHeader.blockHash)) { - throw Error( - `Invalid execution payload parentHash ${toRootHex(payload.parentHash)} latest blockHash ${toRootHex( - latestExecutionPayloadHeader.blockHash - )}` - ); + if (isMergeTransitionComplete(state)) { + const {latestExecutionPayloadHeader} = state; + if (!byteArrayEquals(payload.parentHash, latestExecutionPayloadHeader.blockHash)) { + throw Error( + `Invalid execution payload parentHash ${toRootHex(payload.parentHash)} latest blockHash ${toRootHex( + latestExecutionPayloadHeader.blockHash + )}` + ); + } } // Verify random diff --git a/packages/state-transition/src/util/execution.ts b/packages/state-transition/src/util/execution.ts index a64f21a08699..0f5b450f5ca9 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -2,6 +2,7 @@ import {ForkName, ForkPostBellatrix, ForkPreGloas, ForkSeq} from "@lodestar/para import { BeaconBlock, BeaconBlockBody, + BlindedBeaconBlock, BlindedBeaconBlockBody, ExecutionPayload, ExecutionPayloadHeader, @@ -9,16 +10,54 @@ import { capella, deneb, isBlindedBeaconBlockBody, + isExecutionPayload, ssz, } from "@lodestar/types"; import { BeaconStateAllForks, + BeaconStateBellatrix, BeaconStateCapella, BeaconStateExecutions, CachedBeaconStateAllForks, CachedBeaconStateExecutions, } from "../types.js"; +/** + * Execution enabled = merge is done. + * When (A) state has execution data OR (B) block has execution data + */ +export function isExecutionEnabled(state: BeaconStateExecutions, block: BeaconBlock | BlindedBeaconBlock): boolean { + if (isMergeTransitionComplete(state)) { + return true; + } + + // Throws if not post-bellatrix block. A fork-guard before isExecutionEnabled() prevents this from happening + const payload = getFullOrBlindedPayload(block); + + return isExecutionPayload(payload) + ? !ssz.bellatrix.ExecutionPayload.equals(payload, ssz.bellatrix.ExecutionPayload.defaultValue()) + : !ssz.bellatrix.ExecutionPayloadHeader.equals( + state.latestExecutionPayloadHeader, + ssz.bellatrix.ExecutionPayloadHeader.defaultValue() + ); +} + +/** + * Merge is complete when the state includes execution layer data: + * state.latestExecutionPayloadHeader NOT EMPTY or state is post-capella + */ +export function isMergeTransitionComplete(state: BeaconStateExecutions): boolean { + if (isCapellaStateType(state)) { + // All networks have completed the merge transition before capella + return true; + } + + return !ssz.bellatrix.ExecutionPayloadHeader.equals( + (state as BeaconStateBellatrix).latestExecutionPayloadHeader, + ssz.bellatrix.ExecutionPayloadHeader.defaultValue() + ); +} + /** Type guard for bellatrix.BeaconState */ export function isExecutionStateType(state: BeaconStateAllForks): state is BeaconStateExecutions { return (state as BeaconStateExecutions).latestExecutionPayloadHeader !== undefined;