@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
191 lines (163 loc) • 6.85 kB
text/typescript
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;
}
}