UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

962 lines 66.4 kB
import path from "node:path"; import { UpdateHeadOpt } from "@lodestar/fork-choice"; import { EFFECTIVE_BALANCE_INCREMENT, GENESIS_SLOT, SLOTS_PER_EPOCH, isForkPostGloas, } from "@lodestar/params"; import { computeEndSlotAtEpoch, computeEpochAtSlot, computeStartSlotAtEpoch, getEffectiveBalancesFromStateBytes, isStatePostAltair, isStatePostElectra, isStatePostGloas, } from "@lodestar/state-transition"; import { isBlindedBeaconBlock, ssz, sszTypesFor, } from "@lodestar/types"; import { fromHex, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toRootHex } from "@lodestar/utils"; import { GENESIS_EPOCH, ZERO_HASH } from "../constants/index.js"; import { BLOB_SIDECARS_IN_WRAPPER_INDEX } from "../db/repositories/blobSidecars.js"; import { BuilderStatus } from "../execution/builder/http.js"; import { computeNodeIdFromPrivateKey } from "../network/subnets/interface.js"; import { BufferPool } from "../util/bufferPool.js"; import { Clock, ClockEvent } from "../util/clock.js"; import { CustodyConfig, getValidatorsCustodyRequirement } from "../util/dataColumns.js"; import { callInNextEventLoop } from "../util/eventLoop.js"; import { ensureDir, writeIfNotExist } from "../util/file.js"; import { isOptimisticBlock } from "../util/forkChoice.js"; import { JobItemQueue } from "../util/queue/itemQueue.js"; import { SerializedCache } from "../util/serializedCache.js"; import { getSlotFromSignedBeaconBlockSerialized } from "../util/sszBytes.js"; import { ArchiveStore } from "./archiveStore/archiveStore.js"; import { CheckpointBalancesCache } from "./balancesCache.js"; import { BeaconProposerCache } from "./beaconProposerCache.js"; import { isBlockInputBlobs, isBlockInputColumns } from "./blocks/blockInput/index.js"; import { BlockProcessor } from "./blocks/index.js"; import { PayloadEnvelopeProcessor } from "./blocks/payloadEnvelopeProcessor.js"; import { persistBlockInput } from "./blocks/writeBlockInputToDb.js"; import { persistPayloadEnvelopeInput } from "./blocks/writePayloadEnvelopeInputToDb.js"; import { BlsMultiThreadWorkerPool, BlsSingleThreadVerifier } from "./bls/index.js"; import { ColumnReconstructionTracker } from "./ColumnReconstructionTracker.js"; import { ChainEvent, ChainEventEmitter } from "./emitter.js"; import { initializeForkChoice } from "./forkChoice/index.js"; import { GetBlobsTracker } from "./GetBlobsTracker.js"; import { FindHeadFnName } from "./interface.js"; import { LightClientServer } from "./lightClient/index.js"; import { AggregatedAttestationPool, AttestationPool, ExecutionPayloadBidPool, OpPool, PayloadAttestationPool, SyncCommitteeMessagePool, SyncContributionAndProofPool, } from "./opPools/index.js"; import { PrepareNextSlotScheduler } from "./prepareNextSlot.js"; import { computeNewStateRoot } from "./produceBlock/computeNewStateRoot.js"; import { BlockType } from "./produceBlock/index.js"; import { produceBlockBody, produceCommonBlockBody } from "./produceBlock/produceBlockBody.js"; import { QueuedStateRegenerator, RegenCaller } from "./regen/index.js"; import { ReprocessController } from "./reprocess.js"; import { SeenAggregators, SeenAttesters, SeenBlockProposers, SeenContributionAndProof, SeenExecutionPayloadBids, SeenPayloadAttesters, SeenPayloadEnvelopeInput, SeenProposerPreferences, SeenSyncCommitteeMessages, } from "./seenCache/index.js"; import { SeenAggregatedAttestations } from "./seenCache/seenAggregateAndProof.js"; import { SeenAttestationDatas } from "./seenCache/seenAttestationData.js"; import { SeenBlockAttesters } from "./seenCache/seenBlockAttesters.js"; import { SeenBlockInput } from "./seenCache/seenGossipBlockInput.js"; import { ShufflingCache } from "./shufflingCache.js"; import { DbCPStateDatastore, checkpointToDatastoreKey } from "./stateCache/datastore/db.js"; import { FileCPStateDatastore } from "./stateCache/datastore/file.js"; import { FIFOBlockStateCache } from "./stateCache/fifoBlockStateCache.js"; import { PersistentCheckpointStateCache } from "./stateCache/persistentCheckpointsCache.js"; /** * The maximum number of cached produced results to keep in memory. * * Arbitrary constant. Blobs and payloads should be consumed immediately in the same slot * they are produced. A value of 1 would probably be sufficient. However it's sensible to * allow some margin if the node overloads. */ const DEFAULT_MAX_CACHED_PRODUCED_RESULTS = 4; /** * The maximum number of pending unfinalized block writes to the database before backpressure is applied. * Write queue entries hold references to block inputs, keeping them in memory even after cache eviction. * This is especially important for supernodes which store all 128 columns per block — each pending * write can hold significant memory. Keep moderate to avoid OOM during sync. */ const DEFAULT_MAX_PENDING_UNFINALIZED_BLOCK_WRITES = 16; /** * The maximum number of pending unfinalized payload envelope writes to the database before backpressure is applied. * Payload envelope write queue entries hold references to payload inputs (including columns), * keeping them in memory. Keep moderate to avoid OOM during sync. */ const DEFAULT_MAX_PENDING_UNFINALIZED_PAYLOAD_ENVELOPE_WRITES = 16; export class BeaconChain { genesisTime; genesisValidatorsRoot; executionEngine; executionBuilder; // Expose config for convenience in modularized functions config; custodyConfig; logger; metrics; validatorMonitor; bufferPool; anchorStateLatestBlockSlot; bls; forkChoice; clock; emitter; regen; lightClientServer; reprocessController; archiveStore; unfinalizedBlockWrites; unfinalizedPayloadEnvelopeWrites; // Ops pool attestationPool; aggregatedAttestationPool; syncCommitteeMessagePool; syncContributionAndProofPool; executionPayloadBidPool; payloadAttestationPool; opPool; // Gossip seen cache seenAttesters = new SeenAttesters(); seenAggregators = new SeenAggregators(); seenPayloadAttesters = new SeenPayloadAttesters(); seenAggregatedAttestations; seenExecutionPayloadBids = new SeenExecutionPayloadBids(); seenProposerPreferences = new SeenProposerPreferences(); seenBlockProposers = new SeenBlockProposers(); seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); seenContributionAndProof; seenAttestationDatas; seenBlockInputCache; seenPayloadEnvelopeInputCache; // Seen cache for liveness checks seenBlockAttesters = new SeenBlockAttesters(); // Global state caches pubkeyCache; beaconProposerCache; checkpointBalancesCache; shufflingCache; /** * Cache produced results (ExecutionPayload, DA Data) from the local execution so that we can send * and get signed/published blinded versions which beacon node can * assemble into full blocks before publishing to the network. */ blockProductionCache = new Map(); blacklistedBlocks; serializedCache; getBlobsTracker; columnReconstructionTracker; opts; blockProcessor; payloadEnvelopeProcessor; db; // this is only available if nHistoricalStates is enabled cpStateDatastore; abortController = new AbortController(); processShutdownCallback; _earliestAvailableSlot; get earliestAvailableSlot() { return this._earliestAvailableSlot; } set earliestAvailableSlot(slot) { if (this._earliestAvailableSlot !== slot) { this._earliestAvailableSlot = slot; this.emitter.emit(ChainEvent.updateStatus); } } constructor(opts, { privateKey, config, pubkeyCache, db, dbName, dataDir, logger, processShutdownCallback, clock, metrics, validatorMonitor, anchorState, isAnchorStateFinalized, executionEngine, executionBuilder, }) { this.opts = opts; this.config = config; this.db = db; this.logger = logger; this.processShutdownCallback = processShutdownCallback; this.metrics = metrics; this.validatorMonitor = validatorMonitor; this.genesisTime = anchorState.genesisTime; this.anchorStateLatestBlockSlot = anchorState.latestBlockHeader.slot; this.genesisValidatorsRoot = anchorState.genesisValidatorsRoot; this.executionEngine = executionEngine; this.executionBuilder = executionBuilder; const signal = this.abortController.signal; const emitter = new ChainEventEmitter(); // by default, verify signatures on both main threads and worker threads const bls = opts.blsVerifyAllMainThread ? new BlsSingleThreadVerifier({ metrics, pubkeyCache }) : new BlsMultiThreadWorkerPool(opts, { logger, metrics, pubkeyCache }); if (!clock) clock = new Clock({ config, genesisTime: this.genesisTime, signal }); this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null])); this.attestationPool = new AttestationPool(config, clock, this.opts?.preaggregateSlotDistance, metrics); this.aggregatedAttestationPool = new AggregatedAttestationPool(this.config, metrics); this.syncCommitteeMessagePool = new SyncCommitteeMessagePool(config, clock, this.opts?.preaggregateSlotDistance); this.syncContributionAndProofPool = new SyncContributionAndProofPool(config, clock, metrics, logger); this.executionPayloadBidPool = new ExecutionPayloadBidPool(); this.payloadAttestationPool = new PayloadAttestationPool(config, clock, metrics); this.opPool = new OpPool(config); this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); this.seenContributionAndProof = new SeenContributionAndProof(metrics); this.seenAttestationDatas = new SeenAttestationDatas(metrics, this.opts?.attDataCacheSlotDistance); const nodeId = computeNodeIdFromPrivateKey(privateKey); const initialCustodyGroupCount = opts.initialCustodyGroupCount ?? config.CUSTODY_REQUIREMENT; this.metrics?.peerDas.custodyGroupCount.set(initialCustodyGroupCount); // TODO: backfill not implemented yet this.metrics?.peerDas.custodyGroupsBackfilled.set(0); this.custodyConfig = new CustodyConfig({ nodeId, config, initialCustodyGroupCount, }); this.beaconProposerCache = new BeaconProposerCache(opts); this.checkpointBalancesCache = new CheckpointBalancesCache(); this.serializedCache = new SerializedCache(); this.seenBlockInputCache = new SeenBlockInput({ config, custodyConfig: this.custodyConfig, clock, chainEvents: emitter, signal, serializedCache: this.serializedCache, metrics, logger, }); this._earliestAvailableSlot = anchorState.slot; this.shufflingCache = new ShufflingCache(metrics, logger, this.opts, [ { shuffling: anchorState.getPreviousShuffling(), decisionRoot: anchorState.previousDecisionRoot, }, { shuffling: anchorState.getCurrentShuffling(), decisionRoot: anchorState.currentDecisionRoot, }, { shuffling: anchorState.getNextShuffling(), decisionRoot: anchorState.nextDecisionRoot, }, ]); // Global cache of validators pubkey/index mapping this.pubkeyCache = pubkeyCache; const fileDataStore = opts.nHistoricalStatesFileDataStore ?? true; const blockStateCache = new FIFOBlockStateCache(this.opts, { metrics }); this.bufferPool = new BufferPool(anchorState.serializedSize(), metrics); this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(this.db); const checkpointStateCache = new PersistentCheckpointStateCache({ config, metrics, logger, clock, blockStateCache, bufferPool: this.bufferPool, datastore: this.cpStateDatastore, }, this.opts); const { checkpoint } = anchorState.computeAnchorCheckpoint(); blockStateCache.add(anchorState); blockStateCache.setHeadState(anchorState); checkpointStateCache.add(checkpoint, anchorState); const forkChoice = initializeForkChoice(config, emitter, clock.currentSlot, anchorState, isAnchorStateFinalized, opts, this.justifiedBalancesGetter.bind(this), metrics, logger); const regen = new QueuedStateRegenerator({ config, forkChoice, blockStateCache, checkpointStateCache, seenBlockInputCache: this.seenBlockInputCache, db, metrics, validatorMonitor, logger, emitter, signal, }); if (!opts.disableLightClientServer) { this.lightClientServer = new LightClientServer(opts, { config, clock, db, metrics, emitter, logger, signal }); } this.reprocessController = new ReprocessController(this.metrics); this.blockProcessor = new BlockProcessor(this, metrics, opts, signal); this.payloadEnvelopeProcessor = new PayloadEnvelopeProcessor(this, metrics, signal); this.forkChoice = forkChoice; this.seenPayloadEnvelopeInputCache = new SeenPayloadEnvelopeInput({ config, clock, forkChoice, chainEvents: emitter, signal, serializedCache: this.serializedCache, metrics, logger, }); const anchorBlockSlot = anchorState.latestBlockHeader.slot; if (isStatePostGloas(anchorState) && anchorBlockSlot > 0) { const anchorBid = anchorState.latestExecutionPayloadBid; this.seenPayloadEnvelopeInputCache.addFromBid({ blockRootHex: toRootHex(checkpoint.root), slot: anchorBlockSlot, forkName: anchorState.forkName, proposerIndex: anchorState.latestBlockHeader.proposerIndex, bid: anchorBid, sampledColumns: this.custodyConfig.sampledColumns, custodyColumns: this.custodyConfig.custodyColumns, timeCreatedSec: Math.floor(Date.now() / 1000), }); } this.clock = clock; this.regen = regen; this.bls = bls; this.emitter = emitter; this.getBlobsTracker = new GetBlobsTracker({ logger, executionEngine: this.executionEngine, emitter, metrics, config, }); this.columnReconstructionTracker = new ColumnReconstructionTracker({ logger, emitter, metrics, config, }); this.archiveStore = new ArchiveStore({ db, chain: this, logger: logger, metrics }, { ...opts, dbName, anchorState: { finalizedCheckpoint: anchorState.finalizedCheckpoint } }, signal); this.unfinalizedBlockWrites = new JobItemQueue(persistBlockInput.bind(this), { maxLength: DEFAULT_MAX_PENDING_UNFINALIZED_BLOCK_WRITES, signal, }, metrics?.unfinalizedBlockWritesQueue); this.unfinalizedPayloadEnvelopeWrites = new JobItemQueue(persistPayloadEnvelopeInput.bind(this), { maxLength: DEFAULT_MAX_PENDING_UNFINALIZED_PAYLOAD_ENVELOPE_WRITES, signal, }, metrics?.unfinalizedPayloadEnvelopeWritesQueue); // always run PrepareNextSlotScheduler except for fork_choice spec tests if (!opts?.disablePrepareNextSlot) { new PrepareNextSlotScheduler(this, this.config, metrics, this.logger, signal); } if (metrics) { metrics.clockSlot.addCollect(() => this.onScrapeMetrics(metrics)); } // Event handlers. emitter is created internally and dropped on close(). Not need to .removeListener() clock.addListener(ClockEvent.slot, this.onClockSlot.bind(this)); clock.addListener(ClockEvent.epoch, this.onClockEpoch.bind(this)); emitter.addListener(ChainEvent.forkChoiceFinalized, this.onForkChoiceFinalized.bind(this)); emitter.addListener(ChainEvent.forkChoiceJustified, this.onForkChoiceJustified.bind(this)); emitter.addListener(ChainEvent.checkpoint, this.onCheckpoint.bind(this)); } async init() { await this.archiveStore.init(); await this.loadFromDisk(); } async close() { await this.archiveStore.close(); await this.bls.close(); // Since we don't persist unfinalized fork-choice, // we can abort any ongoing unfinalized block writes. // TODO: persist fork choice to disk and allow unfinalized block writes to complete. this.unfinalizedBlockWrites.dropAllJobs(); this.unfinalizedPayloadEnvelopeWrites.dropAllJobs(); this.abortController.abort(); } seenBlock(blockRoot) { return this.seenBlockInputCache.hasBlock(blockRoot) || this.forkChoice.hasBlockHexUnsafe(blockRoot); } seenPayloadEnvelope(blockRoot) { return this.seenPayloadEnvelopeInputCache.hasPayload(blockRoot) || this.forkChoice.hasPayloadHexUnsafe(blockRoot); } regenCanAcceptWork() { return this.regen.canAcceptWork(); } blsThreadPoolCanAcceptWork() { return this.bls.canAcceptWork(); } validatorSeenAtEpoch(index, epoch) { // Caller must check that epoch is not older that current epoch - 1 // else the caches for that epoch may already be pruned. return ( // Dedicated cache for liveness checks, registers attesters seen through blocks. // Note: this check should be cheaper + overlap with counting participants of aggregates from gossip. this.seenBlockAttesters.isKnown(epoch, index) || // // Re-use gossip caches. Populated on validation of gossip + API messages // seenAttesters = single signer of unaggregated attestations this.seenAttesters.isKnown(epoch, index) || // seenAggregators = single aggregator index, not participants of the aggregate this.seenAggregators.isKnown(epoch, index) || // seenPayloadAttesters = single signer of payload attestation message this.seenPayloadAttesters.isKnown(epoch, index) || // seenBlockProposers = single block proposer this.seenBlockProposers.seenAtEpoch(epoch, index)); } /** Populate in-memory caches with persisted data. Call at least once on startup */ async loadFromDisk() { await this.regen.init(); await this.opPool.fromPersisted(this.db); } /** Persist in-memory data to the DB. Call at least once before stopping the process */ async persistToDisk() { await this.archiveStore.persistToDisk(); await this.opPool.toPersisted(this.db); } getHeadState() { // head state should always exist const head = this.forkChoice.getHead(); const headState = this.regen.getClosestHeadState(head); if (!headState) { throw Error(`headState does not exist for head root=${head.blockRoot} slot=${head.slot}`); } return headState; } async getHeadStateAtCurrentEpoch(regenCaller) { return this.getHeadStateAtEpoch(this.clock.currentEpoch, regenCaller); } async getHeadStateAtEpoch(epoch, regenCaller) { // using getHeadState() means we'll use checkpointStateCache if it's available const headState = this.getHeadState(); // head state is in the same epoch, or we pulled up head state already from past epoch if (epoch <= computeEpochAtSlot(headState.slot)) { // should go to this most of the time return headState; } // only use regen queue if necessary, it'll cache in checkpointStateCache if regen gets through epoch transition const head = this.forkChoice.getHead(); const startSlot = computeStartSlotAtEpoch(epoch); return this.regen.getBlockSlotState(head, startSlot, { dontTransferCache: true }, regenCaller); } async getStateBySlot(slot, opts) { const finalizedBlock = this.forkChoice.getFinalizedBlock(); if (slot < finalizedBlock.slot) { // request for finalized state not supported in this API // fall back to caller to look in db or getHistoricalStateBySlot return null; } if (opts?.allowRegen) { // Find closest canonical block to slot, then trigger regen const block = this.forkChoice.getCanonicalBlockClosestLteSlot(slot) ?? finalizedBlock; const state = await this.regen.getBlockSlotState(block, slot, { dontTransferCache: true }, RegenCaller.restApi); return { state, executionOptimistic: isOptimisticBlock(block), finalized: slot === finalizedBlock.slot && finalizedBlock.slot !== GENESIS_SLOT, }; } // Just check if state is already in the cache. If it's not dialed to the correct slot, // do not bother in advancing the state. restApiCanTriggerRegen == false means do no work const block = this.forkChoice.getCanonicalBlockAtSlot(slot); if (!block) { return null; } const state = this.regen.getStateSync(block.stateRoot); return (state && { state, executionOptimistic: isOptimisticBlock(block), finalized: slot === finalizedBlock.slot && finalizedBlock.slot !== GENESIS_SLOT, }); } async getHistoricalStateBySlot(slot) { if (!this.opts.serveHistoricalState) { throw Error("Historical state regen is not enabled, set --serveHistoricalState to fetch this data"); } return this.archiveStore.getHistoricalStateBySlot(slot); } async getStateByStateRoot(stateRoot, opts) { if (opts?.allowRegen) { const state = await this.regen.getState(stateRoot, RegenCaller.restApi); const block = this.forkChoice.getBlockDefaultStatus(ssz.phase0.BeaconBlockHeader.hashTreeRoot(state.latestBlockHeader)); const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; return { state, executionOptimistic: block != null && isOptimisticBlock(block), finalized: state.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, }; } // TODO: This can only fulfill requests for a very narrow set of roots. // - very recent states that happen to be in the cache // - 1 every 100s of states that are persisted in the archive state // TODO: This is very inneficient for debug requests of serialized content, since it deserializes to serialize again const cachedStateCtx = this.regen.getStateSync(stateRoot); if (cachedStateCtx) { const block = this.forkChoice.getBlockDefaultStatus(ssz.phase0.BeaconBlockHeader.hashTreeRoot(cachedStateCtx.latestBlockHeader)); const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; return { state: cachedStateCtx, executionOptimistic: block != null && isOptimisticBlock(block), finalized: cachedStateCtx.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, }; } // this is mostly useful for a node with `--chain.archiveStateEpochFrequency 1` const data = await this.db.stateArchive.getBinaryByRoot(fromHex(stateRoot)); return data && { state: data, executionOptimistic: false, finalized: true }; } async getPersistedCheckpointState(checkpoint) { if (!this.cpStateDatastore) { throw new Error("n-historical-state flag is not enabled"); } if (checkpoint == null) { // return the last safe checkpoint state by default return this.cpStateDatastore.readLatestSafe(); } // TODO GLOAS: Need to revisit the design of this api. Currently we just retrieve FULL state of the checkpoint for backwards compatibility. // because pre-gloas we always store FULL checkpoint state. const persistedKey = checkpointToDatastoreKey(checkpoint); return this.cpStateDatastore.read(persistedKey); } getStateByCheckpoint(checkpoint) { // finalized or justified checkpoint states maynot be available with PersistentCheckpointStateCache, use getCheckpointStateOrBytes() api to get Uint8Array 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)); const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; return { state: cachedStateCtx, executionOptimistic: block != null && isOptimisticBlock(block), finalized: cachedStateCtx.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, }; } return null; } async getStateOrBytesByCheckpoint(checkpoint) { 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; return { state: cachedStateCtx, executionOptimistic: block != null && isOptimisticBlock(block), finalized: checkpoint.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, }; } return null; } async getCanonicalBlockAtSlot(slot) { const finalizedBlock = this.forkChoice.getFinalizedBlock(); if (slot > finalizedBlock.slot) { // Unfinalized slot, attempt to find in fork-choice const block = this.forkChoice.getCanonicalBlockAtSlot(slot); if (block) { // Block found in fork-choice. // It may be in the block input cache, awaiting full DA reconstruction, check there first // Otherwise (most likely), check the hot db const blockInput = this.seenBlockInputCache.get(block.blockRoot); if (blockInput?.hasBlock()) { return { block: blockInput.getBlock(), executionOptimistic: isOptimisticBlock(block), finalized: false }; } const data = await this.db.block.get(fromHex(block.blockRoot)); if (data) { return { block: data, executionOptimistic: isOptimisticBlock(block), finalized: false }; } } // A non-finalized slot expected to be found in the hot db, could be archived during // this function runtime, so if not found in the hot db, fallback to the cold db // TODO: Add a lock to the archiver to have determinstic behaviour on where are blocks } const data = await this.db.blockArchive.get(slot); return data && { block: data, executionOptimistic: false, finalized: true }; } async getBlockByRoot(root) { const block = this.forkChoice.getBlockHexDefaultStatus(root); if (block) { // Block found in fork-choice. // It may be in the block input cache, awaiting full DA reconstruction, check there first // Otherwise (most likely), check the hot db const blockInput = this.seenBlockInputCache.get(block.blockRoot); if (blockInput?.hasBlock()) { return { block: blockInput.getBlock(), executionOptimistic: isOptimisticBlock(block), finalized: false }; } const data = await this.db.block.get(fromHex(root)); if (data) { return { block: data, executionOptimistic: isOptimisticBlock(block), finalized: false }; } // If block is not found in hot db, try cold db since there could be an archive cycle happening // TODO: Add a lock to the archiver to have deterministic behavior on where are blocks } const data = await this.db.blockArchive.getByRoot(fromHex(root)); return data && { block: data, executionOptimistic: false, finalized: true }; } async getSerializedBlockByRoot(root) { const block = this.forkChoice.getBlockHexDefaultStatus(root); if (block) { // Block found in fork-choice. // It may be in the block input cache, awaiting full DA reconstruction, check there first // Otherwise (most likely), check the hot db const blockInput = this.seenBlockInputCache.get(block.blockRoot); if (blockInput?.hasBlock()) { const signedBlock = blockInput.getBlock(); const serialized = this.serializedCache.get(signedBlock); if (serialized) { return { block: serialized, executionOptimistic: isOptimisticBlock(block), finalized: false, slot: blockInput.slot, }; } return { block: sszTypesFor(blockInput.forkName).SignedBeaconBlock.serialize(signedBlock), executionOptimistic: isOptimisticBlock(block), finalized: false, slot: blockInput.slot, }; } const data = await this.db.block.getBinary(fromHex(root)); if (data) { const slot = getSlotFromSignedBeaconBlockSerialized(data); if (slot === null) throw new Error(`Invalid block data stored in DB for root: ${root}`); return { block: data, executionOptimistic: isOptimisticBlock(block), finalized: false, slot }; } // If block is not found in hot db, try cold db since there could be an archive cycle happening // TODO: Add a lock to the archiver to have deterministic behavior on where are blocks } const data = await this.db.blockArchive.getBinaryEntryByRoot(fromHex(root)); return data && { block: data.value, executionOptimistic: false, finalized: true, slot: data.key }; } async getBlobSidecars(blockSlot, blockRootHex) { const blockInput = this.seenBlockInputCache.get(blockRootHex); if (blockInput) { if (!isBlockInputBlobs(blockInput)) { throw new Error(`Expected block input to have blobs: slot=${blockSlot} root=${blockRootHex}`); } if (!blockInput.hasAllData()) { return null; } return blockInput.getBlobs(); } const unfinalizedBlobSidecars = (await this.db.blobSidecars.get(fromHex(blockRootHex)))?.blobSidecars ?? null; if (unfinalizedBlobSidecars) { return unfinalizedBlobSidecars; } return (await this.db.blobSidecarsArchive.get(blockSlot))?.blobSidecars ?? null; } async getSerializedBlobSidecars(blockSlot, blockRootHex) { const blockInput = this.seenBlockInputCache.get(blockRootHex); if (blockInput) { if (!isBlockInputBlobs(blockInput)) { throw new Error(`Expected block input to have blobs: slot=${blockSlot} root=${blockRootHex}`); } if (!blockInput.hasAllData()) { return null; } return ssz.deneb.BlobSidecars.serialize(blockInput.getBlobs()); } const unfinalizedBlobSidecarsWrapper = await this.db.blobSidecars.getBinary(fromHex(blockRootHex)); if (unfinalizedBlobSidecarsWrapper) { return unfinalizedBlobSidecarsWrapper.slice(BLOB_SIDECARS_IN_WRAPPER_INDEX); } const finalizedBlobSidecarsWrapper = await this.db.blobSidecarsArchive.getBinary(blockSlot); if (finalizedBlobSidecarsWrapper) { return finalizedBlobSidecarsWrapper.slice(BLOB_SIDECARS_IN_WRAPPER_INDEX); } return null; } async getSerializedExecutionPayloadEnvelope(blockSlot, blockRootHex) { const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex); if (payloadInput?.hasPayloadEnvelope()) { const envelope = payloadInput.getPayloadEnvelope(); const serialized = this.serializedCache.get(envelope); if (serialized) { return serialized; } return ssz.gloas.SignedExecutionPayloadEnvelope.serialize(envelope); } return ((await this.db.executionPayloadEnvelope.getBinary(fromHex(blockRootHex))) ?? (await this.db.executionPayloadEnvelopeArchive.getBinary(blockSlot)) ?? null); } async getExecutionPayloadEnvelope(blockSlot, blockRootHex) { const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex); if (payloadInput?.hasPayloadEnvelope()) { return payloadInput.getPayloadEnvelope(); } return ((await this.db.executionPayloadEnvelope.get(fromHex(blockRootHex))) ?? (await this.db.executionPayloadEnvelopeArchive.get(blockSlot)) ?? null); } async getParentExecutionRequests(parentBlockSlot, parentBlockRootHex) { // at the fork boundary, parent is pre-gloas if (!isForkPostGloas(this.config.getForkName(parentBlockSlot))) { return ssz.electra.ExecutionRequests.defaultValue(); } const envelope = await this.getExecutionPayloadEnvelope(parentBlockSlot, parentBlockRootHex); if (envelope === null) { throw Error(`Parent execution payload envelope not found slot=${parentBlockSlot}, root=${parentBlockRootHex}`); } return envelope.message.executionRequests; } async getDataColumnSidecars(blockSlot, blockRootHex) { const fork = this.config.getForkName(blockSlot); if (isForkPostGloas(fork)) { // After gloas, columns are tracked in PayloadEnvelopeInput const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex); if (payloadInput) { return payloadInput.getAllColumns(); } } else { // Before gloas, columns are tracked in BlockInput const blockInput = this.seenBlockInputCache.get(blockRootHex); if (blockInput) { if (!isBlockInputColumns(blockInput)) { throw new Error(`Expected block input to have columns: slot=${blockSlot} root=${blockRootHex}`); } return blockInput.getAllColumns(); } } const sidecarsUnfinalized = await this.db.dataColumnSidecar.values(fromHex(blockRootHex)); if (sidecarsUnfinalized.length > 0) { return sidecarsUnfinalized; } const sidecarsFinalized = await this.db.dataColumnSidecarArchive.values(blockSlot); return sidecarsFinalized; } async getSerializedDataColumnSidecars(blockSlot, blockRootHex, indices) { const fork = this.config.getForkName(blockSlot); if (isForkPostGloas(fork)) { // After gloas, columns are tracked in PayloadEnvelopeInput const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex); if (payloadInput) { return indices.map((index) => { const sidecar = payloadInput.getColumn(index); if (!sidecar) { return undefined; } const serialized = this.serializedCache.get(sidecar); if (serialized) { return serialized; } return sszTypesFor(fork).DataColumnSidecar.serialize(sidecar); }); } } else { // Before gloas, columns are tracked in BlockInput const blockInput = this.seenBlockInputCache.get(blockRootHex); if (blockInput) { if (!isBlockInputColumns(blockInput)) { throw new Error(`Expected block input to have columns: slot=${blockSlot} root=${blockRootHex}`); } return indices.map((index) => { const sidecar = blockInput.getColumn(index); if (!sidecar) { return undefined; } const serialized = this.serializedCache.get(sidecar); if (serialized) { return serialized; } return sszTypesFor(blockInput.forkName).DataColumnSidecar.serialize(sidecar); }); } } const sidecarsUnfinalized = await this.db.dataColumnSidecar.getManyBinary(fromHex(blockRootHex), indices); if (sidecarsUnfinalized.some((sidecar) => sidecar != null)) { return sidecarsUnfinalized; } const sidecarsFinalized = await this.db.dataColumnSidecarArchive.getManyBinary(blockSlot, indices); return sidecarsFinalized; } async produceCommonBlockBody(blockAttributes) { const { slot, parentBlock } = blockAttributes; const state = await this.regen.getBlockSlotState(parentBlock, slot, { dontTransferCache: true }, RegenCaller.produceBlock); // TODO: To avoid breaking changes for metric define this attribute const blockType = BlockType.Full; return produceCommonBlockBody.call(this, blockType, state, blockAttributes); } produceBlock(blockAttributes) { return this.produceBlockWrapper(BlockType.Full, blockAttributes); } produceBlindedBlock(blockAttributes) { return this.produceBlockWrapper(BlockType.Blinded, blockAttributes); } async produceBlockWrapper(blockType, { randaoReveal, graffiti, slot, feeRecipient, commonBlockBodyPromise, parentBlock, }) { const state = await this.regen.getBlockSlotState(parentBlock, slot, { dontTransferCache: true }, RegenCaller.produceBlock); const proposerIndex = state.getBeaconProposer(slot); const proposerPubKey = this.pubkeyCache.getOrThrow(proposerIndex).toBytes(); const { body, produceResult, executionPayloadValue, shouldOverrideBuilder } = await produceBlockBody.call(this, blockType, state, { randaoReveal, graffiti, slot, feeRecipient, parentBlock, proposerIndex, proposerPubKey, commonBlockBodyPromise, }); // The hashtree root computed here for debug log will get cached and hence won't introduce additional delays const bodyRoot = produceResult.type === BlockType.Full ? this.config.getForkTypes(slot).BeaconBlockBody.hashTreeRoot(body) : this.config .getPostBellatrixForkTypes(slot) .BlindedBeaconBlockBody.hashTreeRoot(body); this.logger.debug("Computing block post state from the produced body", { slot, bodyRoot: toRootHex(bodyRoot), blockType, }); const block = { slot, proposerIndex, parentRoot: fromHex(parentBlock.blockRoot), stateRoot: ZERO_HASH, body, }; const { newStateRoot, proposerReward } = computeNewStateRoot(this.metrics, state, block); block.stateRoot = newStateRoot; const blockRoot = produceResult.type === BlockType.Full ? this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block) : this.config.getPostBellatrixForkTypes(slot).BlindedBeaconBlock.hashTreeRoot(block); const blockRootHex = toRootHex(blockRoot); const fork = this.config.getForkName(slot); // 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. this.blockProductionCache.set(blockRootHex, produceResult); this.metrics?.blockProductionCacheSize.set(this.blockProductionCache.size); return { block, executionPayloadValue, consensusBlockValue: gweiToWei(proposerReward), shouldOverrideBuilder }; } async processBlock(block, opts) { return this.blockProcessor.processBlocksJob([block], null, opts); } async processChainSegment(blocks, payloadEnvelopes, opts) { await this.blockProcessor.processBlocksJob(blocks, payloadEnvelopes, opts); } async processExecutionPayload(payloadInput, opts) { return this.payloadEnvelopeProcessor.processPayloadEnvelopeJob(payloadInput, opts); } getStatus() { const head = this.forkChoice.getHead(); const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint(); const boundary = this.config.getForkBoundaryAtEpoch(this.clock.currentEpoch); return { // fork_digest: The node's ForkDigest (compute_fork_digest(current_fork_version, genesis_validators_root)) where // - current_fork_version is the fork version at the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync) // - genesis_validators_root is the static Root found in state.genesis_validators_root // - epoch of fork boundary is used to get blob parameters of current Blob Parameter Only (BPO) fork forkDigest: this.config.forkBoundary2ForkDigest(boundary), // finalized_root: state.finalized_checkpoint.root for the state corresponding to the head block (Note this defaults to Root(b'\x00' * 32) for the genesis finalized checkpoint). finalizedRoot: finalizedCheckpoint.epoch === GENESIS_EPOCH ? ZERO_HASH : finalizedCheckpoint.root, finalizedEpoch: finalizedCheckpoint.epoch, // TODO: PERFORMANCE: Memoize to prevent re-computing every time headRoot: fromHex(head.blockRoot), headSlot: head.slot, earliestAvailableSlot: this._earliestAvailableSlot, }; } recomputeForkChoiceHead(caller) { this.metrics?.forkChoice.requests.inc(); const timer = this.metrics?.forkChoice.findHead.startTimer({ caller }); try { return this.forkChoice.updateAndGetHead({ mode: UpdateHeadOpt.GetCanonicalHead }).head; } catch (e) { this.metrics?.forkChoice.errors.inc({ entrypoint: UpdateHeadOpt.GetCanonicalHead }); throw e; } finally { timer?.(); } } predictProposerHead(slot) { this.metrics?.forkChoice.requests.inc(); const timer = this.metrics?.forkChoice.findHead.startTimer({ caller: FindHeadFnName.predictProposerHead }); const secFromSlot = this.clock.secFromSlot(slot); try { return this.forkChoice.updateAndGetHead({ mode: UpdateHeadOpt.GetPredictedProposerHead, secFromSlot, slot }).head; } catch (e) { this.metrics?.forkChoice.errors.inc({ entrypoint: UpdateHeadOpt.GetPredictedProposerHead }); throw e; } finally { timer?.(); } } getProposerHead(slot) { this.metrics?.forkChoice.requests.inc(); const timer = this.metrics?.forkChoice.findHead.startTimer({ caller: FindHeadFnName.getProposerHead }); const secFromSlot = this.clock.secFromSlot(slot); try { const { head, isHeadTimely, notReorgedReason } = this.forkChoice.updateAndGetHead({ mode: UpdateHeadOpt.GetProposerHead, secFromSlot, slot, }); if (isHeadTimely && notReorgedReason !== undefined) { this.metrics?.forkChoice.notReorgedReason.inc({ reason: notReorgedReason }); } return head; } catch (e) { this.metrics?.forkChoice.errors.inc({ entrypoint: UpdateHeadOpt.GetProposerHead }); throw e; } finally { timer?.(); } } /** * Returns Promise that resolves either on block found or once 1 slot passes. * Used to handle unknown block root for both unaggregated and aggregated attestations. * @returns true if blockFound */ waitForBlock(slot, root) { return this.reprocessController.waitForBlockOfAttestation(slot, root); } persistBlock(data, suffix) { const slot = data.slot; if (isBlindedBeaconBlock(data)) { const sszType = this.config.getPostBellatrixForkTypes(slot).BlindedBeaconBlock; void this.persistSszObject("BlindedBeaconBlock", sszType.serialize(data), sszType.hashTreeRoot(data), suffix); } else { const sszType = this.config.getForkTypes(slot).BeaconBlock; void this.persistSszObject("BeaconBlock", sszType.serialize(data), sszType.hashTreeRoot(data), suffix); } } /** * Invalid state root error is critical and it causes the node to stale most of the time so we want to always * persist preState, postState and block for further investigation. */ async persistInvalidStateRoot(preState, postState, block) { const blockSlot = block.message.slot; const blockType = this.config.getForkTypes(blockSlot).SignedBeaconBlock; const postStateRoot = postState.hashTreeRoot(); const logStr = `slot_${blockSlot}_invalid_state_root_${toRootHex(postStateRoot)}`; await Promise.all([ this.persistSszObject(`SignedBeaconBlock_slot_${blockSlot}`, blockType.serialize(block), blockType.hashTreeRoot(block), `${logStr}_block`), this.persistSszObject(`preState_slot_${preState.slot}_BeaconState`, preState.serialize(), preState.hashTreeRoot(), `${logStr}_pre_state`), this.persistSszObject(`postState_slot_${postState.slot}_BeaconState`, postState.serialize(), postState.hashTreeRoot(), `${logStr}_post_state`), ]); } persistInvalidSszValue(type, sszObject, suffix) { if (this.opts.persistInvalidSszObjects) { void this.persistSszObject(type.typeName, type.serialize(sszObject), type.hashTreeRoot(sszObject), suffix); } } persistInvalidSszBytes(typeName, sszBytes, suffix) { if (this.opts.persistInvalidSszObjects) { void this.persistSszObject(typeName, sszBytes, sszBytes, suffix); } } /** * Regenerate state for attestation verification, this does not happen with default chain option of maxSkipSlots = 32 . * However, need to handle just in case. Lodestar doesn't support multiple regen state requests for attestation verification * at the same time, bounded inside "ShufflingCache.insertPromise()" function. * Leave this function in chain instead of attestatation verification code to make sure we're aware of its performance impact. */ async regenStateForAttestationVerification(attEpoch, shufflingDependentRoot, attHeadBlock, regenCaller) { // this is to prevent multiple calls to get shuffling for the same epoch and dependent root // any subsequent calls of the same epoch and dependent root will wait for this promise to resolve this.shufflingCache.insertPromise(attEpoch, shufflingDependentRoot); const blockEpoch = computeEpochAtSlot(attHeadBlock.slot); let state; if (blockEpoch < attEpoch - 1) { // thanks to one epoch look ahead, we don't need to dial up to attEpoch const targetSlot = computeStartSlotAtEpoch(attEpoch - 1); this.metrics?.gossipAttestation.useHeadBlockStateDialedToTargetEpoch.inc({ caller: regenCaller }); state = await this.regen.getBlockSlotState(attHeadBlock, targetSlot, { dontTransferCache: true }, regenCaller); } else if (blockEpoch > attEpoch) { // should not happen, handled inside attestation verification code throw Error(`Block epoch ${blockEpoch} is after attestation epoch ${attEpoch}`); } else { // should use either current or next shuffling of head state // it's not likely to hit this since these shufflings are cached already // so handle just in case this.metrics?.gossipAttestation.useHeadBlockState.inc({ caller: regenCaller }); state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller); } // resolve the promise to unblock other calls of the same epoch and dependent root this.shufflingCache.processState(state); return state.getShufflingAtEpoch(attEpoch); } /** * `ForkChoice.onBlock` must never throw for a block that is valid with respect to the network * `justifiedBalancesGetter()` must never throw and it should always return a state. * @param blockState state that declares justified checkpoint `checkpoint` */ justifiedBalancesGette