UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

266 lines • 12.4 kB
import { SLOTS_PER_EPOCH } from "@lodestar/params"; import { toRootHex } from "@lodestar/utils"; import { GENESIS_SLOT } from "../constants/constants.js"; import { ExecutionEngineState } from "../execution/index.js"; import { NetworkEvent } from "../network/index.js"; import { ClockEvent } from "../util/clock.js"; import { isOptimisticBlock } from "../util/forkChoice.js"; import { MIN_EPOCH_TO_START_GOSSIP } from "./constants.js"; import { SyncState, syncStateMetric } from "./interface.js"; import { RangeSync, RangeSyncEvent, RangeSyncStatus } from "./range/range.js"; import { BlockInputSync } from "./unknownBlock.js"; import { PeerSyncType, getPeerSyncType, peerSyncTypes } from "./utils/remoteSyncType.js"; export class BeaconSync { logger; network; chain; metrics; opts; rangeSync; unknownBlockSync; /** For metrics only */ peerSyncType = new Map(); slotImportTolerance; constructor(opts, modules) { const { config, chain, metrics, network, logger } = modules; this.opts = opts; this.network = network; this.chain = chain; this.metrics = metrics; this.logger = logger; this.rangeSync = new RangeSync(modules, opts); this.unknownBlockSync = new BlockInputSync(config, network, chain, logger, metrics, opts); this.slotImportTolerance = opts.slotImportTolerance ?? SLOTS_PER_EPOCH; // Subscribe to RangeSync completing a SyncChain and recompute sync state if (!opts.disableRangeSync) { // prod code this.logger.debug("RangeSync enabled."); this.rangeSync.on(RangeSyncEvent.completedChain, this.updateSyncState); this.network.events.on(NetworkEvent.peerConnected, this.addPeer); this.network.events.on(NetworkEvent.peerDisconnected, this.removePeer); this.chain.clock.on(ClockEvent.epoch, this.onClockEpoch); } else { // test code, this is needed for Unknown block sync sim test this.unknownBlockSync.subscribeToNetwork(); this.logger.debug("RangeSync disabled."); // In case node is started with `rangeSync` disabled and `unknownBlockSync` is enabled. // If the epoch boundary happens right away the `onClockEpoch` will check for the `syncDiff` and if // it's more than 2 epoch will disable the disabling the `unknownBlockSync` as well. // This will result into node hanging on the head slot and not syncing any blocks. // This was the scenario in the test case `Unknown block sync` in `packages/cli/test/sim/multi_fork.test.ts` // So we are adding a particular delay to ensure that the `unknownBlockSync` is enabled. const syncStartSlot = this.chain.clock.currentSlot; // Having one epoch time for the node to connect to peers and start a syncing process const epochCheckForSyncSlot = syncStartSlot + SLOTS_PER_EPOCH; const initiateEpochCheckForSync = () => { if (this.chain.clock.currentSlot > epochCheckForSyncSlot) { this.logger.info("Initiating epoch check for sync progress"); this.chain.clock.off(ClockEvent.slot, initiateEpochCheckForSync); this.chain.clock.on(ClockEvent.epoch, this.onClockEpoch); } }; this.chain.clock.on(ClockEvent.slot, initiateEpochCheckForSync); } if (metrics) { metrics.syncStatus.addCollect(() => this.scrapeMetrics(metrics)); } } close() { this.network.events.off(NetworkEvent.peerConnected, this.addPeer); this.network.events.off(NetworkEvent.peerDisconnected, this.removePeer); this.chain.clock.off(ClockEvent.epoch, this.onClockEpoch); this.rangeSync.close(); this.unknownBlockSync.close(); } getSyncStatus() { const currentSlot = this.chain.clock.currentSlot; const elOffline = this.chain.executionEngine.state === ExecutionEngineState.OFFLINE || this.chain.executionEngine.state === ExecutionEngineState.AUTH_FAILED; // If we are pre/at genesis, signal ready if (currentSlot <= GENESIS_SLOT) { return { headSlot: 0, syncDistance: 0, isSyncing: false, isOptimistic: false, elOffline, }; } const head = this.chain.forkChoice.getHead(); switch (this.state) { case SyncState.SyncingFinalized: case SyncState.SyncingHead: case SyncState.Stalled: return { headSlot: head.slot, syncDistance: currentSlot - head.slot, isSyncing: true, isOptimistic: isOptimisticBlock(head), elOffline, }; case SyncState.Synced: return { headSlot: head.slot, syncDistance: 0, isSyncing: false, isOptimistic: isOptimisticBlock(head), elOffline, }; default: throw new Error("Node is stopped, cannot get sync status"); } } isSyncing() { const state = this.state; // Don't run the getter twice return state === SyncState.SyncingFinalized || state === SyncState.SyncingHead; } isSynced() { return this.state === SyncState.Synced; } get state() { const currentSlot = this.chain.clock.currentSlot; const headSlot = this.chain.forkChoice.getHead().slot; if ( // Consider node synced IF // Before genesis OR (currentSlot < 0 || // head is behind clock but close enough with some tolerance (headSlot <= currentSlot && headSlot >= currentSlot - this.slotImportTolerance)) && // Ensure there at least one connected peer to not claim synced if has no peers // Allow to bypass this conditions for local networks with a single node (this.opts.isSingleNode || this.network.getConnectedPeerCount() > 0) // TODO: Consider enabling this condition (used in Lighthouse) // && headSlot > 0 ) { return SyncState.Synced; } const rangeSyncState = this.rangeSync.state; switch (rangeSyncState.status) { case RangeSyncStatus.Finalized: return SyncState.SyncingFinalized; case RangeSyncStatus.Head: return SyncState.SyncingHead; case RangeSyncStatus.Idle: return SyncState.Stalled; default: throw new Error("Unreachable code"); } } /** Full debug state for lodestar API */ getSyncChainsDebugState() { return this.rangeSync.getSyncChainsDebugState(); } /** * A peer has connected which has blocks that are unknown to us. * * This function handles the logic associated with the connection of a new peer. If the peer * is sufficiently ahead of our current head, a range-sync (batch) sync is started and * batches of blocks are queued to download from the peer. Batched blocks begin at our latest * finalized head. * * If the peer is within the `SLOT_IMPORT_TOLERANCE`, then it's head is sufficiently close to * ours that we consider it fully sync'd with respect to our current chain. */ addPeer = (data) => { const localStatus = this.chain.getStatus(); const syncType = getPeerSyncType(localStatus, data.status, this.chain.forkChoice, this.slotImportTolerance); this.logger.verbose("Peer sync type classified", { peer: data.peer, syncType, localFinalizedEpoch: localStatus.finalizedEpoch, localFinalizedRoot: toRootHex(localStatus.finalizedRoot), localHeadSlot: localStatus.headSlot, localHeadRoot: toRootHex(localStatus.headRoot), remoteFinalizedEpoch: data.status.finalizedEpoch, remoteFinalizedRoot: toRootHex(data.status.finalizedRoot), remoteHeadSlot: data.status.headSlot, remoteHeadRoot: toRootHex(data.status.headRoot), }); // For metrics only this.peerSyncType.set(data.peer, syncType); if (syncType === PeerSyncType.Advanced) { this.rangeSync.addPeer(data.peer, localStatus, data.status); } this.updateSyncState(); }; /** * Must be called by libp2p when a peer is removed from the peer manager */ removePeer = (data) => { this.rangeSync.removePeer(data.peer); this.peerSyncType.delete(data.peer.toString()); }; /** * Run this function when the sync state can potentially change. */ updateSyncState = () => { const state = this.state; // Don't run the getter twice // We have become synced, subscribe to all the gossip core topics if (state === SyncState.Synced && this.chain.clock.currentEpoch >= MIN_EPOCH_TO_START_GOSSIP) { if (!this.network.isSubscribedToGossipCoreTopics()) { this.network .subscribeGossipCoreTopics() .then(() => { this.metrics?.syncSwitchGossipSubscriptions.inc({ action: "subscribed" }); this.logger.info("Subscribed gossip core topics"); }) .catch((e) => { this.logger.error("Error subscribing to gossip core topics", {}, e); }); } // also start searching for unknown blocks if (!this.unknownBlockSync.isSubscribedToNetwork()) { this.unknownBlockSync.subscribeToNetwork(); this.metrics?.blockInputSync.switchNetworkSubscriptions.inc({ action: "subscribed" }); } } // If we stopped being synced and fallen significantly behind, stop gossip else if (state !== SyncState.Synced) { const syncDiff = this.chain.clock.currentSlot - this.chain.forkChoice.getHead().slot; if (syncDiff > this.slotImportTolerance * 2) { if (this.network.isSubscribedToGossipCoreTopics()) { this.logger.warn(`Node sync has fallen behind by ${syncDiff} slots`); this.network .unsubscribeGossipCoreTopics() .then(() => { this.metrics?.syncSwitchGossipSubscriptions.inc({ action: "unsubscribed" }); this.logger.info("Un-subscribed gossip core topics"); }) .catch((e) => { this.logger.error("Error unsubscribing to gossip core topics", {}, e); }); } // also stop searching for unknown blocks if (this.unknownBlockSync.isSubscribedToNetwork()) { this.unknownBlockSync.unsubscribeFromNetwork(); this.metrics?.blockInputSync.switchNetworkSubscriptions.inc({ action: "unsubscribed" }); } } } }; onClockEpoch = () => { // If a node witness the genesis event consider starting gossip // Also, ensure that updateSyncState is run at least once per epoch. // If the chain gets stuck or very overloaded it could helps to resolve the situation // by realizing it's way behind and turning gossip off. this.updateSyncState(); }; scrapeMetrics(metrics) { // Compute current sync state metrics.syncStatus.set(syncStateMetric[this.state]); // Count peers by syncType const peerCountByType = { [PeerSyncType.Advanced]: 0, [PeerSyncType.FullySynced]: 0, [PeerSyncType.Behind]: 0, }; for (const syncType of this.peerSyncType.values()) { peerCountByType[syncType]++; } for (const syncType of peerSyncTypes) { metrics.syncPeersBySyncType.set({ syncType }, peerCountByType[syncType]); } } } //# sourceMappingURL=sync.js.map