UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

143 lines 6.73 kB
import { isForkPostFulu } from "@lodestar/params"; 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 { BatchStatus } from "../batch.js"; /** * Balance and organize peers to perform requests with a SyncChain * Shuffles peers only once on instantiation */ export class ChainPeersBalancer { peers; activeRequestsByPeer = new Map(); custodyConfig; syncType; maxConcurrentRequests; /** * No need to specify `maxConcurrentRequests` for production code * It is used for testing purposes to limit the number of concurrent requests */ constructor(peers, batches, custodyConfig, syncType, 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) { 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) { 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; } filterPeers(batch, requestColumns, noActiveRequest) { const eligiblePeers = []; 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(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; }, []); if (columns.length > 0) { eligiblePeers.push({ syncInfo: peer, columns: columns.length, hasEarliestAvailableSlots: earliestAvailableSlot != null, }); } } return eligiblePeers; } } //# sourceMappingURL=peerBalancer.js.map