UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

191 lines (163 loc) 6.85 kB
import {isForkPostFulu} from "@lodestar/params"; import {PeerSyncMeta} from "../../../network/peers/peersData.js"; import {CustodyConfig} from "../../../util/dataColumns.js"; import {PeerIdStr} from "../../../util/peerId.js"; import {shuffle} from "../../../util/shuffle.js"; import {sortBy} from "../../../util/sortBy.js"; import {MAX_CONCURRENT_REQUESTS} from "../../constants.js"; import {RangeSyncType} from "../../utils/remoteSyncType.js"; import {Batch, BatchStatus} from "../batch.js"; import {ChainTarget} from "./chainTarget.js"; export type PeerSyncInfo = PeerSyncMeta & { target: ChainTarget; }; type PeerInfoColumn = {syncInfo: PeerSyncInfo; columns: number; hasEarliestAvailableSlots: boolean}; /** * Balance and organize peers to perform requests with a SyncChain * Shuffles peers only once on instantiation */ export class ChainPeersBalancer { private peers: PeerSyncInfo[]; private activeRequestsByPeer = new Map<PeerIdStr, number>(); private readonly custodyConfig: CustodyConfig; private readonly syncType: RangeSyncType; private readonly maxConcurrentRequests: number; /** * No need to specify `maxConcurrentRequests` for production code * It is used for testing purposes to limit the number of concurrent requests */ constructor( peers: PeerSyncInfo[], batches: Batch[], custodyConfig: CustodyConfig, syncType: RangeSyncType, maxConcurrentRequests = MAX_CONCURRENT_REQUESTS ) { this.peers = shuffle(peers); this.custodyConfig = custodyConfig; this.syncType = syncType; this.maxConcurrentRequests = maxConcurrentRequests; // Compute activeRequestsByPeer from all batches internal states for (const batch of batches) { if (batch.state.status === BatchStatus.Downloading) { this.activeRequestsByPeer.set(batch.state.peer, (this.activeRequestsByPeer.get(batch.state.peer) ?? 0) + 1); } } } /** * Return the most suitable peer to retry * Sort peers by (1) less active requests (2) most columns we need. * Peers that failed this batch or already succeeded for the same request are excluded inside `filterPeers`. */ bestPeerToRetryBatch(batch: Batch): PeerSyncMeta | undefined { if (batch.state.status !== BatchStatus.AwaitingDownload) { return; } const {columnsRequest} = batch.requests; // TODO(fulu): This is fulu specific and hinders our peer selection PreFulu const pendingDataColumns = columnsRequest?.columns ?? this.custodyConfig.sampledColumns; const eligiblePeers = this.filterPeers(batch, pendingDataColumns, false); const sortedBestPeers = sortBy( eligiblePeers, ({syncInfo}) => this.activeRequestsByPeer.get(syncInfo.peerId) ?? 0, // prefer peers with least active req ({columns}) => -1 * columns // prefer peers with the most columns ); if (sortedBestPeers.length > 0) { const bestPeer = sortedBestPeers[0]; // we will use this peer for batch in SyncChain right after this call this.activeRequestsByPeer.set( bestPeer.syncInfo.peerId, (this.activeRequestsByPeer.get(bestPeer.syncInfo.peerId) ?? 0) + 1 ); return bestPeer.syncInfo; } return undefined; } /** * Return peers with 0 or no active requests that has a higher target slot than this batch and has columns we need. */ idlePeerForBatch(batch: Batch): PeerSyncInfo | undefined { if (batch.state.status !== BatchStatus.AwaitingDownload) { return; } const eligiblePeers = this.filterPeers(batch, this.custodyConfig.sampledColumns, true); // pick idle peer that has (for pre-fulu they are the same) // - earliestAvailableSlot defined // - the most columns we need const sortedBestPeers = sortBy( eligiblePeers, ({columns}) => -1 * columns // prefer peers with most columns we need ); const bestPeer = sortedBestPeers[0]; if (bestPeer != null) { // we will use this peer for batch in SyncChain right after this call this.activeRequestsByPeer.set(bestPeer.syncInfo.peerId, 1); return bestPeer.syncInfo; } return undefined; } private filterPeers(batch: Batch, requestColumns: number[], noActiveRequest: boolean): PeerInfoColumn[] { const eligiblePeers: PeerInfoColumn[] = []; if (batch.state.status !== BatchStatus.AwaitingDownload) { return eligiblePeers; } // Skip peers that failed this batch, or that already returned the exact current request shape. const failedPeers = new Set<PeerIdStr>(batch.getFailedPeers()); for (const peer of this.peers) { const {earliestAvailableSlot, target, peerId} = peer; if (failedPeers.has(peerId) || batch.hasPeerSucceededCurrentRequest(peer)) { continue; } const activeRequest = this.activeRequestsByPeer.get(peerId) ?? 0; if (noActiveRequest && activeRequest > 0) { // consumer wants to find peer with no active request, but this peer has active request continue; } if (activeRequest >= this.maxConcurrentRequests) { // consumer wants to find peer with no more than MAX_CONCURRENT_REQUESTS active requests continue; } if (target.slot < batch.startSlot) { continue; } if (isForkPostFulu(batch.forkName) && this.syncType === RangeSyncType.Head) { // for head sync, target slot is head slot and each peer may have a different head slot // we don't want to retry a batch with a peer that's not as up-to-date as the previous peer // see https://github.com/ChainSafe/lodestar/issues/8193 const blocks = batch.state?.blocks; const lastBlock = blocks?.at(-1); const lastBlockSlot = lastBlock?.slot; if (lastBlockSlot && lastBlockSlot > target.slot) { continue; } } if (!isForkPostFulu(batch.forkName)) { // pre-fulu logic, we don't care columns and earliestAvailableSlot eligiblePeers.push({syncInfo: peer, columns: 0, hasEarliestAvailableSlots: false}); continue; } // we don't accept peers without earliestAvailableSlot because it may return 0 blocks and we get stuck // see https://github.com/ChainSafe/lodestar/issues/8147 if (earliestAvailableSlot == null) { continue; } if (earliestAvailableSlot > batch.startSlot) { continue; } const columns = peer.custodyColumns.reduce((acc, elem) => { if (requestColumns.includes(elem)) { acc.push(elem); } return acc; }, [] as number[]); if (columns.length > 0) { eligiblePeers.push({ syncInfo: peer, columns: columns.length, hasEarliestAvailableSlots: earliestAvailableSlot != null, }); } } return eligiblePeers; } }