@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
901 lines • 49.3 kB
JavaScript
import path from "node:path";
import { PubkeyIndexMap } from "@chainsafe/pubkey-index-map";
import { ExecutionStatus, UpdateHeadOpt } from "@lodestar/fork-choice";
import { ForkSeq, GENESIS_SLOT, SLOTS_PER_EPOCH, isForkPostElectra } from "@lodestar/params";
import { computeAnchorCheckpoint, computeEndSlotAtEpoch, computeEpochAtSlot, computeStartSlotAtEpoch, createCachedBeaconState, getEffectiveBalanceIncrementsZeroInactive, isCachedBeaconState, processSlots, } from "@lodestar/state-transition";
import { isBlindedBeaconBlock, } from "@lodestar/types";
import { fromHex, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toRootHex } from "@lodestar/utils";
import { GENESIS_EPOCH, ZERO_HASH } from "../constants/index.js";
import { BufferPool } from "../util/bufferPool.js";
import { Clock, ClockEvent } from "../util/clock.js";
import { ensureDir, writeIfNotExist } from "../util/file.js";
import { isOptimisticBlock } from "../util/forkChoice.js";
import { SerializedCache } from "../util/serializedCache.js";
import { ArchiveStore } from "./archiveStore/archiveStore.js";
import { CheckpointBalancesCache } from "./balancesCache.js";
import { BeaconProposerCache } from "./beaconProposerCache.js";
import { BlockProcessor } from "./blocks/index.js";
import { BlsMultiThreadWorkerPool, BlsSingleThreadVerifier } from "./bls/index.js";
import { ChainEvent, ChainEventEmitter } from "./emitter.js";
import { initializeForkChoice } from "./forkChoice/index.js";
import { FindHeadFnName, } from "./interface.js";
import { LightClientServer } from "./lightClient/index.js";
import { AggregatedAttestationPool, AttestationPool, OpPool, SyncCommitteeMessagePool, SyncContributionAndProofPool, } from "./opPools/index.js";
import { PrepareNextSlotScheduler } from "./prepareNextSlot.js";
import { computeNewStateRoot } from "./produceBlock/computeNewStateRoot.js";
import { BlobsResultType, 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 { computeAttestationsRewards } from "./rewards/attestationsRewards.js";
import { computeBlockRewards } from "./rewards/blockRewards.js";
import { computeSyncCommitteeRewards } from "./rewards/syncCommitteeRewards.js";
import { SeenAggregators, SeenAttesters, SeenBlockProposers, SeenContributionAndProof, SeenSyncCommitteeMessages, } from "./seenCache/index.js";
import { SeenGossipBlockInput } from "./seenCache/index.js";
import { SeenAggregatedAttestations } from "./seenCache/seenAggregateAndProof.js";
import { SeenAttestationDatas } from "./seenCache/seenAttestationData.js";
import { SeenBlockAttesters } from "./seenCache/seenBlockAttesters.js";
import { SeenBlockInputCache } from "./seenCache/seenBlockInput.js";
import { ShufflingCache } from "./shufflingCache.js";
import { BlockStateCacheImpl } from "./stateCache/blockStateCacheImpl.js";
import { DbCPStateDatastore } from "./stateCache/datastore/db.js";
import { FileCPStateDatastore } from "./stateCache/datastore/file.js";
import { FIFOBlockStateCache } from "./stateCache/fifoBlockStateCache.js";
import { InMemoryCheckpointStateCache } from "./stateCache/inMemoryCheckpointsCache.js";
import { PersistentCheckpointStateCache } from "./stateCache/persistentCheckpointsCache.js";
/**
* Arbitrary constants, 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_ROOTS = 4;
export class BeaconChain {
constructor(opts, { config, db, dbName, dataDir, logger, processShutdownCallback, clock, metrics, validatorMonitor, anchorState, eth1, executionEngine, executionBuilder, }) {
this.opPool = new OpPool();
// Gossip seen cache
this.seenAttesters = new SeenAttesters();
this.seenAggregators = new SeenAggregators();
this.seenBlockProposers = new SeenBlockProposers();
this.seenSyncCommitteeMessages = new SeenSyncCommitteeMessages();
this.seenGossipBlockInput = new SeenGossipBlockInput();
// Seen cache for liveness checks
this.seenBlockAttesters = new SeenBlockAttesters();
/** Map keyed by executionPayload.blockHash of the block for those blobs */
this.producedContentsCache = new Map();
// Cache payloads 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.
this.producedBlockRoot = new Map();
this.producedBlindedBlockRoot = new Set();
this.abortController = new AbortController();
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.eth1 = eth1;
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 })
: new BlsMultiThreadWorkerPool(opts, { logger, metrics });
if (!clock)
clock = new Clock({ config, genesisTime: this.genesisTime, signal });
this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null]));
const preAggregateCutOffTime = (2 / 3) * this.config.SECONDS_PER_SLOT;
this.attestationPool = new AttestationPool(config, clock, preAggregateCutOffTime, this.opts?.preaggregateSlotDistance, metrics);
this.aggregatedAttestationPool = new AggregatedAttestationPool(this.config, metrics);
this.syncCommitteeMessagePool = new SyncCommitteeMessagePool(clock, preAggregateCutOffTime, this.opts?.preaggregateSlotDistance);
this.syncContributionAndProofPool = new SyncContributionAndProofPool(clock, metrics, logger);
this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics);
this.seenContributionAndProof = new SeenContributionAndProof(metrics);
this.seenAttestationDatas = new SeenAttestationDatas(metrics, this.opts?.attDataCacheSlotDistance);
this.beaconProposerCache = new BeaconProposerCache(opts);
this.checkpointBalancesCache = new CheckpointBalancesCache();
this.seenBlockInputCache = new SeenBlockInputCache({
config,
clock,
chainEvents: emitter,
signal,
metrics,
logger,
});
// Restore state caches
// anchorState may already by a CachedBeaconState. If so, don't create the cache again, since deserializing all
// pubkeys takes ~30 seconds for 350k keys (mainnet 2022Q2).
// When the BeaconStateCache is created in eth1 genesis builder it may be incorrect. Until we can ensure that
// it's safe to re-use _ANY_ BeaconStateCache, this option is disabled by default and only used in tests.
const cachedState = isCachedBeaconState(anchorState) && opts.skipCreateStateCacheIfAvailable
? anchorState
: createCachedBeaconState(anchorState, {
config,
pubkey2index: new PubkeyIndexMap(),
index2pubkey: [],
});
this.shufflingCache = cachedState.epochCtx.shufflingCache = new ShufflingCache(metrics, logger, this.opts, [
{
shuffling: cachedState.epochCtx.previousShuffling,
decisionRoot: cachedState.epochCtx.previousDecisionRoot,
},
{
shuffling: cachedState.epochCtx.currentShuffling,
decisionRoot: cachedState.epochCtx.currentDecisionRoot,
},
{
shuffling: cachedState.epochCtx.nextShuffling,
decisionRoot: cachedState.epochCtx.nextDecisionRoot,
},
]);
// Persist single global instance of state caches
this.pubkey2index = cachedState.epochCtx.pubkey2index;
this.index2pubkey = cachedState.epochCtx.index2pubkey;
const fileDataStore = opts.nHistoricalStatesFileDataStore ?? true;
const blockStateCache = this.opts.nHistoricalStates
? new FIFOBlockStateCache(this.opts, { metrics })
: new BlockStateCacheImpl({ metrics });
this.bufferPool = this.opts.nHistoricalStates
? new BufferPool(anchorState.type.tree_serializedSize(anchorState.node), metrics)
: null;
const checkpointStateCache = this.opts.nHistoricalStates
? new PersistentCheckpointStateCache({
metrics,
logger,
clock,
blockStateCache,
bufferPool: this.bufferPool,
datastore: fileDataStore
? // debug option if we want to investigate any issues with the DB
new FileCPStateDatastore(dataDir)
: // production option
new DbCPStateDatastore(this.db),
}, this.opts)
: new InMemoryCheckpointStateCache({ metrics });
const { checkpoint } = computeAnchorCheckpoint(config, anchorState);
blockStateCache.add(cachedState);
blockStateCache.setHeadState(cachedState);
checkpointStateCache.add(checkpoint, cachedState);
const forkChoice = initializeForkChoice(config, emitter, clock.currentSlot, cachedState, opts, this.justifiedBalancesGetter.bind(this), logger);
const regen = new QueuedStateRegenerator({
config,
forkChoice,
blockStateCache,
checkpointStateCache,
db,
metrics,
validatorMonitor,
logger,
emitter,
signal,
});
if (!opts.disableLightClientServer) {
this.lightClientServer = new LightClientServer(opts, { config, db, metrics, emitter, logger });
}
this.reprocessController = new ReprocessController(this.metrics);
this.blockProcessor = new BlockProcessor(this, metrics, opts, signal);
this.forkChoice = forkChoice;
this.clock = clock;
this.regen = regen;
this.bls = bls;
this.emitter = emitter;
this.serializedCache = new SerializedCache();
this.archiveStore = new ArchiveStore({ db, chain: this, logger: logger, metrics }, { ...opts, dbName, anchorState: { finalizedCheckpoint: anchorState.finalizedCheckpoint } }, signal);
// Stop polling eth1 data if anchor state is in Electra AND deposit_requests_start_index is reached
const anchorStateFork = this.config.getForkName(anchorState.slot);
if (isForkPostElectra(anchorStateFork)) {
const { eth1DepositIndex, depositRequestsStartIndex } = anchorState;
if (eth1DepositIndex === Number(depositRequestsStartIndex)) {
this.eth1.stopPollingEth1Data();
}
}
// 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));
}
async init() {
await this.archiveStore.init();
await this.loadFromDisk();
}
async close() {
await this.archiveStore.close();
await this.bls.close();
this.abortController.abort();
}
seenBlock(blockRoot) {
return this.seenGossipBlockInput.hasBlock(blockRoot) || this.forkChoice.hasBlockHex(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) ||
// 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.blockRoot, 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.blockRoot, 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.getBlock(state.latestBlockHeader.hashTreeRoot());
const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
return {
state,
executionOptimistic: block != null && isOptimisticBlock(block),
finalized: state.epochCtx.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.getBlock(cachedStateCtx.latestBlockHeader.hashTreeRoot());
const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
return {
state: cachedStateCtx,
executionOptimistic: block != null && isOptimisticBlock(block),
finalized: cachedStateCtx.epochCtx.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH,
};
}
const data = await this.db.stateArchive.getByRoot(fromHex(stateRoot));
return data && { state: data, executionOptimistic: false, finalized: true };
}
getStateByCheckpoint(checkpoint) {
// finalized or justified checkpoint states maynot be available with PersistentCheckpointStateCache, use getCheckpointStateOrBytes() api to get Uint8Array
const cachedStateCtx = this.regen.getCheckpointStateSync(checkpoint);
if (cachedStateCtx) {
const block = this.forkChoice.getBlock(cachedStateCtx.latestBlockHeader.hashTreeRoot());
const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
return {
state: cachedStateCtx,
executionOptimistic: block != null && isOptimisticBlock(block),
finalized: cachedStateCtx.epochCtx.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH,
};
}
return null;
}
async getStateOrBytesByCheckpoint(checkpoint) {
const cachedStateCtx = await this.regen.getCheckpointStateOrBytes(checkpoint);
if (cachedStateCtx) {
const block = this.forkChoice.getBlock(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) {
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.getBlockHex(root);
if (block) {
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 produceCommonBlockBody(blockAttributes) {
const { slot, parentBlockRoot } = blockAttributes;
const state = await this.regen.getBlockSlotState(toRootHex(parentBlockRoot), 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, parentBlockRoot, parentSlot, }) {
const state = await this.regen.getBlockSlotState(toRootHex(parentBlockRoot), slot, { dontTransferCache: true }, RegenCaller.produceBlock);
const proposerIndex = state.epochCtx.getBeaconProposer(slot);
const proposerPubKey = state.epochCtx.index2pubkey[proposerIndex].toBytes();
const { body, blobs, executionPayloadValue, shouldOverrideBuilder } = await produceBlockBody.call(this, blockType, state, {
randaoReveal,
graffiti,
slot,
feeRecipient,
parentSlot,
parentBlockRoot,
proposerIndex,
proposerPubKey,
commonBlockBodyPromise,
});
// The hashtree root computed here for debug log will get cached and hence won't introduce additional delays
const bodyRoot = blockType === 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: parentBlockRoot,
stateRoot: ZERO_HASH,
body,
};
const { newStateRoot, proposerReward } = computeNewStateRoot(this.metrics, state, block);
block.stateRoot = newStateRoot;
const blockRoot = blockType === BlockType.Full
? this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block)
: this.config.getPostBellatrixForkTypes(slot).BlindedBeaconBlock.hashTreeRoot(block);
const blockRootHex = toRootHex(blockRoot);
// track the produced block for consensus broadcast validations
if (blockType === BlockType.Full) {
this.logger.debug("Setting executionPayload cache for produced block", { blockRootHex, slot, blockType });
this.producedBlockRoot.set(blockRootHex, block.body.executionPayload ?? null);
this.metrics?.blockProductionCaches.producedBlockRoot.set(this.producedBlockRoot.size);
}
else {
this.logger.debug("Tracking the produced blinded block", { blockRootHex, slot, blockType });
this.producedBlindedBlockRoot.add(blockRootHex);
this.metrics?.blockProductionCaches.producedBlindedBlockRoot.set(this.producedBlindedBlockRoot.size);
}
// Cache for latter broadcasting
//
// blinded blobs will be fetched and added to this cache later before finally
// publishing the blinded block's full version
if (blobs.type === BlobsResultType.produced) {
// body is of full type here
const { blockHash, contents } = blobs;
this.producedContentsCache.set(blockHash, contents);
this.metrics?.blockProductionCaches.producedContentsCache.set(this.producedContentsCache.size);
}
return { block, executionPayloadValue, consensusBlockValue: gweiToWei(proposerReward), shouldOverrideBuilder };
}
/**
* https://github.com/ethereum/consensus-specs/blob/dev/specs/eip4844/validator.md#sidecar
* def get_blobs_sidecar(block: BeaconBlock, blobs: Sequence[Blob]) -> BlobSidecars:
* return BlobSidecars(
* beacon_block_root=hash_tree_root(block),
* beacon_block_slot=block.slot,
* blobs=blobs,
* kzg_aggregated_proof=compute_proof_from_blobs(blobs),
* )
*/
getContents(beaconBlock) {
const blockHash = toRootHex(beaconBlock.body.executionPayload.blockHash);
const contents = this.producedContentsCache.get(blockHash);
if (!contents) {
throw Error(`No contents for executionPayload.blockHash ${blockHash}`);
}
return contents;
}
async processBlock(block, opts) {
return this.blockProcessor.processBlocksJob([block], opts);
}
async processChainSegment(blocks, opts) {
return this.blockProcessor.processBlocksJob(blocks, 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,
};
}
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}_${preState.type.typeName}`, preState.serialize(), preState.hashTreeRoot(), `${logStr}_pre_state`),
this.persistSszObject(`postState_slot_${postState.slot}_${postState.type.typeName}`, 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);
}
}
persistInvalidSszView(view, suffix) {
if (this.opts.persistInvalidSszObjects) {
void this.persistSszObject(view.type.typeName, view.serialize(), view.hashTreeRoot(), 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.blockRoot, 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);
}
// should always be the current epoch of the active context so no need to await a result from the ShufflingCache
return state.epochCtx.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`
*/
justifiedBalancesGetter(checkpoint, blockState) {
this.metrics?.balancesCache.requests.inc();
const effectiveBalances = this.checkpointBalancesCache.get(checkpoint);
if (effectiveBalances) {
return effectiveBalances;
}
// not expected, need metrics
this.metrics?.balancesCache.misses.inc();
this.logger.debug("checkpointBalances cache miss", {
epoch: checkpoint.epoch,
root: checkpoint.rootHex,
});
const { state, stateId, shouldWarn } = this.closestJustifiedBalancesStateToCheckpoint(checkpoint, blockState);
this.metrics?.balancesCache.closestStateResult.inc({ stateId });
if (shouldWarn) {
this.logger.warn("currentJustifiedCheckpoint state not avail, using closest state", {
checkpointEpoch: checkpoint.epoch,
checkpointRoot: checkpoint.rootHex,
stateId,
stateSlot: state.slot,
stateRoot: toRootHex(state.hashTreeRoot()),
});
}
return getEffectiveBalanceIncrementsZeroInactive(state);
}
/**
* - Assumptions + invariant this function is based on:
* - Our cache can only persist X states at once to prevent OOM
* - Some old states (including to-be justified checkpoint) may / must be dropped from the cache
* - Thus, there is no guarantee that the state for a justified checkpoint will be available in the cache
* @param blockState state that declares justified checkpoint `checkpoint`
*/
closestJustifiedBalancesStateToCheckpoint(checkpoint, blockState) {
const state = this.regen.getCheckpointStateSync(checkpoint);
if (state) {
return { state, stateId: "checkpoint_state", shouldWarn: false };
}
// Check if blockState is in the same epoch, not need to iterate the fork-choice then
if (computeEpochAtSlot(blockState.slot) === checkpoint.epoch) {
return { state: blockState, stateId: "block_state_same_epoch", shouldWarn: true };
}
// Find a state in the same branch of checkpoint at same epoch. Balances should exactly the same
for (const descendantBlock of this.forkChoice.forwardIterateDescendants(checkpoint.rootHex)) {
if (computeEpochAtSlot(descendantBlock.slot) === checkpoint.epoch) {
const descendantBlockState = this.regen.getStateSync(descendantBlock.stateRoot);
if (descendantBlockState) {
return { state: descendantBlockState, stateId: "descendant_state_same_epoch", shouldWarn: true };
}
}
}
// Check if blockState is in the next epoch, not need to iterate the fork-choice then
if (computeEpochAtSlot(blockState.slot) === checkpoint.epoch + 1) {
return { state: blockState, stateId: "block_state_next_epoch", shouldWarn: true };
}
// Find a state in the same branch of checkpoint at a latter epoch. Balances are not the same, but should be close
// Note: must call .forwardIterateDescendants() again since nodes are not sorted
for (const descendantBlock of this.forkChoice.forwardIterateDescendants(checkpoint.rootHex)) {
if (computeEpochAtSlot(descendantBlock.slot) > checkpoint.epoch) {
const descendantBlockState = this.regen.getStateSync(descendantBlock.stateRoot);
if (descendantBlockState) {
return { state: blockState, stateId: "descendant_state_latter_epoch", shouldWarn: true };
}
}
}
// If there's no state available in the same branch of checkpoint use blockState regardless of its epoch
return { state: blockState, stateId: "block_state_any_epoch", shouldWarn: true };
}
async persistSszObject(prefix, bytes, root, logStr) {
const now = new Date();
// yyyy-MM-dd
const dateStr = now.toISOString().split("T")[0];
// by default store to lodestar_archive of current dir
const dirpath = path.join(this.opts.persistInvalidSszObjectsDir ?? "invalid_ssz_objects", dateStr);
const filepath = path.join(dirpath, `${prefix}_${toRootHex(root)}.ssz`);
await ensureDir(dirpath);
// as of Feb 17 2022 there are a lot of duplicate files stored with different date suffixes
// remove date suffixes in file name, and check duplicate to avoid redundant persistence
await writeIfNotExist(filepath, bytes);
this.logger.debug("Persisted invalid ssz object", { id: logStr, filepath });
}
onScrapeMetrics(metrics) {
// aggregatedAttestationPool tracks metrics on its own
metrics.opPool.attestationPool.size.set(this.attestationPool.getAttestationCount());
metrics.opPool.attesterSlashingPoolSize.set(this.opPool.attesterSlashingsSize);
metrics.opPool.proposerSlashingPoolSize.set(this.opPool.proposerSlashingsSize);
metrics.opPool.voluntaryExitPoolSize.set(this.opPool.voluntaryExitsSize);
metrics.opPool.syncCommitteeMessagePoolSize.set(this.syncCommitteeMessagePool.size);
// syncContributionAndProofPool tracks metrics on its own
metrics.opPool.blsToExecutionChangePoolSize.set(this.opPool.blsToExecutionChangeSize);
metrics.chain.blacklistedBlocks.set(this.blacklistedBlocks.size);
const forkChoiceMetrics = this.forkChoice.getMetrics();
metrics.forkChoice.votes.set(forkChoiceMetrics.votes);
metrics.forkChoice.queuedAttestations.set(forkChoiceMetrics.queuedAttestations);
metrics.forkChoice.validatedAttestationDatas.set(forkChoiceMetrics.validatedAttestationDatas);
metrics.forkChoice.balancesLength.set(forkChoiceMetrics.balancesLength);
metrics.forkChoice.nodes.set(forkChoiceMetrics.nodes);
metrics.forkChoice.indices.set(forkChoiceMetrics.indices);
const fork = this.config.getForkName(this.clock.currentSlot);
if (isForkPostElectra(fork)) {
const headStateElectra = this.getHeadState();
metrics.pendingDeposits.set(headStateElectra.pendingDeposits.length);
metrics.pendingPartialWithdrawals.set(headStateElectra.pendingPartialWithdrawals.length);
metrics.pendingConsolidations.set(headStateElectra.pendingConsolidations.length);
}
}
onClockSlot(slot) {
this.logger.verbose("Clock slot", { slot });
// CRITICAL UPDATE
if (this.forkChoice.irrecoverableError) {
this.processShutdownCallback(this.forkChoice.irrecoverableError);
}
this.forkChoice.updateTime(slot);
this.metrics?.clockSlot.set(slot);
this.attestationPool.prune(slot);
this.aggregatedAttestationPool.prune(slot);
this.syncCommitteeMessagePool.prune(slot);
this.seenSyncCommitteeMessages.prune(slot);
this.seenAttestationDatas.onSlot(slot);
this.reprocessController.onSlot(slot);
// Prune old cached block production artifacts, those are only useful on their slot
pruneSetToMax(this.producedBlockRoot, this.opts.maxCachedProducedRoots ?? DEFAULT_MAX_CACHED_PRODUCED_ROOTS);
this.metrics?.blockProductionCaches.producedBlockRoot.set(this.producedBlockRoot.size);
pruneSetToMax(this.producedBlindedBlockRoot, this.opts.maxCachedProducedRoots ?? DEFAULT_MAX_CACHED_PRODUCED_ROOTS);
this.metrics?.blockProductionCaches.producedBlindedBlockRoot.set(this.producedBlindedBlockRoot.size);
if (this.config.getForkSeq(slot) >= ForkSeq.deneb) {
pruneSetToMax(this.producedContentsCache, this.opts.maxCachedProducedRoots ?? DEFAULT_MAX_CACHED_PRODUCED_ROOTS);
this.metrics?.blockProductionCaches.producedContentsCache.set(this.producedContentsCache.size);
}
const metrics = this.metrics;
if (metrics && (slot + 1) % SLOTS_PER_EPOCH === 0) {
// On the last slot of the epoch
sleep((1000 * this.config.SECONDS_PER_SLOT) / 2)
.then(() => this.validatorMonitor?.onceEveryEndOfEpoch(this.getHeadState()))
.catch((e) => {
if (!isErrorAborted(e))
this.logger.error("Error on validator monitor onceEveryEndOfEpoch", { slot }, e);
});
}
}
onClockEpoch(epoch) {
this.metrics?.clockEpoch.set(epoch);
this.seenAttesters.prune(epoch);
this.seenAggregators.prune(epoch);
this.seenAggregatedAttestations.prune(epoch);
this.seenBlockAttesters.prune(epoch);
this.beaconProposerCache.prune(epoch);
// Poll for merge block in the background to speed-up block production. Only if:
// - after BELLATRIX_FORK_EPOCH
// - Beacon node synced
// - head state not isMergeTransitionComplete
if (this.config.BELLATRIX_FORK_EPOCH - epoch < 1) {
const head = this.forkChoice.getHead();
if (epoch - computeEpochAtSlot(head.slot) < 5 && head.executionStatus === ExecutionStatus.PreMerge) {
this.eth1.startPollingMergeBlock();
}
}
}
onNewHead(head) {
this.syncContributionAndProofPool.prune(head.slot);
this.seenContributionAndProof.prune(head.slot);
}
onForkChoiceJustified(cp) {
this.logger.verbose("Fork choice justified", { epoch: cp.epoch, root: cp.rootHex });
}
async onForkChoiceFinalized(cp) {
this.logger.verbose("Fork choice finalized", { epoch: cp.epoch, root: cp.rootHex });
this.seenBlockProposers.prune(computeStartSlotAtEpoch(cp.epoch));
// TODO: Improve using regen here
const { blockRoot, stateRoot, slot } = this.forkChoice.getHead();
const headState = this.regen.getStateSync(stateRoot);
const headBlock = await this.db.block.get(fromHex(blockRoot));
if (headBlock == null) {
throw Error(`Head block ${slot} ${headBlock} is not available in database`);
}
if (headState) {
this.opPool.pruneAll(headBlock, headState);
}
if (headState === null) {
this.logger.verbose("Head state is null");
}
}
async updateBeaconProposerData(epoch, proposers) {
for (const proposer of proposers) {
this.beaconProposerCache.add(epoch, proposer);
}
}
updateBuilderStatus(clockSlot) {
const executionBuilder = this.executionBuilder;
if (executionBuilder) {
const { faultInspectionWindow, allowedFaults } = executionBuilder;
const slotsPresent = this.forkChoice.getSlotsPresent(clockSlot - faultInspectionWindow);
const previousStatus = executionBuilder.status;
const shouldEnable = slotsPresent >= Math.min(faultInspectionWindow - allowedFaults, clockSlot);
executionBuilder.updateStatus(shouldEnable);
// The status changed we should log
const status = executionBuilder.status;
const builderLog = {
status,
slotsPresent,
faultInspectionWindow,
allowedFaults,
};
if (status !== previousStatus) {
this.logger.info("Execution builder status updated", builderLog);
}
else {
this.logger.verbose("Execution builder status", builderLog);
}
}
}
async getBlockRewards(block) {
let preState = this.regen.getPreStateSync(block);
if (preState === null) {
throw Error(`Pre-state is unavailable given block's parent root ${toRootHex(block.parentRoot)}`);
}
preState = processSlots(preState, block.slot); // Dial preState's slot to block.slot
const postState = this.regen.getStateSync(toRootHex(block.stateRoot)) ?? undefined;
return computeBlockRewards(block, preState.clone(), postState?.clone());
}
async getAttestationsRewards(epoch, validatorIds) {
// We use end slot of (epoch + 1) to ensure we have seen all attestations. On-time or late. Any late attestation beyond this slot is not considered
const slot = computeEndSlotAtEpoch(epoch + 1);
const stateResult = await this.getStateBySlot(slot, { allowRegen: false }); // No regen if state not in cache
if (stateResult === null) {
throw Error(`State is unavailable for slot ${slot}`);
}
const { executionOptimistic, finalized } = stateResult;
const stateRoot = toRootHex(stateResult.state.hashTreeRoot());
const cachedState = this.regen.getStateSync(stateRoot);
if (cachedState === null) {
throw Error(`State is not in cache for slot ${slot}`);
}
const rewards = await computeAttestationsRewards(epoch, cachedState, this.config, validatorIds);
return { rewards, executionOptimistic, finalized };
}
async getSyncCommitteeRewards(block, validatorIds) {
let preState = this.regen.getPreStateSync(block);
if (preState === null) {
throw Error(`Pre-state is unavailable given block's parent root ${toRootHex(block.parentRoot)}`);
}
preState = processSlots(preState, block.slot); // Dial preState's slot to block.slot
return computeSyncCommitteeRewards(block, preState.clone(), validatorIds);
}
}
//# sourceMappingURL=chain.js.map