UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

208 lines • 13.4 kB
import { routes } from "@lodestar/api"; import { ForkSeq, SLOTS_PER_EPOCH, isForkPostElectra } from "@lodestar/params"; import { StateHashTreeRootSource, computeEpochAtSlot, computeTimeAtSlot, isExecutionStateType, } from "@lodestar/state-transition"; import { fromHex, isErrorAborted, sleep } from "@lodestar/utils"; import { GENESIS_SLOT, ZERO_HASH_HEX } from "../constants/constants.js"; import { ClockEvent } from "../util/clock.js"; import { isQueueErrorAborted } from "../util/queue/index.js"; import { ForkchoiceCaller } from "./forkChoice/index.js"; import { getPayloadAttributesForSSE, prepareExecutionPayload } from "./produceBlock/produceBlockBody.js"; import { RegenCaller } from "./regen/index.js"; /* With 12s slot times, this scheduler will run 4s before the start of each slot (`12 / 3 = 4`). */ export const SCHEDULER_LOOKAHEAD_FACTOR = 3; /* We don't want to do more epoch transition than this */ const PREPARE_EPOCH_LIMIT = 1; /** * At Bellatrix, if we are responsible for proposing in next slot, we want to prepare payload * 4s (1/3 slot) before the start of next slot * * For all forks, when clock is 1/3 slot before an epoch, we want to prepare for the next epoch * transition from our head so that: * + validators vote for block head on time through attestation * + validators propose blocks on time * + For Bellatrix, to compute proposers of next epoch so that we can prepare new payloads * */ export class PrepareNextSlotScheduler { constructor(chain, config, metrics, logger, signal) { this.chain = chain; this.config = config; this.metrics = metrics; this.logger = logger; this.signal = signal; /** * Use clockSlot instead of clockEpoch to schedule the task at more exact time. */ this.prepareForNextSlot = async (clockSlot) => { const prepareSlot = clockSlot + 1; const prepareEpoch = computeEpochAtSlot(prepareSlot); const nextEpoch = computeEpochAtSlot(clockSlot) + 1; const isEpochTransition = prepareEpoch === nextEpoch; const fork = this.config.getForkName(prepareSlot); // Early return if we are pre-genesis // or we are pre-bellatrix and this is not an epoch transition if (prepareSlot <= GENESIS_SLOT || (ForkSeq[fork] < ForkSeq.bellatrix && !isEpochTransition)) { return; } try { // At 1/3 slot time before the next slot, we either prepare payload or precompute // epoch transition const slotMs = this.config.SECONDS_PER_SLOT * 1000; await sleep(slotMs - slotMs / SCHEDULER_LOOKAHEAD_FACTOR, this.signal); // calling updateHead() here before we produce a block to reduce reorg possibility const { slot: headSlot, blockRoot: headRoot } = this.chain.recomputeForkChoiceHead(ForkchoiceCaller.prepareNextSlot); // PS: previously this was comparing slots, but that gave no leway on the skipped // slots on epoch bounday. Making it more fluid. if (prepareSlot - headSlot > PREPARE_EPOCH_LIMIT * SLOTS_PER_EPOCH) { this.metrics?.precomputeNextEpochTransition.count.inc({ result: "skip" }, 1); this.logger.debug("Skipping PrepareNextSlotScheduler - head slot is too behind current slot", { nextEpoch, headSlot, clockSlot, }); return; } this.logger.verbose("Running prepareForNextSlot", { nextEpoch, prepareSlot, headSlot, headRoot, isEpochTransition, }); const precomputeEpochTransitionTimer = isEpochTransition ? this.metrics?.precomputeNextEpochTransition.duration.startTimer() : null; // No need to wait for this or the clock drift // Pre Bellatrix: we only do precompute state transition for the last slot of epoch // For Bellatrix, we always do the `processSlots()` to prepare payload for the next slot const prepareState = await this.chain.regen.getBlockSlotState(headRoot, prepareSlot, // the slot 0 of next epoch will likely use this Previous Root Checkpoint state for state transition so we transfer cache here // the resulting state with cache will be cached in Checkpoint State Cache which is used for the upcoming block processing // for other slots dontTransferCached=true because we don't run state transition on this state // // Shuffling calculation will be done asynchronously when passing asyncShufflingCalculation=true. Shuffling will be queued in // beforeProcessEpoch and should theoretically be ready immediately after the synchronous epoch transition finished and the // event loop is free. In long periods of non-finality too many forks will cause the shufflingCache to throw an error for // too many queued shufflings so only run async during normal epoch transition. See issue ChainSafe/lodestar#7244 { dontTransferCache: !isEpochTransition, asyncShufflingCalculation: true }, RegenCaller.precomputeEpoch); if (isExecutionStateType(prepareState)) { const proposerIndex = prepareState.epochCtx.getBeaconProposer(prepareSlot); const feeRecipient = this.chain.beaconProposerCache.get(proposerIndex); let updatedPrepareState = prepareState; let updatedHeadRoot = headRoot; if (feeRecipient) { // If we are proposing next slot, we need to predict if we can proposer-boost-reorg or not const { slot: proposerHeadSlot, blockRoot: proposerHeadRoot } = this.chain.predictProposerHead(clockSlot); // If we predict we can reorg, update prepareState with proposer head block if (proposerHeadRoot !== headRoot || proposerHeadSlot !== headSlot) { this.logger.verbose("Weak head detected. May build on parent block instead", { proposerHeadSlot, proposerHeadRoot, headSlot, headRoot, }); this.metrics?.weakHeadDetected.inc(); updatedPrepareState = (await this.chain.regen.getBlockSlotState(proposerHeadRoot, prepareSlot, { dontTransferCache: !isEpochTransition }, RegenCaller.predictProposerHead)); updatedHeadRoot = proposerHeadRoot; } // Update the builder status, if enabled shoot an api call to check status this.chain.updateBuilderStatus(clockSlot); if (this.chain.executionBuilder?.status) { this.chain.executionBuilder.checkStatus().catch((e) => { this.logger.error("Builder disabled as the check status api failed", { prepareSlot }, e); }); } const preparationTime = computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000; this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime); const safeBlockHash = this.chain.forkChoice.getJustifiedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; const finalizedBlockHash = this.chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; // awaiting here instead of throwing an async call because there is no other task // left for scheduler and this gives nice sematics to catch and log errors in the // try/catch wrapper here. await prepareExecutionPayload(this.chain, this.logger, fork, // State is of execution type fromHex(updatedHeadRoot), safeBlockHash, finalizedBlockHash, updatedPrepareState, feeRecipient); this.logger.verbose("PrepareNextSlotScheduler prepared new payload", { prepareSlot, proposerIndex, feeRecipient, }); } this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition); // If emitPayloadAttributes is true emit a SSE payloadAttributes event if (this.chain.opts.emitPayloadAttributes === true) { const data = await getPayloadAttributesForSSE(fork, this.chain, { prepareState: updatedPrepareState, prepareSlot, parentBlockRoot: fromHex(headRoot), // The likely consumers of this API are builders and will anyway ignore the // feeRecipient, so just pass zero hash for now till a real use case arises feeRecipient: "0x0000000000000000000000000000000000000000000000000000000000000000", }); this.chain.emitter.emit(routes.events.EventType.payloadAttributes, { data, version: fork }); } } else { this.computeStateHashTreeRoot(prepareState, isEpochTransition); } // assuming there is no reorg, it caches the checkpoint state & helps avoid doing a full state transition in the next slot // + when gossip block comes, we need to validate and run state transition // + 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); const previousHits = this.chain.regen.updatePreComputedCheckpoint(headRoot, nextEpoch); if (previousHits === 0) { this.metrics?.precomputeNextEpochTransition.waste.inc(); } this.metrics?.precomputeNextEpochTransition.hits.set(previousHits ?? 0); // Check if we can stop polling eth1 data this.stopEth1Polling(); this.logger.verbose("Completed PrepareNextSlotScheduler epoch transition", { nextEpoch, headSlot, prepareSlot, previousHits, }); precomputeEpochTransitionTimer?.(); } } catch (e) { if (!isErrorAborted(e) && !isQueueErrorAborted(e)) { this.metrics?.precomputeNextEpochTransition.count.inc({ result: "error" }, 1); this.logger.error("Failed to run prepareForNextSlot", { nextEpoch, isEpochTransition, prepareSlot }, e); } } }; this.chain.clock.on(ClockEvent.slot, this.prepareForNextSlot); this.signal.addEventListener("abort", () => { this.chain.clock.off(ClockEvent.slot, this.prepareForNextSlot); }, { once: true }); } computeStateHashTreeRoot(state, isEpochTransition) { // cache HashObjects for faster hashTreeRoot() later, especially for computeNewStateRoot() if we need to produce a block at slot 0 of epoch // see https://github.com/ChainSafe/lodestar/issues/6194 const hashTreeRootTimer = this.metrics?.stateHashTreeRootTime.startTimer({ source: isEpochTransition ? StateHashTreeRootSource.prepareNextEpoch : StateHashTreeRootSource.prepareNextSlot, }); state.hashTreeRoot(); hashTreeRootTimer?.(); } /** * Stop eth1 data polling after eth1_deposit_index has reached deposit_requests_start_index in Electra as described in EIP-6110 */ stopEth1Polling() { // Only continue if eth1 is still polling and finalized checkpoint is in Electra. State regen is expensive if (this.chain.eth1.isPollingEth1Data()) { const finalizedCheckpoint = this.chain.forkChoice.getFinalizedCheckpoint(); const checkpointFork = this.config.getForkInfoAtEpoch(finalizedCheckpoint.epoch).name; if (isForkPostElectra(checkpointFork)) { const finalizedState = this.chain.getStateByCheckpoint(finalizedCheckpoint)?.state; if (finalizedState !== undefined && finalizedState.eth1DepositIndex === Number(finalizedState.depositRequestsStartIndex)) { // Signal eth1 to stop polling eth1Data this.chain.eth1.stopPollingEth1Data(); } } } } } //# sourceMappingURL=prepareNextSlot.js.map