UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

347 lines • 16.1 kB
import { EventEmitter } from "node:events"; import { computeStartSlotAtEpoch, isStatePostGloas } from "@lodestar/state-transition"; import { prettyPrintIndices, toRootHex } from "@lodestar/utils"; import { AttestationImportOpt } from "../../chain/blocks/index.js"; import { assertLinearChainSegment } from "../../chain/blocks/utils/chainSegment.js"; import { BlockError } from "../../chain/errors/index.js"; import { DownloadByRangeError, DownloadByRangeErrorCode, cacheByRangeResponses, downloadByRange, } from "../utils/downloadByRange.js"; import { RangeSyncType, getRangeSyncTarget, rangeSyncTypes } from "../utils/remoteSyncType.js"; import { SyncChain } from "./chain.js"; import { updateChains } from "./utils/index.js"; export { RangeSyncEvent }; var RangeSyncEvent; (function (RangeSyncEvent) { RangeSyncEvent["completedChain"] = "RangeSync-completedChain"; })(RangeSyncEvent || (RangeSyncEvent = {})); export { RangeSyncStatus }; var RangeSyncStatus; (function (RangeSyncStatus) { /** A finalized chain is being synced */ RangeSyncStatus[RangeSyncStatus["Finalized"] = 0] = "Finalized"; /** There are no finalized chains and we are syncing one more head chains */ RangeSyncStatus[RangeSyncStatus["Head"] = 1] = "Head"; /** There are no head or finalized chains and no long range sync is in progress */ RangeSyncStatus[RangeSyncStatus["Idle"] = 2] = "Idle"; })(RangeSyncStatus || (RangeSyncStatus = {})); /** * RangeSync groups peers by their `status` into static target `SyncChain` instances * Peers on each chain will be queried for batches until reaching their target. * * Not all SyncChain-s will sync at once, and are grouped by sync type: * - Finalized Chain Sync * - Head Chain Sync * * ### Finalized Chain Sync * * At least one peer's status finalized checkpoint is greater than ours. Then we'll form * a chain starting from our finalized epoch and sync up to their finalized checkpoint. * - Only one finalized chain can sync at a time * - The finalized chain with the largest peer pool takes priority * - As peers' status progresses we will switch to a SyncChain with a better target * * ### Head Chain Sync * * If no Finalized Chain Sync is active, and the peer's STATUS head is beyond * `SLOT_IMPORT_TOLERANCE`, then we'll form a chain starting from our finalized epoch and sync * up to their head. * - More than one head chain can sync in parallel * - If there are many head chains the ones with more peers take priority */ export class RangeSync extends EventEmitter { chain; network; metrics; config; logger; /** There is a single chain per type, 1 finalized sync, 1 head sync */ chains = new Map(); opts; constructor(modules, opts) { super(); const { chain, network, metrics, config, logger } = modules; this.chain = chain; this.network = network; this.metrics = metrics; this.config = config; this.logger = logger; this.opts = opts; if (metrics) { metrics.syncStatus.addCollect(() => this.scrapeMetrics(metrics)); } } /** Throw / return all AsyncGenerators inside every SyncChain instance */ close() { for (const chain of this.chains.values()) { chain.remove(); } } /** * A peer with a relevant STATUS message has been found, which also is advanced from us. * Add this peer to an existing chain or create a new one. The update the chains status. */ addPeer(peerId, localStatus, peerStatus) { // Compute if we should do a Finalized or Head sync with this peer const { syncType, startEpoch, target } = getRangeSyncTarget(localStatus, peerStatus, this.chain); this.logger.debug("Sync peer joined", { peer: peerId, syncType, startEpoch, targetSlot: target.slot, targetRoot: toRootHex(target.root), localHeadSlot: localStatus.headSlot, earliestAvailableSlot: peerStatus.earliestAvailableSlot ?? Infinity, }); // If the peer existed in any other chain, remove it. // re-status'd peers can exist in multiple finalized chains, only one sync at a time if (syncType === RangeSyncType.Head) { this.removePeer(peerId); } this.addPeerOrCreateChain(startEpoch, target, peerId, syncType); this.update(localStatus.finalizedEpoch); } /** * Remove this peer from all head and finalized chains. A chain may become peer-empty and be dropped */ removePeer(peerId) { for (const syncChain of this.chains.values()) { syncChain.removePeer(peerId); } } /** * Compute the current RangeSync state, not cached */ get state() { const syncingHeadTargets = []; for (const chain of this.chains.values()) { if (chain.isSyncing) { if (chain.syncType === RangeSyncType.Finalized) { return { status: RangeSyncStatus.Finalized, target: chain.target }; } syncingHeadTargets.push(chain.target); } } if (syncingHeadTargets.length > 0) { return { status: RangeSyncStatus.Head, targets: syncingHeadTargets }; } return { status: RangeSyncStatus.Idle }; } /** Full debug state for lodestar API */ getSyncChainsDebugState() { return Array.from(this.chains.values()) .map((syncChain) => syncChain.getDebugState()) .reverse(); // Newest additions first } /** Convenience method for `SyncChain` */ processChainSegment = async (blocks, payloadEnvelopes, syncType) => { // Not trusted, verify signatures const flags = { // Only skip importing attestations for finalized sync. For head sync attestation are valuable. // Importing attestations also triggers a head update, see https://github.com/ChainSafe/lodestar/issues/3804 // TODO: Review if this is okay, can we prevent some attacks by importing attestations? importAttestations: syncType === RangeSyncType.Finalized ? AttestationImportOpt.Skip : undefined, // Ignores ALREADY_KNOWN or GENESIS_BLOCK errors, and continues with the next block in chain segment ignoreIfKnown: true, // Ignore WOULD_REVERT_FINALIZED_SLOT error, continue with the next block in chain segment ignoreIfFinalized: true, // We won't attest to this block so it's okay to ignore a SYNCING message from execution layer fromRangeSync: true, // when this runs, syncing is the most important thing and gossip is not likely to run // so we can utilize worker threads to verify signatures blsVerifyOnMainThread: false, }; if (this.opts?.disableProcessAsChainSegment) { // Should only be used for debugging or testing for (const block of blocks) { await this.chain.processBlock(block, flags); const payloadEnvelope = payloadEnvelopes?.get(block.slot); if (payloadEnvelope) { await this.chain.processExecutionPayload(payloadEnvelope); } } } else { await this.chain.processChainSegment(blocks, payloadEnvelopes, flags); } }; downloadByRange = async (peer, batch) => { const batchBlocks = batch.getBlocks(); const requests = batch.getRequestsForPeer(peer); const parentRoot = requests.parentPayloadRequest?.envelopeBlockRoot ?? requests.parentPayloadRequest?.blockRoot; const parentPayloadCommitments = parentRoot ? batch.getParentPayloadCommitments(parentRoot) : undefined; const { result, warnings } = await downloadByRange({ config: this.config, network: this.network, logger: this.logger, peerIdStr: peer.peerId, batchBlocks, parentPayloadCommitments, peerDasMetrics: this.chain.metrics?.peerDas, ...requests, }); const { responses, payloadEnvelopes: downloadedPayloadEnvelopes } = result; const { blocks, payloadEnvelopes } = cacheByRangeResponses({ cache: this.chain.seenBlockInputCache, seenPayloadEnvelopeInputCache: this.chain.seenPayloadEnvelopeInputCache, peerIdStr: peer.peerId, responses, batchBlocks, downloadedPayloadEnvelopes, existingPayloadEnvelopes: batch.getPayloadEnvelopes(), custodyConfig: this.chain.custodyConfig, seenTimestampSec: Date.now() / 1000, }); const segmentBlocks = blocks.filter((b) => b.hasBlock()).sort((a, b) => a.slot - b.slot); const envelopeSlots = payloadEnvelopes ? Array.from(payloadEnvelopes.entries()) .filter(([, pi]) => pi.hasPayloadEnvelope()) .map(([slot]) => slot) .sort((a, b) => a - b) : []; this.logger.verbose("downloadByRange batch ready", { peer: peer.peerId, blockSlots: prettyPrintIndices(segmentBlocks.map((b) => b.slot)), envelopeSlots: prettyPrintIndices(envelopeSlots), ...batch.getMetadata(), }); if (segmentBlocks.length > 1) { try { assertLinearChainSegment(this.config, segmentBlocks, payloadEnvelopes, null); } catch (err) { if (err instanceof BlockError) { this.logger.debug("downloadByRange segment validation failed", { peer: peer.peerId, reason: err.type.code, slot: err.signedBlock.message.slot, detail: JSON.stringify(err.type), ...batch.getMetadata(), }, err); // with this error, the peer will be penalized inside SyncChain throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT, slot: err.signedBlock.message.slot, reason: err.type.code, }); } throw err; } } return { result: { blocks, payloadEnvelopes }, warnings }; }; pruneBlockInputs = (blocks) => { for (const block of blocks) { this.chain.seenBlockInputCache.prune(block.blockRootHex); } }; /** Convenience method for `SyncChain` */ reportPeer = (peer, action, actionName) => { this.network.reportPeer(peer, action, actionName); }; getConnectedPeerSyncMeta = (peerId) => { return this.network.getConnectedPeerSyncMeta(peerId); }; /** Convenience method for `SyncChain` */ onSyncChainEnd = (err, target) => { this.update(this.chain.forkChoice.getFinalizedCheckpoint().epoch); this.emit(RangeSyncEvent.completedChain); if (err === null && target !== null) { this.metrics?.syncRange.syncChainHighestTargetSlotCompleted.set(target.slot); } }; addPeerOrCreateChain(startEpoch, target, peer, syncType) { let syncChain = this.chains.get(syncType); if (!syncChain) { // The first batch of a new sync chain may need to detect whether the parent block was an // gloas "empty" block (no envelope produced). It does so by comparing the first // downloaded block's `bid.parentBlockHash` against the head state's `latestExecutionPayloadBid.blockHash`. const headState = this.chain.getHeadState(); const latestBid = isStatePostGloas(headState) ? headState.latestExecutionPayloadBid : undefined; syncChain = new SyncChain(startEpoch, target, syncType, { processChainSegment: this.processChainSegment, downloadByRange: this.downloadByRange, reportPeer: this.reportPeer, getConnectedPeerSyncMeta: this.getConnectedPeerSyncMeta, pruneBlockInputs: this.pruneBlockInputs, onEnd: this.onSyncChainEnd, }, { config: this.config, clock: this.chain.clock, logger: this.logger, custodyConfig: this.chain.custodyConfig, metrics: this.metrics, }, latestBid); this.chains.set(syncType, syncChain); this.metrics?.syncRange.syncChainsEvents.inc({ syncType: syncChain.syncType, event: "add" }); this.logger.debug("SyncChain added", { syncType, firstEpoch: syncChain.firstBatchEpoch, targetSlot: syncChain.target.slot, targetRoot: toRootHex(syncChain.target.root), peer, }); } syncChain.addPeer(peer, target); } update(localFinalizedEpoch) { const localFinalizedSlot = computeStartSlotAtEpoch(localFinalizedEpoch); // Remove chains that are out-dated, peer-empty, completed or failed for (const [id, syncChain] of this.chains.entries()) { // Checks if a Finalized or Head chain should be removed if ( // Sync chain has completed syncing or encountered an error syncChain.isRemovable || // Sync chain has no more peers to download from syncChain.peers === 0 || // Outdated: our chain has progressed beyond this sync chain syncChain.target.slot < localFinalizedSlot || this.chain.forkChoice.hasBlock(syncChain.target.root)) { syncChain.remove(); this.chains.delete(id); this.metrics?.syncRange.syncChainsEvents.inc({ syncType: syncChain.syncType, event: "remove" }); this.logger.debug("SyncChain removed", { id: syncChain.logId, localFinalizedSlot, lastValidatedSlot: syncChain.lastValidatedSlot, firstEpoch: syncChain.firstBatchEpoch, targetSlot: syncChain.target.slot, targetRoot: toRootHex(syncChain.target.root), validatedEpochs: syncChain.validatedEpochs, }); // Re-status peers from successful chain. Potentially trigger a Head sync this.network .reStatusPeers(syncChain.getPeers()) .catch((e) => this.logger.error("Error resyncing peers", {}, e)); } } const { toStop, toStart } = updateChains(Array.from(this.chains.values())); for (const syncChain of toStop) { syncChain.stopSyncing(); if (syncChain.isSyncing) { this.metrics?.syncRange.syncChainsEvents.inc({ syncType: syncChain.syncType, event: "stop" }); } } for (const syncChain of toStart) { syncChain.startSyncing(localFinalizedEpoch); if (!syncChain.isSyncing) { this.metrics?.syncRange.syncChainsEvents.inc({ syncType: syncChain.syncType, event: "start" }); } } } scrapeMetrics(metrics) { metrics.syncRange.syncChainsPeers.reset(); const syncChainsByType = { [RangeSyncType.Finalized]: 0, [RangeSyncType.Head]: 0, }; for (const chain of this.chains.values()) { metrics.syncRange.syncChainsPeers.observe({ syncType: chain.syncType }, chain.peers); syncChainsByType[chain.syncType]++; } for (const syncType of rangeSyncTypes) { metrics.syncRange.syncChains.set({ syncType }, syncChainsByType[syncType]); } } } //# sourceMappingURL=range.js.map