@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
962 lines • 66.4 kB
JavaScript
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