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
11 changes: 6 additions & 5 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import {ChainEvent, CommonBlockBody} from "../../../chain/index.js";
import {PREPARE_NEXT_SLOT_BPS} from "../../../chain/prepareNextSlot.js";
import {BlockType, ProduceFullDeneb, ProduceFullGloas} from "../../../chain/produceBlock/index.js";
import {RegenCaller} from "../../../chain/regen/index.js";
import {CheckpointHexPayload} from "../../../chain/stateCache/types.js";
import {CheckpointHex} from "../../../chain/stateCache/types.js";
import {validateApiAggregateAndProof} from "../../../chain/validation/index.js";
import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof.js";
import {ZERO_HASH} from "../../../constants/index.js";
Expand Down Expand Up @@ -301,7 +301,7 @@ export function getValidatorApi(
* |
* prepareNextSlot (4s before next slot)
*/
async function waitForCheckpointState(cpHex: CheckpointHexPayload): Promise<IBeaconStateView | null> {
async function waitForCheckpointState(cpHex: CheckpointHex): Promise<IBeaconStateView | null> {
const cpState = chain.regen.getCheckpointStateSync(cpHex);
if (cpState) {
return cpState;
Expand Down Expand Up @@ -1113,7 +1113,6 @@ export function getValidatorApi(
const cpState = await waitForCheckpointState({
rootHex: head.blockRoot,
epoch,
payloadPresent: head.payloadStatus === PayloadStatus.FULL,
});
if (cpState) {
state = cpState;
Expand Down Expand Up @@ -1642,15 +1641,17 @@ export function getValidatorApi(
throw Error("Cached block production result is not full block");
}

const {executionPayload, executionRequests, payloadEnvelopeStateRoot} = produceResult as ProduceFullGloas;
const {executionPayload, executionRequests} = produceResult as ProduceFullGloas;

const envelope: gloas.ExecutionPayloadEnvelope = {
payload: executionPayload,
executionRequests: executionRequests,
builderIndex: BUILDER_INDEX_SELF_BUILD,
beaconBlockRoot,
slot,
stateRoot: payloadEnvelopeStateRoot,
// TODO GLOAS: stateRoot is no longer computed during block production.
// This field will be removed when we implement defer payload processing
stateRoot: ZERO_HASH,
Comment on lines +1652 to +1654
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Setting stateRoot to ZERO_HASH in the ExecutionPayloadEnvelope will cause validation failures during payload import on other nodes (or even the same node if re-imported). The importExecutionPayload function still performs a strict check comparing the envelope's stateRoot against the computed postPayloadStateRoot. If the intention is to defer payload processing and avoid computing the state root during production, the importer must also be updated to skip this verification for Gloas blocks, otherwise this change breaks Gloas block production and import.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Set execution payload envelope stateRoot correctly

getExecutionPayloadEnvelope() now returns stateRoot: ZERO_HASH, but payload import still enforces that envelope.stateRoot matches the computed post-payload state root (packages/beacon-node/src/chain/blocks/importExecutionPayload.ts, lines 215-219). In the standard post-gloas proposer flow, the validator signs exactly this envelope and submits it, so self-built payload envelopes will be rejected with a state-root mismatch instead of importing successfully.

Useful? React with 👍 / 👎.

};

logger.info("Produced execution payload envelope", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {AllocSource, BufferPool} from "../../../util/bufferPool.js";
import {getStateSlotFromBytes} from "../../../util/multifork.js";
import {IStateRegenerator} from "../../regen/interface.js";
import {serializeState} from "../../serializeState.js";
import {fcCheckpointToHexPayload} from "../../stateCache/persistentCheckpointsCache.js";
import {StateArchiveStrategy, StatesArchiveOpts} from "../interface.js";

/**
Expand Down Expand Up @@ -108,9 +107,8 @@ export class FrequencyStateArchiveStrategy implements StateArchiveStrategy {
async archiveState(finalized: CheckpointWithPayloadStatus, metrics?: Metrics | null): Promise<void> {
// starting from Mar 2024, the finalized state could be from disk or in memory
let timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
// Convert fork-choice checkpoint to beacon-node checkpoint with payloadPresent
const finalizedHexPayload = fcCheckpointToHexPayload(finalized);
const finalizedStateOrBytes = await this.regen.getCheckpointStateOrBytes(finalizedHexPayload);
const finalizedHex = {epoch: finalized.epoch, rootHex: finalized.rootHex};
const finalizedStateOrBytes = await this.regen.getCheckpointStateOrBytes(finalizedHex);
timer?.({step: FrequencyStateArchiveStep.GetFinalizedState});

const {rootHex} = finalized;
Expand Down
43 changes: 19 additions & 24 deletions packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
ForkChoiceErrorCode,
NotReorgedReason,
getSafeExecutionBlockHash,
isGloasBlock,
} from "@lodestar/fork-choice";
import {
ForkPostAltair,
Expand Down Expand Up @@ -87,7 +86,7 @@ export async function importBlock(
fullyVerifiedBlock: FullyVerifiedBlock,
opts: ImportBlockOpts
): Promise<void> {
const {blockInput, postBlockState, parentBlockSlot, executionStatus, dataAvailabilityStatus, indexedAttestations} =
const {blockInput, postState, parentBlockSlot, executionStatus, dataAvailabilityStatus, indexedAttestations} =
fullyVerifiedBlock;
const block = blockInput.getBlock();
const source = blockInput.getBlockSource();
Expand All @@ -99,7 +98,7 @@ export async function importBlock(
const blockEpoch = computeEpochAtSlot(blockSlot);
const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
const blockDelaySec =
fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postBlockState.genesisTime);
fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postState.genesisTime);
const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
const fork = this.config.getForkSeq(blockSlot);

Expand All @@ -122,10 +121,10 @@ export async function importBlock(
// 2. Import block to fork choice

// Should compute checkpoint balances before forkchoice.onBlock
this.checkpointBalancesCache.processState(blockRootHex, postBlockState);
this.checkpointBalancesCache.processState(blockRootHex, postState);
const blockSummary = this.forkChoice.onBlock(
block.message,
postBlockState,
postState,
blockDelaySec,
currentSlot,
fork >= ForkSeq.gloas ? ExecutionStatus.PayloadSeparated : executionStatus,
Expand All @@ -134,11 +133,7 @@ export async function importBlock(

// This adds the state necessary to process the next block
// Some block event handlers require state being in state cache so need to do this before emitting EventType.block
// Pre-Gloas: blockSummary.payloadStatus is always FULL, payloadPresent = true
// 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, postBlockState);
this.regen.processState(blockRootHex, postState);

// For Gloas blocks, create PayloadEnvelopeInput so it's available for later payload import
if (fork >= ForkSeq.gloas) {
Expand Down Expand Up @@ -191,7 +186,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(postBlockState);
const rootCache = new RootCache(postState);
const invalidAttestationErrorsByCode = new Map<string, {error: Error; count: number}>();

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

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

try {
this.emitter.emit(routes.events.EventType.head, {
Expand Down Expand Up @@ -390,10 +385,10 @@ export async function importBlock(
// we want to import block asap so do this in the next event loop
callInNextEventLoop(() => {
try {
if (isStatePostAltair(postBlockState)) {
if (isStatePostAltair(postState)) {
this.lightClientServer?.onImportBlockHead(
block.message as BeaconBlock<ForkPostAltair>,
postBlockState,
postState,
parentBlockSlot
);
}
Expand All @@ -415,11 +410,11 @@ export async function importBlock(
// and the block is weak and can potentially be reorged out.
let shouldOverrideFcu = false;

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

if (feeRecipient) {
Expand Down Expand Up @@ -499,22 +494,22 @@ export async function importBlock(
}
}

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

// Cache shufflings when crossing an epoch boundary
const parentEpoch = computeEpochAtSlot(parentBlockSlot);
if (parentEpoch < blockEpoch) {
this.shufflingCache.processState(postBlockState);
this.shufflingCache.processState(postState);
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 = postBlockState;
const checkpointState = postState;
const cp = getCheckpointFromState(checkpointState);
this.regen.addCheckpointState(cp, checkpointState, payloadPresent);
this.regen.addCheckpointState(cp, checkpointState);
// consumers should not mutate state ever
this.emitter.emit(ChainEvent.checkpoint, cp, checkpointState);

Expand Down Expand Up @@ -602,11 +597,11 @@ export async function importBlock(
this.metrics?.parentBlockDistance.observe(blockSlot - parentBlockSlot);
this.metrics?.proposerBalanceDeltaAny.observe(fullyVerifiedBlock.proposerBalanceDelta);
this.validatorMonitor?.registerImportedBlock(block.message, fullyVerifiedBlock);
if (isStatePostAltair(fullyVerifiedBlock.postBlockState)) {
if (isStatePostAltair(fullyVerifiedBlock.postState)) {
this.validatorMonitor?.registerSyncAggregateInBlock(
blockEpoch,
(block as altair.SignedBeaconBlock).message.body.syncAggregate,
fullyVerifiedBlock.postBlockState.currentSyncCommitteeIndexed.validatorIndices
fullyVerifiedBlock.postState.currentSyncCommitteeIndexed.validatorIndices
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,10 @@ export async function importExecutionPayload(
);

// 8. Cache payload state
this.regen.processPayloadState(postPayloadState);
this.regen.processState(blockRootHex, postPayloadState);
if (postPayloadState.slot % SLOTS_PER_EPOCH === 0) {
const {checkpoint} = postPayloadState.computeAnchorCheckpoint();
this.regen.addCheckpointState(checkpoint, postPayloadState, true);
this.regen.addCheckpointState(checkpoint, postPayloadState);
}

// 9. Record metrics for payload envelope and column sources
Expand Down
2 changes: 1 addition & 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,7 @@ export async function processBlocks(
const fullyVerifiedBlocks = relevantBlocks.map(
(block, i): FullyVerifiedBlock => ({
blockInput: block,
postBlockState: postStates[i],
postState: postStates[i],
postPayloadState: null,
parentBlockSlot: parentSlots[i],
executionStatus: executionStatuses[i],
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/blocks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export type ImportBlockOpts = {

type FullyVerifiedBlockBase = {
blockInput: IBlockInput;
postBlockState: IBeaconStateView;
postState: IBeaconStateView;
parentBlockSlot: Slot;
proposerBalanceDelta: number;
dataAvailabilityStatus: DataAvailabilityStatus;
Expand Down
45 changes: 13 additions & 32 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {BeaconConfig} from "@lodestar/config";
import {CheckpointWithPayloadStatus, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice";
import {LoggerNode} from "@lodestar/logger/node";
import {
BUILDER_INDEX_SELF_BUILD,
EFFECTIVE_BALANCE_INCREMENT,
type ForkPostFulu,
type ForkPostGloas,
Expand All @@ -24,7 +23,6 @@ import {
getEffectiveBalancesFromStateBytes,
isStatePostAltair,
isStatePostElectra,
isStatePostGloas,
} from "@lodestar/state-transition";
import {
BeaconBlock,
Expand Down Expand Up @@ -93,8 +91,8 @@ import {
} from "./opPools/index.js";
import {IChainOptions} from "./options.js";
import {PrepareNextSlotScheduler} from "./prepareNextSlot.js";
import {computeNewStateRoot, computePayloadEnvelopeStateRoot} from "./produceBlock/computeNewStateRoot.js";
import {AssembledBlockType, BlockType, ProduceFullGloas, ProduceResult} from "./produceBlock/index.js";
import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js";
import {AssembledBlockType, BlockType, ProduceResult} from "./produceBlock/index.js";
import {BlockAttributes, produceBlockBody, produceCommonBlockBody} from "./produceBlock/produceBlockBody.js";
import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js";
import {ReprocessController} from "./reprocess.js";
Expand All @@ -118,7 +116,7 @@ import {DbCPStateDatastore, checkpointToDatastoreKey} from "./stateCache/datasto
import {FileCPStateDatastore} from "./stateCache/datastore/file.js";
import {CPStateDatastore} from "./stateCache/datastore/types.js";
import {FIFOBlockStateCache} from "./stateCache/fifoBlockStateCache.js";
import {PersistentCheckpointStateCache, fcCheckpointToHexPayload} from "./stateCache/persistentCheckpointsCache.js";
import {PersistentCheckpointStateCache} from "./stateCache/persistentCheckpointsCache.js";
import {CheckpointStateCache} from "./stateCache/types.js";
import {ValidatorMonitor} from "./validatorMonitor.js";

Expand Down Expand Up @@ -685,8 +683,8 @@ export class BeaconChain implements IBeaconChain {
checkpoint: CheckpointWithPayloadStatus
): {state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null {
// finalized or justified checkpoint states maynot be available with PersistentCheckpointStateCache, use getCheckpointStateOrBytes() api to get Uint8Array
const checkpointHexPayload = fcCheckpointToHexPayload(checkpoint);
const cachedStateCtx = this.regen.getCheckpointStateSync(checkpointHexPayload);
const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex};
const cachedStateCtx = this.regen.getCheckpointStateSync(checkpointHex);
if (cachedStateCtx) {
const block = this.forkChoice.getBlockDefaultStatus(
ssz.phase0.BeaconBlockHeader.hashTreeRoot(cachedStateCtx.latestBlockHeader)
Expand All @@ -705,8 +703,8 @@ export class BeaconChain implements IBeaconChain {
async getStateOrBytesByCheckpoint(
checkpoint: CheckpointWithPayloadStatus
): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> {
const checkpointHexPayload = fcCheckpointToHexPayload(checkpoint);
const cachedStateCtx = await this.regen.getCheckpointStateOrBytes(checkpointHexPayload);
const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex};
const cachedStateCtx = await this.regen.getCheckpointStateOrBytes(checkpointHex);
if (cachedStateCtx) {
const block = this.forkChoice.getBlockDefaultStatus(checkpoint.root);
const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
Expand Down Expand Up @@ -1062,7 +1060,7 @@ export class BeaconChain implements IBeaconChain {
body,
} as AssembledBlockType<T>;

const {newStateRoot, proposerReward, postBlockState} = computeNewStateRoot(this.metrics, state, block);
const {newStateRoot, proposerReward} = computeNewStateRoot(this.metrics, state, block);
block.stateRoot = newStateRoot;
const blockRoot =
produceResult.type === BlockType.Full
Expand All @@ -1071,26 +1069,9 @@ export class BeaconChain implements IBeaconChain {
const blockRootHex = toRootHex(blockRoot);

const fork = this.config.getForkName(slot);
if (isForkPostGloas(fork)) {
// TODO GLOAS: we should retire BlockType post-gloas, may need a new enum for self vs non-self built
if (produceResult.type !== BlockType.Full) {
throw Error(`Unexpected block type=${produceResult.type} for post-gloas fork=${fork}`);
}

const gloasResult = produceResult as ProduceFullGloas;
const envelope: gloas.ExecutionPayloadEnvelope = {
payload: gloasResult.executionPayload,
executionRequests: gloasResult.executionRequests,
builderIndex: BUILDER_INDEX_SELF_BUILD,
beaconBlockRoot: blockRoot,
slot,
stateRoot: ZERO_HASH,
};
if (!isStatePostGloas(postBlockState)) {
throw Error(`Expected gloas+ post-state for execution payload envelope, got fork=${postBlockState.forkName}`);
}
const payloadEnvelopeStateRoot = computePayloadEnvelopeStateRoot(this.metrics, postBlockState, envelope);
gloasResult.payloadEnvelopeStateRoot = payloadEnvelopeStateRoot;
// TODO GLOAS: we should retire BlockType post-gloas, may need a new enum for self vs non-self built
if (isForkPostGloas(fork) && produceResult.type !== BlockType.Full) {
throw Error(`Unexpected block type=${produceResult.type} for post-gloas fork=${fork}`);
}

// Track the produced block for consensus broadcast validations, later validation, etc.
Expand Down Expand Up @@ -1338,8 +1319,8 @@ export class BeaconChain implements IBeaconChain {
checkpoint: CheckpointWithPayloadStatus,
blockState: IBeaconStateView
): {state: IBeaconStateView; stateId: string; shouldWarn: boolean} {
const checkpointHexPayload = fcCheckpointToHexPayload(checkpoint);
const state = this.regen.getCheckpointStateSync(checkpointHexPayload);
const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex};
const state = this.regen.getCheckpointStateSync(checkpointHex);
if (state) {
return {state, stateId: "checkpoint_state", shouldWarn: false};
}
Expand Down
8 changes: 2 additions & 6 deletions packages/beacon-node/src/chain/prepareNextSlot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {routes} from "@lodestar/api";
import {ChainForkConfig} from "@lodestar/config";
import {PayloadStatus, getSafeExecutionBlockHash} from "@lodestar/fork-choice";
import {getSafeExecutionBlockHash} from "@lodestar/fork-choice";
import {ForkPostBellatrix, ForkSeq, SLOTS_PER_EPOCH, isForkPostBellatrix} from "@lodestar/params";
import {
IBeaconStateView,
Expand Down Expand Up @@ -217,11 +217,7 @@ export class PrepareNextSlotScheduler {
// + if next slot is a skipped slot, it'd help getting target checkpoint state faster to validate attestations
if (isEpochTransition) {
this.metrics?.precomputeNextEpochTransition.count.inc({result: "success"}, 1);
// Determine payloadPresent from head block's payload status
// Pre-Gloas: payloadStatus is always FULL → payloadPresent = true
// Post-Gloas: FULL → true, EMPTY → false, PENDING → false (conservative, treat as block state)
const payloadPresent = headBlock.payloadStatus === PayloadStatus.FULL;
const previousHits = this.chain.regen.updatePreComputedCheckpoint(headRoot, nextEpoch, payloadPresent);
const previousHits = this.chain.regen.updatePreComputedCheckpoint(headRoot, nextEpoch);
if (previousHits === 0) {
this.metrics?.precomputeNextEpochTransition.waste.inc();
}
Expand Down
Loading
Loading