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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {routes} from "@lodestar/api";
import {
AncestorStatus,
EpochDifference,
ExecutionStatus,
ForkChoiceError,
ForkChoiceErrorCode,
NotReorgedReason,
Expand Down Expand Up @@ -84,7 +85,7 @@ export async function importBlock(
fullyVerifiedBlock: FullyVerifiedBlock,
opts: ImportBlockOpts
): Promise<void> {
const {blockInput, postState, parentBlockSlot, executionStatus, dataAvailabilityStatus, indexedAttestations} =
const {blockInput, postBlockState, parentBlockSlot, executionStatus, dataAvailabilityStatus, indexedAttestations} =
fullyVerifiedBlock;
const block = blockInput.getBlock();
const source = blockInput.getBlockSource();
Expand All @@ -96,7 +97,7 @@ export async function importBlock(
const blockEpoch = computeEpochAtSlot(blockSlot);
const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
const blockDelaySec =
fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postState.genesisTime);
fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postBlockState.genesisTime);
const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
const fork = this.config.getForkSeq(blockSlot);

Expand All @@ -119,13 +120,13 @@ export async function importBlock(
// 2. Import block to fork choice

// Should compute checkpoint balances before forkchoice.onBlock
this.checkpointBalancesCache.processState(blockRootHex, postState);
this.checkpointBalancesCache.processState(blockRootHex, postBlockState);
const blockSummary = this.forkChoice.onBlock(
block.message,
postState,
postBlockState,
blockDelaySec,
currentSlot,
executionStatus,
fork >= ForkSeq.gloas ? ExecutionStatus.PayloadSeparated : executionStatus,
dataAvailabilityStatus
);

Expand All @@ -135,7 +136,7 @@ export async function importBlock(
// Post-Gloas: blockSummary.payloadStatus is always PENDING, so payloadPresent = false (block state only, no payload processing yet)
const payloadPresent = !isGloasBlock(blockSummary);
// processState manages both block state and payload state variants together for memory/disk management
this.regen.processBlockState(blockRootHex, postState);
this.regen.processBlockState(blockRootHex, postBlockState);

// For Gloas blocks, create PayloadEnvelopeInput so it's available for later payload import
if (fork >= ForkSeq.gloas) {
Expand Down Expand Up @@ -171,7 +172,7 @@ export async function importBlock(
(opts.importAttestations !== AttestationImportOpt.Skip && blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT)
) {
const attestations = block.message.body.attestations;
const rootCache = new RootCache(postState);
const rootCache = new RootCache(postBlockState);
const invalidAttestationErrorsByCode = new Map<string, {error: Error; count: number}>();

const addAttestation = fork >= ForkSeq.electra ? addAttestationPostElectra : addAttestationPreElectra;
Expand All @@ -185,7 +186,7 @@ export async function importBlock(
const attDataRoot = toRootHex(ssz.phase0.AttestationData.hashTreeRoot(indexedAttestation.data));
addAttestation.call(
this,
postState,
postBlockState,
target,
attDataRoot,
attestation as Attestation<ForkPostElectra>,
Expand Down Expand Up @@ -300,7 +301,7 @@ export async function importBlock(

if (newHead.blockRoot !== oldHead.blockRoot) {
// Set head state as strong reference
this.regen.updateHeadState(newHead, postState);
this.regen.updateHeadState(newHead, postBlockState);

try {
this.emitter.emit(routes.events.EventType.head, {
Expand Down Expand Up @@ -372,7 +373,7 @@ export async function importBlock(
try {
this.lightClientServer?.onImportBlockHead(
block.message as BeaconBlock<ForkPostAltair>,
postState,
postBlockState,
parentBlockSlot
);
} catch (e) {
Expand All @@ -393,11 +394,11 @@ export async function importBlock(
// and the block is weak and can potentially be reorged out.
let shouldOverrideFcu = false;

if (blockSlot >= currentSlot && postState.isExecutionStateType) {
if (blockSlot >= currentSlot && postBlockState.isExecutionStateType) {
let notOverrideFcuReason = NotReorgedReason.Unknown;
const proposalSlot = blockSlot + 1;
try {
const proposerIndex = postState.getBeaconProposer(proposalSlot);
const proposerIndex = postBlockState.getBeaconProposer(proposalSlot);
const feeRecipient = this.beaconProposerCache.get(proposerIndex);

if (feeRecipient) {
Expand Down Expand Up @@ -477,20 +478,20 @@ export async function importBlock(
}
}

if (!postState.isStateValidatorsNodesPopulated()) {
this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postState.slot});
if (!postBlockState.isStateValidatorsNodesPopulated()) {
this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postBlockState.slot});
}

// Cache shufflings when crossing an epoch boundary
const parentEpoch = computeEpochAtSlot(parentBlockSlot);
if (parentEpoch < blockEpoch) {
this.shufflingCache.processState(postState);
this.shufflingCache.processState(postBlockState);
this.logger.verbose("Processed shuffling for next epoch", {parentEpoch, blockEpoch, slot: blockSlot});
}

if (blockSlot % SLOTS_PER_EPOCH === 0) {
// Cache state to preserve epoch transition work
const checkpointState = postState;
const checkpointState = postBlockState;
const cp = getCheckpointFromState(checkpointState);
this.regen.addCheckpointState(cp, checkpointState, payloadPresent);
// consumers should not mutate state ever
Expand Down Expand Up @@ -584,7 +585,7 @@ export async function importBlock(
this.validatorMonitor?.registerSyncAggregateInBlock(
blockEpoch,
(block as altair.SignedBeaconBlock).message.body.syncAggregate,
fullyVerifiedBlock.postState.currentSyncCommitteeIndexed.validatorIndices
fullyVerifiedBlock.postBlockState.currentSyncCommitteeIndexed.validatorIndices
);
}

Expand Down
35 changes: 23 additions & 12 deletions packages/beacon-node/src/chain/blocks/importExecutionPayload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {routes} from "@lodestar/api";
import {ForkName} from "@lodestar/params";
import {ExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice";
import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params";
import {getExecutionPayloadEnvelopeSignatureSet} from "@lodestar/state-transition";
import {byteArrayEquals, fromHex, toRootHex} from "@lodestar/utils";
import {ExecutionPayloadStatus} from "../../execution/index.js";
Expand Down Expand Up @@ -51,6 +52,19 @@ export class PayloadError extends Error {
}
}

function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExecutionStatus {
switch (status) {
case ExecutionPayloadStatus.VALID:
return ExecutionStatus.Valid;
// TODO GLOAS: Handle optimistic import for payload
case ExecutionPayloadStatus.SYNCING:
case ExecutionPayloadStatus.ACCEPTED:
return ExecutionStatus.Syncing;
default:
throw new Error(`Unexpected execution payload status for fork choice: ${status}`);
}
}

/**
* Import an execution payload envelope after all data is available.
*
Expand Down Expand Up @@ -161,12 +175,7 @@ export async function importExecutionPayload(

case ExecutionPayloadStatus.ACCEPTED:
case ExecutionPayloadStatus.SYNCING:
// TODO GLOAS: Handle optimistic import for payload - for now treat as error
throw new PayloadError({
code: PayloadErrorCode.EXECUTION_ENGINE_ERROR,
execStatus: execResult.status,
errorMessage: execResult.validationError ?? "EL syncing, payload not yet validated",
});
break;

case ExecutionPayloadStatus.INVALID_BLOCK_HASH:
case ExecutionPayloadStatus.ELERROR:
Expand Down Expand Up @@ -204,14 +213,16 @@ export async function importExecutionPayload(
blockRootHex,
payloadInput.getBlockHashHex(),
envelope.message.payload.blockNumber,
toRootHex(postPayloadStateRoot)
toRootHex(postPayloadStateRoot),
toForkChoiceExecutionStatus(execResult.status)
);

// 7. Cache payload state
// TODO GLOAS: Enable when PR #8868 merged (adds processPayloadState)
// this.regen.processPayloadState(postPayloadState);
// if epoch boundary also call
// this.regen.addCheckpointState(cp, checkpointState, true);
this.regen.processPayloadState(postPayloadState);
if (postPayloadState.slot % SLOTS_PER_EPOCH === 0) {
const {checkpoint} = postPayloadState.computeAnchorCheckpoint();
this.regen.addCheckpointState(checkpoint, postPayloadState, true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to use consistent names, eg. here it's called postPayloadState, while in other places we call it postEnvelopeState

}

// 8. Record metrics for payload envelope and column sources
this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source});
Expand Down
3 changes: 2 additions & 1 deletion packages/beacon-node/src/chain/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export async function processBlocks(
const fullyVerifiedBlocks = relevantBlocks.map(
(block, i): FullyVerifiedBlock => ({
blockInput: block,
postState: postStates[i],
postBlockState: postStates[i],
postEnvelopeState: null,
parentBlockSlot: parentSlots[i],
executionStatus: executionStatuses[i],
// start supporting optimistic syncing/processing
Expand Down
39 changes: 25 additions & 14 deletions packages/beacon-node/src/chain/blocks/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ChainForkConfig} from "@lodestar/config";
import {MaybeValidExecutionStatus} from "@lodestar/fork-choice";
import {BlockExecutionStatus, PayloadExecutionStatus} 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";
Expand Down Expand Up @@ -88,24 +88,35 @@ export type ImportBlockOpts = {
seenTimestampSec?: number;
};

/**
* A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import
*/
export type FullyVerifiedBlock = {
type FullyVerifiedBlockBase = {
blockInput: IBlockInput;
postState: IBeaconStateView;
postBlockState: IBeaconStateView;
parentBlockSlot: Slot;
proposerBalanceDelta: number;
/**
* If the execution payload couldnt be verified because of EL syncing status,
* used in optimistic sync or for merge block
*/
executionStatus: MaybeValidExecutionStatus;
dataAvailabilityStatus: DataAvailabilityStatus;
/**
* Pre-computed indexed attestations from signature verification to avoid duplicate work
*/
/** Pre-computed indexed attestations from signature verification to avoid duplicate work */
indexedAttestations: IndexedAttestation[];
/** Seen timestamp seconds */
seenTimestampSec: number;
};

/**
* A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import.
*
* Discriminated union on `postEnvelopeState`:
* - `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 &
(
| {
postEnvelopeState: null;
Comment thread
twoeths marked this conversation as resolved.
/** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync or for merge block */
executionStatus: BlockExecutionStatus;
}
| {
postEnvelopeState: IBeaconStateView;
executionStatus: PayloadExecutionStatus;
}
);
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {ChainForkConfig} from "@lodestar/config";
import {
BlockExecutionStatus,
ExecutionStatus,
IForkChoice,
LVHInvalidResponse,
LVHValidResponse,
MaybeValidExecutionStatus,
ProtoBlock,
} from "@lodestar/fork-choice";
import {ForkSeq} from "@lodestar/params";
Expand Down Expand Up @@ -33,7 +33,7 @@ type ExecAbortType = {blockIndex: number; execError: BlockError};
export type SegmentExecStatus =
| {
execAborted: null;
executionStatuses: MaybeValidExecutionStatus[];
executionStatuses: BlockExecutionStatus[];
executionTime: number;
}
| {execAborted: ExecAbortType; invalidSegmentLVH?: LVHInvalidResponse};
Expand Down Expand Up @@ -62,7 +62,7 @@ export async function verifyBlocksExecutionPayload(
signal: AbortSignal,
opts: BlockProcessOpts & ImportBlockOpts
): Promise<SegmentExecStatus> {
const executionStatuses: MaybeValidExecutionStatus[] = [];
const executionStatuses: BlockExecutionStatus[] = [];
const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
const lastBlock = blockInputs.at(-1);

Expand Down Expand Up @@ -103,7 +103,7 @@ export async function verifyBlocksExecutionPayload(
return getSegmentErrorResponse({verifyResponse, blockIndex}, parentBlock, blockInputs);
}

// If we are here then its because executionStatus is one of MaybeValidExecutionStatus
// If we are here then its because executionStatus is one of BlockExecutionStatus
const {executionStatus} = verifyResponse;
executionStatuses.push(executionStatus);
}
Expand Down
18 changes: 11 additions & 7 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ import {ForkChoiceMetrics} from "../metrics.js";
import {computeDeltas} from "../protoArray/computeDeltas.js";
import {ProtoArrayError, ProtoArrayErrorCode} from "../protoArray/errors.js";
import {
BlockExecutionStatus,
ExecutionStatus,
HEX_ZERO_HASH,
LVHExecResponse,
MaybeValidExecutionStatus,
NULL_VOTE_INDEX,
PayloadExecutionStatus,
PayloadStatus,
ProtoBlock,
ProtoNode,
Expand Down Expand Up @@ -585,7 +586,7 @@ export class ForkChoice implements IForkChoice {
state: IBeaconStateView,
blockDelaySec: number,
currentSlot: Slot,
executionStatus: MaybeValidExecutionStatus,
executionStatus: BlockExecutionStatus,
dataAvailabilityStatus: DataAvailabilityStatus
): ProtoBlock {
const {parentRoot, slot} = block;
Expand Down Expand Up @@ -977,15 +978,17 @@ export class ForkChoice implements IForkChoice {
blockRoot: RootHex,
executionPayloadBlockHash: RootHex,
executionPayloadNumber: number,
executionPayloadStateRoot: RootHex
executionPayloadStateRoot: RootHex,
executionStatus: PayloadExecutionStatus
): void {
this.protoArray.onExecutionPayload(
blockRoot,
this.fcStore.currentSlot,
executionPayloadBlockHash,
executionPayloadNumber,
executionPayloadStateRoot,
this.proposerBoostRoot
this.proposerBoostRoot,
executionStatus
);
}

Expand Down Expand Up @@ -1425,7 +1428,7 @@ export class ForkChoice implements IForkChoice {
return secFromSlot * 1000 <= proposerReorgCutoff;
}

private getPreMergeExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PreMerge {
private getPreMergeExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PreMerge {
if (executionStatus !== ExecutionStatus.PreMerge)
throw Error(`Invalid pre-merge execution status: expected: ${ExecutionStatus.PreMerge}, got ${executionStatus}`);
return executionStatus;
Expand All @@ -1440,7 +1443,7 @@ export class ForkChoice implements IForkChoice {
}

private getPreGloasExecStatus(
executionStatus: MaybeValidExecutionStatus
executionStatus: BlockExecutionStatus
): ExecutionStatus.Valid | ExecutionStatus.Syncing {
if (executionStatus === ExecutionStatus.PreMerge || executionStatus === ExecutionStatus.PayloadSeparated)
throw Error(
Expand All @@ -1449,7 +1452,7 @@ export class ForkChoice implements IForkChoice {
return executionStatus;
}

private getPostGloasExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PayloadSeparated {
private getPostGloasExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PayloadSeparated {
if (executionStatus !== ExecutionStatus.PayloadSeparated)
throw Error(
`Invalid post-gloas execution status: expected: ${ExecutionStatus.PayloadSeparated}, got ${executionStatus}`
Expand Down Expand Up @@ -1855,6 +1858,7 @@ export function getCommitteeFraction(
* Pre-Gloas: always FULL (payload embedded in block)
* Gloas: determined by state.execution_payload_availability
*
* @param config - The chain fork config to determine fork at checkpoint slot
* @param state - The state to check execution_payload_availability
* @param checkpointEpoch - The epoch of the checkpoint
*/
Expand Down
Loading
Loading