UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

1,263 lines (1,138 loc) 69 kB
import path from "node:path"; import {PrivateKey} from "@libp2p/interface"; import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; import {CheckpointWithHex, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice"; import {LoggerNode} from "@lodestar/logger/node"; import { EFFECTIVE_BALANCE_INCREMENT, type ForkPostFulu, type ForkPostGloas, GENESIS_SLOT, SLOTS_PER_EPOCH, isForkPostGloas, } from "@lodestar/params"; import { EffectiveBalanceIncrements, EpochShuffling, IBeaconStateView, PubkeyCache, computeEndSlotAtEpoch, computeEpochAtSlot, computeStartSlotAtEpoch, getEffectiveBalancesFromStateBytes, isStatePostAltair, isStatePostElectra, isStatePostGloas, } from "@lodestar/state-transition"; import { BeaconBlock, BlindedBeaconBlock, BlindedBeaconBlockBody, DataColumnSidecar, Epoch, Root, RootHex, SignedBeaconBlock, Slot, Status, UintNum64, ValidatorIndex, Wei, deneb, electra, gloas, isBlindedBeaconBlock, phase0, rewards, ssz, sszTypesFor, } from "@lodestar/types"; import {Logger, fromHex, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {GENESIS_EPOCH, ZERO_HASH} from "../constants/index.js"; import {IBeaconDb} from "../db/index.js"; import {BLOB_SIDECARS_IN_WRAPPER_INDEX} from "../db/repositories/blobSidecars.js"; import {BuilderStatus} from "../execution/builder/http.js"; import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js"; import {Metrics} from "../metrics/index.js"; import {computeNodeIdFromPrivateKey} from "../network/subnets/interface.js"; import {BufferPool} from "../util/bufferPool.js"; import {Clock, ClockEvent, IClock} 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 {IBlockInput, isBlockInputBlobs, isBlockInputColumns} from "./blocks/blockInput/index.js"; import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js"; import {PayloadEnvelopeProcessor} from "./blocks/payloadEnvelopeProcessor.js"; import {ImportPayloadOpts} from "./blocks/types.js"; import {persistBlockInput} from "./blocks/writeBlockInputToDb.js"; import {persistPayloadEnvelopeInput} from "./blocks/writePayloadEnvelopeInputToDb.js"; import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "./bls/index.js"; import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js"; import {ChainEvent, ChainEventEmitter} from "./emitter.js"; import {ForkchoiceCaller, initializeForkChoice} from "./forkChoice/index.js"; import {GetBlobsTracker} from "./GetBlobsTracker.js"; import {CommonBlockBody, FindHeadFnName, IBeaconChain, ProposerPreparationData, StateGetOpts} from "./interface.js"; import {LightClientServer} from "./lightClient/index.js"; import { AggregatedAttestationPool, AttestationPool, ExecutionPayloadBidPool, OpPool, PayloadAttestationPool, SyncCommitteeMessagePool, SyncContributionAndProofPool, } from "./opPools/index.js"; import {IChainOptions} from "./options.js"; import {PrepareNextSlotScheduler} from "./prepareNextSlot.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"; import { PayloadEnvelopeInput, 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 {CPStateDatastore} from "./stateCache/datastore/types.js"; import {FIFOBlockStateCache} from "./stateCache/fifoBlockStateCache.js"; import {PersistentCheckpointStateCache} from "./stateCache/persistentCheckpointsCache.js"; import {CheckpointStateCache} from "./stateCache/types.js"; import {ValidatorMonitor} from "./validatorMonitor.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 implements IBeaconChain { readonly genesisTime: UintNum64; readonly genesisValidatorsRoot: Root; readonly executionEngine: IExecutionEngine; readonly executionBuilder?: IExecutionBuilder; // Expose config for convenience in modularized functions readonly config: BeaconConfig; readonly custodyConfig: CustodyConfig; readonly logger: Logger; readonly metrics: Metrics | null; readonly validatorMonitor: ValidatorMonitor | null; readonly bufferPool: BufferPool; readonly anchorStateLatestBlockSlot: Slot; readonly bls: IBlsVerifier; readonly forkChoice: IForkChoice; readonly clock: IClock; readonly emitter: ChainEventEmitter; readonly regen: QueuedStateRegenerator; readonly lightClientServer?: LightClientServer; readonly reprocessController: ReprocessController; readonly archiveStore: ArchiveStore; readonly unfinalizedBlockWrites: JobItemQueue<[IBlockInput], void>; readonly unfinalizedPayloadEnvelopeWrites: JobItemQueue<[PayloadEnvelopeInput], void>; // Ops pool readonly attestationPool: AttestationPool; readonly aggregatedAttestationPool: AggregatedAttestationPool; readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; readonly syncContributionAndProofPool; readonly executionPayloadBidPool: ExecutionPayloadBidPool; readonly payloadAttestationPool: PayloadAttestationPool; readonly opPool: OpPool; // Gossip seen cache readonly seenAttesters = new SeenAttesters(); readonly seenAggregators = new SeenAggregators(); readonly seenPayloadAttesters = new SeenPayloadAttesters(); readonly seenAggregatedAttestations: SeenAggregatedAttestations; readonly seenExecutionPayloadBids = new SeenExecutionPayloadBids(); readonly seenProposerPreferences = new SeenProposerPreferences(); readonly seenBlockProposers = new SeenBlockProposers(); readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); readonly seenContributionAndProof: SeenContributionAndProof; readonly seenAttestationDatas: SeenAttestationDatas; readonly seenBlockInputCache: SeenBlockInput; readonly seenPayloadEnvelopeInputCache: SeenPayloadEnvelopeInput; // Seen cache for liveness checks readonly seenBlockAttesters = new SeenBlockAttesters(); // Global state caches readonly pubkeyCache: PubkeyCache; readonly beaconProposerCache: BeaconProposerCache; readonly checkpointBalancesCache: CheckpointBalancesCache; readonly shufflingCache: 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. */ readonly blockProductionCache = new Map<RootHex, ProduceResult>(); readonly blacklistedBlocks: Map<RootHex, Slot | null>; readonly serializedCache: SerializedCache; readonly getBlobsTracker: GetBlobsTracker; readonly columnReconstructionTracker: ColumnReconstructionTracker; readonly opts: IChainOptions; protected readonly blockProcessor: BlockProcessor; protected readonly payloadEnvelopeProcessor: PayloadEnvelopeProcessor; protected readonly db: IBeaconDb; // this is only available if nHistoricalStates is enabled private readonly cpStateDatastore?: CPStateDatastore; private abortController = new AbortController(); private processShutdownCallback: ProcessShutdownCallback; private _earliestAvailableSlot: Slot; get earliestAvailableSlot(): Slot { return this._earliestAvailableSlot; } set earliestAvailableSlot(slot: Slot) { if (this._earliestAvailableSlot !== slot) { this._earliestAvailableSlot = slot; this.emitter.emit(ChainEvent.updateStatus); } } constructor( opts: IChainOptions, { privateKey, config, pubkeyCache, db, dbName, dataDir, logger, processShutdownCallback, clock, metrics, validatorMonitor, anchorState, isAnchorStateFinalized, executionEngine, executionBuilder, }: { privateKey: PrivateKey; config: BeaconConfig; pubkeyCache: PubkeyCache; db: IBeaconDb; dbName: string; dataDir: string; logger: Logger; processShutdownCallback: ProcessShutdownCallback; /** Used for testing to supply fake clock */ clock?: IClock; metrics: Metrics | null; validatorMonitor: ValidatorMonitor | null; anchorState: IBeaconStateView; isAnchorStateFinalized: boolean; executionEngine: IExecutionEngine; executionBuilder?: IExecutionBuilder; } ) { 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: 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 as LoggerNode, 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(): Promise<void> { await this.archiveStore.init(); await this.loadFromDisk(); } async close(): Promise<void> { 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: RootHex): boolean { return this.seenBlockInputCache.hasBlock(blockRoot) || this.forkChoice.hasBlockHexUnsafe(blockRoot); } seenPayloadEnvelope(blockRoot: RootHex): boolean { return this.seenPayloadEnvelopeInputCache.hasPayload(blockRoot) || this.forkChoice.hasPayloadHexUnsafe(blockRoot); } regenCanAcceptWork(): boolean { return this.regen.canAcceptWork(); } blsThreadPoolCanAcceptWork(): boolean { return this.bls.canAcceptWork(); } validatorSeenAtEpoch(index: ValidatorIndex, epoch: Epoch): boolean { // 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(): Promise<void> { 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(): Promise<void> { await this.archiveStore.persistToDisk(); await this.opPool.toPersisted(this.db); } getHeadState(): IBeaconStateView { // 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: RegenCaller): Promise<IBeaconStateView> { return this.getHeadStateAtEpoch(this.clock.currentEpoch, regenCaller); } async getHeadStateAtEpoch(epoch: Epoch, regenCaller: RegenCaller): Promise<IBeaconStateView> { // 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: Slot, opts?: StateGetOpts ): Promise<{state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null> { 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: number ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { 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: RootHex, opts?: StateGetOpts ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { 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?: phase0.Checkpoint): Promise<Uint8Array | null> { 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: CheckpointWithHex ): {state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null { // 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: CheckpointWithHex ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { 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: Slot ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { 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: string ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { 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: string ): Promise<{block: Uint8Array; executionOptimistic: boolean; finalized: boolean; slot: Slot} | null> { 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: Slot, blockRootHex: string): Promise<deneb.BlobSidecars | null> { 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: Slot, blockRootHex: string): Promise<Uint8Array | null> { 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: Slot, blockRootHex: string): Promise<Uint8Array | null> { 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: Slot, blockRootHex: string ): Promise<gloas.SignedExecutionPayloadEnvelope | null> { 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: Slot, parentBlockRootHex: RootHex ): Promise<electra.ExecutionRequests> { // 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: Slot, blockRootHex: string): Promise<DataColumnSidecar[]> { 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: Slot, blockRootHex: string, indices: number[] ): Promise<(Uint8Array | undefined)[]> { 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 as ForkPostGloas).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 as ForkPostFulu).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: BlockAttributes): Promise<CommonBlockBody> { 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: BlockAttributes & {commonBlockBodyPromise: Promise<CommonBlockBody>}): Promise<{ block: BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Wei; shouldOverrideBuilder?: boolean; }> { return this.produceBlockWrapper<BlockType.Full>(BlockType.Full, blockAttributes); } produceBlindedBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise<CommonBlockBody>}): Promise<{ block: BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Wei; }> { return this.produceBlockWrapper<BlockType.Blinded>(BlockType.Blinded, blockAttributes); } async produceBlockWrapper<T extends BlockType>( blockType: T, { randaoReveal, graffiti, slot, feeRecipient, commonBlockBodyPromise, parentBlock, }: BlockAttributes & {commonBlockBodyPromise: Promise<CommonBlockBody>} ): Promise<{ block: AssembledBlockType<T>; executionPayloadValue: Wei; consensusBlockValue: Wei; shouldOverrideBuilder?: boolean; }> { 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 as BlindedBeaconBlockBody); 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, } as AssembledBlockType<T>; 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 as BlindedBeaconBlock); 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: IBlockInput, opts?: ImportBlockOpts): Promise<void> { return this.blockProcessor.processBlocksJob([block], null, opts); } async processChainSegment( blocks: IBlockInput[], payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null, opts?: ImportBlockOpts ): Promise<void> { await this.blockProcessor.processBlocksJob(blocks, payloadEnvelopes, opts); } async processExecutionPayload(payloadInput: PayloadEnvelopeInput, opts?: ImportPayloadOpts): Promise<void> { return this.payloadEnvelopeProcessor.processPayloadEnvelopeJob(payloadInput, opts); } getStatus(): Status { 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: ForkchoiceCaller): ProtoBlock { 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: Slot): ProtoBlock { 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: Slot): ProtoBlock { 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: Slot, root: RootHex): Promise<boolean> { return this.reprocessController.waitForBlockOfAttestation(slot, root); } persistBlock(data: BeaconBlock | BlindedBeaconBlock, suffix?: string): void { 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: IBeaconStateView, postState: IBeaconStateView, block: SignedBeaconBlock ): Promise<void> { 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