@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
248 lines • 11.8 kB
JavaScript
import { EventEmitter } from "node:events";
import { computeStartSlotAtEpoch } from "@lodestar/state-transition";
import { toRootHex } from "@lodestar/utils";
import { AttestationImportOpt } from "../../chain/blocks/index.js";
import { beaconBlocksMaybeBlobsByRange } from "../../network/reqresp/beaconBlocksMaybeBlobsByRange.js";
import { RangeSyncType, getRangeSyncTarget, rangeSyncTypes } from "../utils/remoteSyncType.js";
import { SyncChain } from "./chain.js";
import { updateChains } from "./utils/index.js";
export var RangeSyncEvent;
(function (RangeSyncEvent) {
RangeSyncEvent["completedChain"] = "RangeSync-completedChain";
})(RangeSyncEvent || (RangeSyncEvent = {}));
export 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 {
constructor(modules, opts) {
super();
/** There is a single chain per type, 1 finalized sync, 1 head sync */
this.chains = new Map();
/** Convenience method for `SyncChain` */
this.processChainSegment = async (blocks, 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,
// we want to be safe to only persist blocks after verifying it to avoid any attacks that may cause our DB
// to grow too much
eagerPersistBlock: false,
};
if (this.opts?.disableProcessAsChainSegment) {
// Should only be used for debugging or testing
for (const block of blocks)
await this.chain.processBlock(block, flags);
}
else {
await this.chain.processChainSegment(blocks, flags);
}
};
/** Convenience method for `SyncChain` */
this.downloadBeaconBlocksByRange = async (peerId, request) => {
return beaconBlocksMaybeBlobsByRange(this.config, this.network, peerId, request, this.chain.clock.currentEpoch);
};
/** Convenience method for `SyncChain` */
this.reportPeer = (peer, action, actionName) => {
this.network.reportPeer(peer, action, actionName);
};
/** Convenience method for `SyncChain` */
this.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);
}
};
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.forkChoice);
this.logger.debug("Sync peer joined", {
peer: peerId,
syncType,
startEpoch,
targetSlot: target.slot,
targetRoot: toRootHex(target.root),
});
// 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
}
addPeerOrCreateChain(startEpoch, target, peer, syncType) {
let syncChain = this.chains.get(syncType);
if (!syncChain) {
syncChain = new SyncChain(startEpoch, target, syncType, {
processChainSegment: this.processChainSegment,
downloadBeaconBlocksByRange: this.downloadBeaconBlocksByRange,
reportPeer: this.reportPeer,
onEnd: this.onSyncChainEnd,
}, { config: this.config, logger: this.logger });
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),
});
}
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