@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
303 lines (269 loc) • 11.9 kB
text/typescript
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {Slot} from "@lodestar/types";
import {Logger, toRootHex} from "@lodestar/utils";
import {IBeaconChain} from "../chain/index.js";
import {GENESIS_SLOT} from "../constants/constants.js";
import {ExecutionEngineState} from "../execution/index.js";
import {Metrics} from "../metrics/index.js";
import {INetwork, NetworkEvent, NetworkEventData} 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 {IBeaconSync, SyncChainDebugState, SyncModules, SyncState, SyncingStatus, syncStateMetric} from "./interface.js";
import {SyncOptions} from "./options.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 implements IBeaconSync {
private readonly logger: Logger;
private readonly network: INetwork;
private readonly chain: IBeaconChain;
private readonly metrics: Metrics | null;
private readonly opts: SyncOptions;
private readonly rangeSync: RangeSync;
private readonly unknownBlockSync: BlockInputSync;
/** For metrics only */
private readonly peerSyncType = new Map<string, PeerSyncType>();
private readonly slotImportTolerance: Slot;
constructor(opts: SyncOptions, modules: SyncModules) {
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 = (): void => {
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(): void {
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(): SyncingStatus {
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(): boolean {
const state = this.state; // Don't run the getter twice
return state === SyncState.SyncingFinalized || state === SyncState.SyncingHead;
}
isSynced(): boolean {
return this.state === SyncState.Synced;
}
get state(): SyncState {
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(): SyncChainDebugState[] {
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.
*/
private addPeer = (data: NetworkEventData[NetworkEvent.peerConnected]): void => {
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
*/
private removePeer = (data: NetworkEventData[NetworkEvent.peerDisconnected]): void => {
this.rangeSync.removePeer(data.peer);
this.peerSyncType.delete(data.peer.toString());
};
/**
* Run this function when the sync state can potentially change.
*/
private updateSyncState = (): void => {
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"});
}
}
}
};
private onClockEpoch = (): void => {
// 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();
};
private scrapeMetrics(metrics: Metrics): void {
// Compute current sync state
metrics.syncStatus.set(syncStateMetric[this.state]);
// Count peers by syncType
const peerCountByType: Record<PeerSyncType, number> = {
[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]);
}
}
}