diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index c25f83329a9b..9334b97158c2 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -1,14 +1,17 @@ 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 {isStatePostGloas} from "@lodestar/state-transition"; +import {fromHex} from "@lodestar/utils"; import {ExecutionPayloadStatus} from "../../execution/index.js"; import {isQueueErrorAborted} from "../../util/queue/index.js"; 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; @@ -17,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", } @@ -37,7 +40,7 @@ export type PayloadErrorType = blockRootHex: string; } | { - code: PayloadErrorCode.STATE_TRANSITION_ERROR; + code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR; message: string; } | { @@ -69,18 +72,19 @@ 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 + * 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` event for payload attestation + * 2. Get the ProtoBlock from fork choice + * 3. Wait for data columns to be available + * 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, @@ -90,17 +94,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. 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. 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, }); } @@ -114,16 +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. Get pre-state for processExecutionPayloadEnvelope - // We need the block state (post-block, pre-payload) to process the envelope + // 4. Regenerate state for envelope verification const blockState = await this.regen.getBlockSlotState( protoBlock, protoBlock.slot, @@ -132,13 +132,30 @@ 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}`, + }); + } + + // 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, }); + } catch (e) { + throw new PayloadError( + { + code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR, + message: (e as Error).message, + }, + `Envelope verification error: ${(e as Error).message}` + ); } - // 6. Run verification steps in parallel - const [execResult, signatureValid, postPayloadResult] = await Promise.all([ + // 5a. Run EL and signature verification in parallel + const [execResult, signatureValid] = await Promise.all([ this.executionEngine.notifyNewPayload( fork, envelope.payload, @@ -149,45 +166,22 @@ 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]); - })(), - - // 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}` - ); - } - })(), + : verifyExecutionPayloadEnvelopeSignature( + this.config, + blockState, + this.pubkeyCache, + signedEnvelope, + payloadInput.proposerIndex, + this.bls + ), ]); - // 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; @@ -213,47 +207,33 @@ 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. 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( "Error pushing payload envelope to unfinalized write queue", - {slot: envelope.payload.slotNumber, blockRoot: blockRootHex}, + {slot, blockRoot: blockRootHex}, e as Error ); } }); - // 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, 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 + // 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}); } - // 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, only for recent enough payloads + 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, @@ -263,7 +243,7 @@ export async function importExecutionPayload( } this.logger.verbose("Execution payload imported", { - slot: envelope.payload.slotNumber, + slot, builderIndex: envelope.builderIndex, blockRoot: blockRootHex, blockHash: blockHashHex, 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..3599bd2f0b63 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"; @@ -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; }; @@ -88,7 +89,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 +106,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 */ + 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 new file mode 100644 index 000000000000..671ed22eb2e7 --- /dev/null +++ b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts @@ -0,0 +1,129 @@ +import {BeaconConfig} from "@lodestar/config"; +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 = { + verifyExecutionRequestsRoot?: boolean; +}; + +/** + * Verify execution payload envelope fields against the post-block state. + * + * 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, + state: IBeaconStateViewGloas, + envelope: gloas.ExecutionPayloadEnvelope, + opts?: VerifyExecutionPayloadEnvelopeOpts +): void { + const {verifyExecutionRequestsRoot = true} = opts ?? {}; + const payload = envelope.payload; + + // 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(); + } + const headerRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(headerValue); + if (!byteArrayEquals(envelope.beaconBlockRoot, headerRoot)) { + throw new Error( + `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(headerRoot)}` + ); + } + + // 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 (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)}` + ); + } + 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=${expectedTimestamp}` + ); + } + + // 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 by the caller +} + +/** + * Verify the BLS signature of an execution payload envelope. + * + * 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, + 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/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/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/forkChoice/index.ts b/packages/beacon-node/src/chain/forkChoice/index.ts index 48f2fa4c3130..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.PENDING : 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.PENDING : 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, }; diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index a2edacdf77b5..4db6e0c00f68 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -282,6 +282,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 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..862f86ca6784 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"; @@ -107,6 +107,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), + }); + } + // Get the block state to verify the builder's signature. const blockState = await chain.regen .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope) 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..71ede563c206 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, @@ -48,8 +49,13 @@ import { BlockInputSource, } from "../../../src/chain/blocks/blockInput/index.js"; import {AttestationImportOpt, BlobSidecarValidation} from "../../../src/chain/blocks/types.js"; +import { + verifyExecutionPayloadEnvelope, + verifyExecutionPayloadEnvelopeSignature, +} from "../../../src/chain/blocks/verifyExecutionPayloadEnvelope.js"; import {BeaconChain, ChainEvent} from "../../../src/chain/index.js"; import {defaultChainOptions} from "../../../src/chain/options.js"; +import {RegenCaller} from "../../../src/chain/regen/index.js"; import {validateFuluBlockDataColumnSidecars} from "../../../src/chain/validation/dataColumnSidecar.js"; import {ZERO_HASH_HEX} from "../../../src/constants/constants.js"; import {ExecutionPayloadStatus} from "../../../src/execution/engine/interface.js"; @@ -382,6 +388,28 @@ const forkChoiceTest = const blockHash = toHex(envelope.message.payload.blockHash); const blockNumber = envelope.message.payload.blockNumber; + // 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( + protoBlock, + protoBlock.slot, + {dontTransferCache: true}, + RegenCaller.processBlock + ); + verifyExecutionPayloadEnvelope(beaconConfig, blockState as IBeaconStateViewGloas, envelope.message); + + // Verify signature + const sigValid = await verifyExecutionPayloadEnvelopeSignature( + beaconConfig, + blockState as IBeaconStateViewGloas, + pubkeyCache, + envelope, + blockState.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, @@ -393,7 +421,6 @@ const forkChoiceTest = beaconBlockRoot, blockHash, blockNumber, - ZERO_HASH_HEX, ExecutionStatus.Valid ); if (!isValid) throw Error("Expect error since this is a negative test"); @@ -592,12 +619,9 @@ 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"))) || - // 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"))), + // 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"), }, }; }; diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 48fd66e28c50..d35e421096ee 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, @@ -71,18 +71,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) { - return blockFns.processExecutionPayloadEnvelope(state as CachedBeaconStateGloas, testCase.signed_envelope, { - verifySignature: true, - verifyStateRoot: true, - }); - } blockFns.processExecutionPayload(fork, state as CachedBeaconStateBellatrix, testCase.body, { executionPayloadStatus: testCase.execution.execution_valid ? ExecutionPayloadStatus.valid @@ -117,6 +110,10 @@ const operationFns: Record> = blockFns.processExecutionPayloadBid(state as CachedBeaconStateGloas, testCase.block); }, + parent_execution_payload: (state, testCase: {block: gloas.BeaconBlock}) => { + blockFns.processParentExecutionPayload(state as CachedBeaconStateGloas, testCase.block); + }, + payload_attestation: (state, testCase: {payload_attestation: gloas.PayloadAttestation}) => { blockFns.processPayloadAttestation(state as CachedBeaconStateGloas, testCase.payload_attestation); }, @@ -144,7 +141,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; @@ -178,7 +174,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/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 0dd85ca17d18..3e43e43b94a4 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -77,17 +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$/, ], + skippedTests: [], // 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 a1f4c77c55cd..30b495edcbed 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -947,7 +947,6 @@ export class ForkChoice implements IForkChoice { blockRoot: RootHex, executionPayloadBlockHash: RootHex, executionPayloadNumber: number, - executionPayloadStateRoot: RootHex, executionStatus: PayloadExecutionStatus ): void { this.protoArray.onExecutionPayload( @@ -955,7 +954,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 f3ea3c12c846..37b5b470e69c 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 ba886fe509ee..7fb2b5969005 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; @@ -572,7 +555,6 @@ export class ProtoArray { currentSlot: Slot, executionPayloadBlockHash: RootHex, executionPayloadNumber: number, - executionPayloadStateRoot: RootHex, proposerBoostRoot: RootHex | null, executionStatus: PayloadExecutionStatus ): void { @@ -628,7 +610,6 @@ export class ProtoArray { executionStatus, executionPayloadBlockHash, executionPayloadNumber, - stateRoot: executionPayloadStateRoot, }; const fullIndex = this.nodes.length; diff --git a/packages/fork-choice/test/unit/protoArray/gloas.test.ts b/packages/fork-choice/test/unit/protoArray/gloas.test.ts index aff01435db4f..c7b39fbd5929 100644 --- a/packages/fork-choice/test/unit/protoArray/gloas.test.ts +++ b/packages/fork-choice/test/unit/protoArray/gloas.test.ts @@ -272,15 +272,7 @@ describe("Gloas Fork Choice", () => { expect(getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.FULL)).toBeUndefined(); // Call onExecutionPayload - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); // FULL should now exist const fullNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.FULL); @@ -292,15 +284,7 @@ describe("Gloas Fork Choice", () => { const block = createTestBlock(gloasForkSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, gloasForkSlot, null); - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); const fullNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.FULL); const pendingIndex = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.PENDING); @@ -312,24 +296,8 @@ describe("Gloas Fork Choice", () => { const block = createTestBlock(gloasForkSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, gloasForkSlot, null); - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); // Should still only have one FULL node const fullNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.FULL); @@ -345,29 +313,13 @@ describe("Gloas Fork Choice", () => { // Calling onExecutionPayload should throw for pre-Gloas blocks expect(() => - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot - 1, - "0x02", - gloasForkSlot - 1, - stateRoot, - null, - ExecutionStatus.Valid - ) + protoArray.onExecutionPayload("0x02", gloasForkSlot - 1, "0x02", gloasForkSlot - 1, null, ExecutionStatus.Valid) ).toThrow(); }); it("throws for unknown block", () => { expect(() => - protoArray.onExecutionPayload( - "0x99", - gloasForkSlot, - "0x99", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ) + protoArray.onExecutionPayload("0x99", gloasForkSlot, "0x99", gloasForkSlot, null, ExecutionStatus.Valid) ).toThrow(); }); }); @@ -432,15 +384,7 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block, gloasForkSlot, null); // Make execution payload available by creating FULL variant - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); // Vote yes from majority of PTC (>50%) const threshold = Math.floor(PTC_SIZE / 2) + 1; @@ -456,15 +400,7 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block, gloasForkSlot, null); // Make execution payload available by creating FULL variant - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); // Vote yes from exactly 50% (not >50%) const threshold = Math.floor(PTC_SIZE / 2); @@ -480,15 +416,7 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block, gloasForkSlot, null); // Make execution payload available by creating FULL variant - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); // Vote mixed yes/no const threshold = Math.floor(PTC_SIZE / 2) + 1; @@ -541,15 +469,7 @@ describe("Gloas Fork Choice", () => { it("intra-block: EMPTY/FULL variants have PENDING as parent", () => { const block = createTestBlock(gloasForkSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, gloasForkSlot, null); - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); const pendingIndex = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.PENDING); const emptyNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.EMPTY); @@ -563,15 +483,7 @@ describe("Gloas Fork Choice", () => { // Block A const blockA = createTestBlock(gloasForkSlot, "0x02Root", genesisRoot, genesisRoot); protoArray.onBlock(blockA, gloasForkSlot, null); - protoArray.onExecutionPayload( - "0x02Root", - gloasForkSlot, - "0x02Hash", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02Root", gloasForkSlot, "0x02Hash", gloasForkSlot, null, ExecutionStatus.Valid); // Block B extends A's FULL (parentBlockHash matches) const blockB = createTestBlock(gloasForkSlot + 1, "0x03Root", "0x02Root", "0x02Hash"); @@ -600,7 +512,7 @@ describe("Gloas Fork Choice", () => { const blockSlot = gloasForkSlot + 10; const block = createTestBlock(blockSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, blockSlot, null); - protoArray.onExecutionPayload("0x02", blockSlot, "0x02", blockSlot, stateRoot, null, ExecutionStatus.Valid); + protoArray.onExecutionPayload("0x02", blockSlot, "0x02", blockSlot, null, ExecutionStatus.Valid); const emptyIndex = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.EMPTY); if (emptyIndex === undefined) throw new Error("Expected emptyIndex to exist"); @@ -677,7 +589,7 @@ describe("Gloas Fork Choice", () => { const blockSlot = gloasForkSlot + 10; const block = createTestBlock(blockSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, blockSlot, null); - protoArray.onExecutionPayload("0x02", blockSlot, "0x02", blockSlot, stateRoot, null, ExecutionStatus.Valid); + protoArray.onExecutionPayload("0x02", blockSlot, "0x02", blockSlot, null, ExecutionStatus.Valid); const emptyIndex = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.EMPTY); if (emptyIndex === undefined) throw new Error("Expected emptyIndex to exist"); @@ -734,15 +646,7 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block3, gloasForkSlot + 2, null); // Create all three variants for block1: PENDING, EMPTY, FULL - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); // Get block1's variants indices before pruning const block1PendingBefore = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.PENDING); @@ -787,15 +691,7 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block2, gloasForkSlot + 1, null); // Create FULL variant for block1 - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); // Set PTC votes for block1 const threshold = Math.floor(PTC_SIZE / 2) + 1; @@ -822,15 +718,7 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block2, gloasForkSlot + 1, null); // Create all three variants for block1 - protoArray.onExecutionPayload( - "0x02", - gloasForkSlot, - "0x02", - gloasForkSlot, - stateRoot, - null, - ExecutionStatus.Valid - ); + protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, null, ExecutionStatus.Valid); // Verify all three variants exist via the public API const pendingIdx = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.PENDING); diff --git a/packages/state-transition/src/block/index.ts b/packages/state-transition/src/block/index.ts index 2dc24d48bd5b..9914aa76d535 100644 --- a/packages/state-transition/src/block/index.ts +++ b/packages/state-transition/src/block/index.ts @@ -14,8 +14,8 @@ 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"; import {processRandao} from "./processRandao.js"; import {processSyncAggregate} from "./processSyncCommittee.js"; @@ -32,7 +32,7 @@ export { processWithdrawals, processExecutionPayloadBid, processPayloadAttestation, - processExecutionPayloadEnvelope, + processParentExecutionPayload, }; export * from "./externalData.js"; @@ -51,10 +51,16 @@ export function processBlock( ): void { const {verifySignatures = true} = opts ?? {}; + // 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); + } + processBlockHeader(state, block); if (fork >= ForkSeq.gloas) { - // After gloas, processWithdrawals does not take a payload parameter + // 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); @@ -67,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 execution payload 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/processExecutionPayloadEnvelope.ts b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts deleted file mode 100644 index 30f3788358c5..000000000000 --- a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts +++ /dev/null @@ -1,169 +0,0 @@ -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"; -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. -export function processExecutionPayloadEnvelope( - state: CachedBeaconStateGloas, - signedEnvelope: gloas.SignedExecutionPayloadEnvelope, - opts?: ProcessExecutionPayloadEnvelopeOpts -): CachedBeaconStateGloas { - 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; -} - -function validateExecutionPayloadEnvelope( - state: CachedBeaconStateGloas, - envelope: gloas.ExecutionPayloadEnvelope -): void { - const payload = envelope.payload; - - // Cache latest block header state root - if (byteArrayEquals(state.latestBlockHeader.stateRoot, ssz.Root.defaultValue())) { - const previousStateRoot = state.hashTreeRoot(); - state.latestBlockHeader.stateRoot = previousStateRoot; - } - - // Verify consistency with the beacon block - if (!byteArrayEquals(envelope.beaconBlockRoot, state.latestBlockHeader.hashTreeRoot())) { - throw new Error( - `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(state.latestBlockHeader.hashTreeRoot())}` - ); - } - - 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 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)}` - ); - } - - // Skipped: Verify the execution payload is valid -} - -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 new file mode 100644 index 000000000000..69d185a7cbcc --- /dev/null +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -0,0 +1,116 @@ +import {ForkPostGloas, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import {BeaconBlock, electra, 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 the first step of processBlock. + * + * 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; + + const isParentBlockFull = byteArrayEquals(bid.parentBlockHash, parentBid.blockHash); + if (!isParentBlockFull) { + // 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, requests); +} + +/** + * Process the parent's execution requests, queue the builder payment, update payload availability, + * and update the latest block hash. + * + * 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 + */ +export function applyParentExecutionPayload(state: CachedBeaconStateGloas, requests: electra.ExecutionRequests): void { + const fork = state.config.getForkSeq(state.slot); + const parentBid = state.latestExecutionPayloadBid; + const parentSlot = parentBid.slot; + const parentEpoch = computeEpochAtSlot(parentSlot); + const currentEpoch = computeEpochAtSlot(state.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) { + processDepositRequest(fork, state, deposit, pendingValidatorPubkeys); + } + } + + for (const withdrawal of requests.withdrawals) { + processWithdrawalRequest(fork, state, withdrawal); + } + + for (const consolidation of requests.consolidations) { + processConsolidationRequest(state, consolidation); + } + + // Settle the builder payment + if (parentEpoch === currentEpoch) { + settleBuilderPayment(state, SLOTS_PER_EPOCH + (parentSlot % SLOTS_PER_EPOCH)); + } 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 been evicted from + // builder_pending_payments. Append the withdrawal directly. + state.builderPendingWithdrawals.push( + ssz.gloas.BuilderPendingWithdrawal.toViewDU({ + feeRecipient: parentBid.feeRecipient, + amount: parentBid.value, + builderIndex: parentBid.builderIndex, + }) + ); + } + + // Update parent payload availability and latest block hash + state.executionPayloadAvailability.set(parentSlot % SLOTS_PER_HISTORICAL_ROOT, true); + 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 { + 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); + } + 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/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index e2b54f5e183a..f667645044a7 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,9 +32,14 @@ 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; + // 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) @@ -48,7 +54,9 @@ export function processWithdrawals( } = getExpectedWithdrawals(fork, state); const numWithdrawals = expectedWithdrawals.length; - // After gloas, withdrawals are verified later in processExecutionPayloadEnvelope + // 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"); diff --git a/packages/state-transition/src/slot/upgradeStateToGloas.ts b/packages/state-transition/src/slot/upgradeStateToGloas.ts index a269dba199c1..f4bc7ad3ec55 100644 --- a/packages/state-transition/src/slot/upgradeStateToGloas.ts +++ b/packages/state-transition/src/slot/upgradeStateToGloas.ts @@ -48,6 +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.nextWithdrawalIndex = stateGloasCloned.nextWithdrawalIndex; stateGloasView.nextWithdrawalValidatorIndex = stateGloasCloned.nextWithdrawalValidatorIndex; stateGloasView.historicalSummaries = stateGloasCloned.historicalSummaries; diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 578d223741ff..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, @@ -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,19 +783,19 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { return new BeaconStateView(newState); } - processExecutionPayloadEnvelope( - signedEnvelope: gloas.SignedExecutionPayloadEnvelope, - opts?: ProcessExecutionPayloadEnvelopeOpts - ): BeaconStateView { - const fork = this.config.getForkName(this.cachedState.slot); - if (!isForkPostGloas(fork)) { - throw Error(`processExecutionPayloadEnvelope is only available for gloas+ forks, got fork=${fork}`); + /** + * 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 (fork < ForkSeq.gloas) { + throw new Error("getExpectedWithdrawalsForFullParent is not available before Gloas"); } - const postPayloadState = processExecutionPayloadEnvelope( - this.cachedState as CachedBeaconStateGloas, - signedEnvelope, - opts - ); - return new BeaconStateView(postPayloadState); + // 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); + + return getExpectedWithdrawals(fork, stateCopy).expectedWithdrawals; } } diff --git a/packages/state-transition/src/stateView/interface.ts b/packages/state-transition/src/stateView/interface.ts index 5eee21e3af9a..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; - processExecutionPayloadEnvelope( - signedEnvelope: gloas.SignedExecutionPayloadEnvelope, - opts?: ProcessExecutionPayloadEnvelopeOpts - ): IBeaconStateView; + /** + * 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[]; } /** 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), 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(