@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
164 lines (147 loc) • 7.16 kB
text/typescript
import {IForkChoice} from "@lodestar/fork-choice";
import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
import {Slot, Status} from "@lodestar/types";
import {IBeaconChain} from "../../chain/interface.js";
import {ChainTarget} from "../range/utils/index.js";
/** The type of peer relative to our current state */
export enum PeerSyncType {
/** The peer is on our chain and is fully synced with respect to our chain */
FullySynced = "FullySynced",
/** The peer has a greater knowledge of the chain than us that warrants a full sync */
Advanced = "Advanced",
/** A peer is behind in the sync and not useful to us for downloading blocks */
Behind = "Behind",
}
// Cache Object.keys iteration for faster loops in metrics
export const peerSyncTypes = Object.keys(PeerSyncType) as PeerSyncType[];
function withinRangeOf(value: number, target: number, range: number): boolean {
return value >= target - range && value <= target + range;
}
export function getPeerSyncType(
local: Status,
remote: Status,
forkChoice: IForkChoice,
slotImportTolerance: number
): PeerSyncType {
// Aux vars: Inclusive boundaries of the range to consider a peer's head synced to ours.
const nearRangeStart = local.headSlot - slotImportTolerance;
const nearRangeEnd = local.headSlot + slotImportTolerance;
if (remote.finalizedEpoch < local.finalizedEpoch) {
// The node has a lower finalized epoch, their chain is not useful to us. There are two
// cases where a node can have a lower finalized epoch:
//
// ## The node is on the same chain
//
// If a node is on the same chain but has a lower finalized epoch, their head must be
// lower than ours. Therefore, we have nothing to request from them.
//
// ## The node is on a fork
//
// If a node is on a fork that has a lower finalized epoch, switching to that fork would
// cause us to revert a finalized block. This is not permitted, therefore we have no
// interest in their blocks.
//
// We keep these peers to allow them to sync from us.
return PeerSyncType.Behind;
}
if (remote.finalizedEpoch > local.finalizedEpoch) {
if (
// Peer is in next epoch, and head is within range => SYNCED
(local.finalizedEpoch + 1 === remote.finalizedEpoch &&
withinRangeOf(remote.headSlot, local.headSlot, slotImportTolerance)) ||
// Peer's head is known => SYNCED
forkChoice.hasBlock(remote.headRoot)
) {
return PeerSyncType.FullySynced;
}
return PeerSyncType.Advanced;
}
// remote.finalizedEpoch == local.finalizedEpoch
// NOTE: if a peer has our same `finalizedEpoch` with a different `finalized_root`
// they are not considered relevant and won't be propagated to sync.
// Check if the peer is the peer is inside the tolerance range to be considered synced.
if (remote.headSlot < nearRangeStart) {
return PeerSyncType.Behind;
}
if (remote.headSlot > nearRangeEnd && !forkChoice.hasBlock(remote.headRoot)) {
// This peer has a head ahead enough of ours and we have no knowledge of their best block.
return PeerSyncType.Advanced;
}
// This peer is either in the tolerance range, or ahead us with an already rejected block.
return PeerSyncType.FullySynced;
}
export enum RangeSyncType {
/** A finalized chain sync should be started with this peer */
Finalized = "Finalized",
/** A head chain sync should be started with this peer */
Head = "Head",
}
// Cache Object.keys iteration for faster loops in metrics
export const rangeSyncTypes = Object.keys(RangeSyncType) as RangeSyncType[];
/**
* Check if a peer requires a finalized chain sync. Only if:
* - The remotes finalized epoch is greater than our current finalized epoch and we have
* not seen the finalized hash before
*/
export function getRangeSyncType(local: Status, remote: Status, forkChoice: IForkChoice): RangeSyncType {
if (remote.finalizedEpoch > local.finalizedEpoch && !forkChoice.hasBlock(remote.finalizedRoot)) {
return RangeSyncType.Finalized;
}
return RangeSyncType.Head;
}
export function getRangeSyncTarget(
local: Status,
remote: Status,
chain: IBeaconChain
): {syncType: RangeSyncType; startEpoch: Slot; target: ChainTarget} {
const forkChoice = chain.forkChoice;
// finalized sync
if (remote.finalizedEpoch > local.finalizedEpoch && !forkChoice.hasBlock(remote.finalizedRoot)) {
return {
// If RangeSyncType.Finalized, the range of blocks fetchable from startEpoch and target must allow to switch
// to RangeSyncType.Head
//
// finalizedRoot is a block with slot <= computeStartSlotAtEpoch(finalizedEpoch).
// If finalizedEpoch does not start with a skipped slot, the SyncChain with this target MUST process the
// first block of the next epoch in order to trigger the condition above `forkChoice.hasBlock(remote.finalizedRoot)`
// and do a Head sync.
//
// When doing a finalized sync, we'll process blocks up to the finalized checkpoint, which does not allow to
// finalize that checkpoint. Instead, our head will be the finalized checkpoint and our finalized checkpoint will
// be some older checkpoint. After completing a finalized SyncChain:
//
// (== finalized, -- non-finalized)
// Remote ====================================================|----------------|
// Local =====================================|--------------|
syncType: RangeSyncType.Finalized,
startEpoch: local.finalizedEpoch,
target: {
slot: computeStartSlotAtEpoch(remote.finalizedEpoch),
root: remote.finalizedRoot,
},
};
}
// we don't want to sync from epoch < minEpoch
// if we boot from an unfinalized checkpoint state, we don't want to sync before anchorStateLatestBlockSlot
// if we boot from a finalized checkpoint state, anchorStateLatestBlockSlot is trusted and we also don't want to sync before it
const minEpoch = Math.max(remote.finalizedEpoch, computeEpochAtSlot(chain.anchorStateLatestBlockSlot));
// head sync
return {
syncType: RangeSyncType.Head,
// The new peer has the same finalized `remote.finalizedEpoch == local.finalizedEpoch` since
// previous filters should prevent a peer with an earlier finalized chain from reaching here.
//
// By default and during stable network conditions, the head sync always starts from
// the finalized epoch (even though it's the head sync) because finalized epoch is < local head.
// This is to prevent the issue noted here https://github.com/ChainSafe/lodestar/pull/7509#discussion_r1984353063.
//
// During non-finality of the network, when starting from an unfinalized checkpoint state, we don't want
// to sync before anchorStateLatestBlockSlot as finalized epoch is too far away. Local head will also be
// the same to that value at startup, the head sync always starts from anchorStateLatestBlockSlot in this case.
startEpoch: Math.min(computeEpochAtSlot(local.headSlot), minEpoch),
target: {
slot: remote.headSlot,
root: remote.headRoot,
},
};
}