UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

853 lines (736 loc) • 32 kB
import type {PeerScoreStatsDump} from "@libp2p/gossipsub/score"; import type {PublishOpts} from "@libp2p/gossipsub/types"; import type {PeerId, PrivateKey} from "@libp2p/interface"; import {peerIdFromPrivateKey} from "@libp2p/peer-id"; import {routes} from "@lodestar/api"; import {BeaconConfig} from "@lodestar/config"; import {LoggerNode} from "@lodestar/logger/node"; import {ForkSeq} from "@lodestar/params"; import {ResponseIncoming} from "@lodestar/reqresp"; import {computeEpochAtSlot} from "@lodestar/state-transition"; import { AttesterSlashing, DataColumnSidecar, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, Root, SignedAggregateAndProof, SignedBeaconBlock, SingleAttestation, SlotRootHex, SubnetID, altair, capella, deneb, fulu, gloas, isGloasDataColumnSidecar, phase0, } from "@lodestar/types"; import {prettyPrintIndices, sleep} from "@lodestar/utils"; import {BlockInputSource} from "../chain/blocks/blockInput/types.js"; import {ChainEvent, IBeaconChain} from "../chain/index.js"; import {computeSubnetForDataColumnSidecar} from "../chain/validation/dataColumnSidecar.js"; import {IBeaconDb} from "../db/interface.js"; import {Metrics, RegistryMetricCreator} from "../metrics/index.js"; import {IClock} from "../util/clock.js"; import {CustodyConfig} from "../util/dataColumns.js"; import {PeerIdStr, peerIdToString} from "../util/peerId.js"; import {promiseAllMaybeAsync} from "../util/promises.js"; import { BeaconBlocksByRootRequest, BlobSidecarsByRootRequest, DataColumnSidecarsByRootRequest, ExecutionPayloadEnvelopesByRootRequest, } from "../util/types.js"; import {INetworkCore, NetworkCore, WorkerNetworkCore} from "./core/index.js"; import {INetworkEventBus, NetworkEvent, NetworkEventBus, NetworkEventData} from "./events.js"; import {getActiveForkBoundaries} from "./forks.js"; import {GossipHandlers, GossipTopicMap, GossipType, GossipTypeMap} from "./gossip/index.js"; import {getGossipSSZType, gossipTopicIgnoreDuplicatePublishError, stringifyGossipTopic} from "./gossip/topic.js"; import {INetwork} from "./interface.js"; import {NetworkOptions} from "./options.js"; import {PeerAction, PeerScoreStats} from "./peers/index.js"; import {PeerSyncMeta} from "./peers/peersData.js"; import {AggregatorTracker} from "./processor/aggregatorTracker.js"; import {NetworkProcessor, PendingGossipsubMessage} from "./processor/index.js"; import {ReqRespMethod} from "./reqresp/index.js"; import {GetReqRespHandlerFn, Version, requestSszTypeByMethod, responseSszTypeByMethod} from "./reqresp/types.js"; import { collectExactOneTyped, collectMaxResponseTyped, collectMaxResponseTypedWithBytes, } from "./reqresp/utils/collect.js"; import {collectSequentialBlocksInRange} from "./reqresp/utils/collectSequentialBlocksInRange.js"; import {CommitteeSubscription} from "./subnets/index.js"; import {isPublishToZeroPeersError, prettyPrintPeerIdStr} from "./util.js"; type NetworkModules = { opts: NetworkOptions; privateKey: PrivateKey; config: BeaconConfig; logger: LoggerNode; chain: IBeaconChain; networkEventBus: NetworkEventBus; aggregatorTracker: AggregatorTracker; networkProcessor: NetworkProcessor; core: INetworkCore; }; export type NetworkInitModules = { opts: NetworkOptions; config: BeaconConfig; privateKey: PrivateKey; peerStoreDir?: string; logger: LoggerNode; metrics: Metrics | null; chain: IBeaconChain; db: IBeaconDb; getReqRespHandler: GetReqRespHandlerFn; // Optionally pass custom GossipHandlers, for testing gossipHandlers?: GossipHandlers; }; /** * Must support running both on worker and on main thread. * * Exists a front class that's what consumers interact with. * This class will multiplex between: * - libp2p in worker * - libp2p in main thread */ export class Network implements INetwork { readonly peerId: PeerId; readonly custodyConfig: CustodyConfig; // TODO: Make private readonly events: INetworkEventBus; private readonly logger: LoggerNode; private readonly config: BeaconConfig; private readonly clock: IClock; private readonly chain: IBeaconChain; // Used only for sleep() statements private readonly controller: AbortController; // TODO: Review private readonly networkProcessor: NetworkProcessor; private readonly core: INetworkCore; private readonly aggregatorTracker: AggregatorTracker; private subscribedToCoreTopics = false; private connectedPeersSyncMeta = new Map<PeerIdStr, Omit<PeerSyncMeta, "peerId">>(); constructor(modules: NetworkModules) { this.peerId = peerIdFromPrivateKey(modules.privateKey); this.config = modules.config; this.custodyConfig = modules.chain.custodyConfig; this.logger = modules.logger; this.chain = modules.chain; this.clock = modules.chain.clock; this.controller = new AbortController(); this.events = modules.networkEventBus; this.networkProcessor = modules.networkProcessor; this.core = modules.core; this.aggregatorTracker = modules.aggregatorTracker; this.events.on(NetworkEvent.peerConnected, this.onPeerConnected); this.events.on(NetworkEvent.peerDisconnected, this.onPeerDisconnected); this.chain.emitter.on(routes.events.EventType.head, this.onHead); this.chain.emitter.on(routes.events.EventType.lightClientFinalityUpdate, ({data}) => this.onLightClientFinalityUpdate(data) ); this.chain.emitter.on(routes.events.EventType.lightClientOptimisticUpdate, ({data}) => this.onLightClientOptimisticUpdate(data) ); this.chain.emitter.on(ChainEvent.updateTargetCustodyGroupCount, this.onTargetGroupCountUpdated); this.chain.emitter.on(ChainEvent.publishDataColumns, this.onPublishDataColumns); this.chain.emitter.on(ChainEvent.publishBlobSidecars, this.onPublishBlobSidecars); this.chain.emitter.on(ChainEvent.updateStatus, this.onUpdateStatus); } static async init({ opts, config, logger, metrics, chain, db, gossipHandlers, privateKey, peerStoreDir, getReqRespHandler, }: NetworkInitModules): Promise<Network> { const events = new NetworkEventBus(); const aggregatorTracker = new AggregatorTracker(); const activeValidatorCount = chain.getHeadState().activeValidatorCount; const initialStatus = chain.getStatus(); const initialCustodyGroupCount = chain.custodyConfig.targetCustodyGroupCount; if (opts.useWorker) { logger.info("running libp2p instance in worker thread"); } const core = opts.useWorker ? await WorkerNetworkCore.init({ opts: { ...opts, peerStoreDir, metricsEnabled: Boolean(metrics), activeValidatorCount, genesisTime: chain.genesisTime, initialStatus, initialCustodyGroupCount, }, config, privateKey, logger, events, metrics, getReqRespHandler, }) : await NetworkCore.init({ opts, config, privateKey, peerStoreDir, logger, clock: chain.clock, events, getReqRespHandler, metricsRegistry: metrics ? new RegistryMetricCreator() : null, initialStatus, initialCustodyGroupCount, activeValidatorCount, }); const networkProcessor = new NetworkProcessor( {chain, db, config, logger, metrics, events, gossipHandlers, core, aggregatorTracker}, opts ); const multiaddresses = opts.localMultiaddrs?.join(","); const peerId = peerIdFromPrivateKey(privateKey); logger.info(`PeerId ${peerIdToString(peerId)}, Multiaddrs ${multiaddresses}`); return new Network({ opts, privateKey, config, logger, chain, networkEventBus: events, aggregatorTracker, networkProcessor, core, }); } get closed(): boolean { return this.controller.signal.aborted; } /** Destroy this instance. Can only be called once. */ async close(): Promise<void> { if (this.closed) return; this.events.off(NetworkEvent.peerConnected, this.onPeerConnected); this.events.off(NetworkEvent.peerDisconnected, this.onPeerDisconnected); this.chain.emitter.off(routes.events.EventType.head, this.onHead); this.chain.emitter.off(routes.events.EventType.lightClientFinalityUpdate, this.onLightClientFinalityUpdate); this.chain.emitter.off(routes.events.EventType.lightClientOptimisticUpdate, this.onLightClientOptimisticUpdate); this.chain.emitter.off(ChainEvent.updateTargetCustodyGroupCount, this.onTargetGroupCountUpdated); this.chain.emitter.off(ChainEvent.publishDataColumns, this.onPublishDataColumns); this.chain.emitter.off(ChainEvent.publishBlobSidecars, this.onPublishBlobSidecars); this.chain.emitter.off(ChainEvent.updateStatus, this.onUpdateStatus); await this.core.close(); // Used only for sleep() statements this.controller.abort(); this.logger.debug("network core closed"); } async scrapeMetrics(): Promise<string> { return this.core.scrapeMetrics(); } /** * Request att subnets up `toSlot`. Network will ensure to mantain some peers for each */ async prepareBeaconCommitteeSubnets(subscriptions: CommitteeSubscription[]): Promise<void> { for (const subscription of subscriptions) { if (subscription.isAggregator) { this.aggregatorTracker.addAggregator(subscription.subnet, subscription.slot); } } this.aggregatorTracker.prune(); return this.core.prepareBeaconCommitteeSubnets(subscriptions); } async prepareSyncCommitteeSubnets(subscriptions: CommitteeSubscription[]): Promise<void> { return this.core.prepareSyncCommitteeSubnets(subscriptions); } /** * The app layer needs to refresh the status of some peers. The sync have reached a target */ async reStatusPeers(peers: PeerIdStr[]): Promise<void> { return this.core.reStatusPeers(peers); } searchUnknownBlock(slotRoot: SlotRootHex, source: BlockInputSource, peer?: PeerIdStr): void { this.networkProcessor.searchUnknownBlock(slotRoot, source, peer); } searchUnknownEnvelope(slotRoot: SlotRootHex, source: BlockInputSource, peer?: PeerIdStr): void { this.networkProcessor.searchUnknownEnvelope(slotRoot, source, peer); } async reportPeer(peer: PeerIdStr, action: PeerAction, actionName: string): Promise<void> { return this.core.reportPeer(peer, action, actionName); } // REST API queries getConnectedPeers(): PeerIdStr[] { return Array.from(this.connectedPeersSyncMeta.keys()); } getConnectedPeerSyncMeta(peerId: PeerIdStr): PeerSyncMeta { const syncMeta = this.connectedPeersSyncMeta.get(peerId); if (!syncMeta) { throw new Error(`peerId=${prettyPrintPeerIdStr(peerId)} not in connectedPeerSyncMeta`); } return {peerId, ...syncMeta}; } getConnectedPeerCount(): number { return this.connectedPeersSyncMeta.size; } async getNetworkIdentity(): Promise<routes.node.NetworkIdentity> { return this.core.getNetworkIdentity(); } /** * Subscribe to all gossip events. Safe to call multiple times */ async subscribeGossipCoreTopics(): Promise<void> { if (!this.subscribedToCoreTopics) { await this.core.subscribeGossipCoreTopics(); // Only mark subscribedToCoreTopics if worker resolved this call this.subscribedToCoreTopics = true; } } /** * Unsubscribe from all gossip events. Safe to call multiple times */ async unsubscribeGossipCoreTopics(): Promise<void> { // Drop all the gossip validation queues this.networkProcessor.dropAllJobs(); await this.core.unsubscribeGossipCoreTopics(); this.subscribedToCoreTopics = false; } isSubscribedToGossipCoreTopics(): boolean { return this.subscribedToCoreTopics; } shouldAggregate(subnet: SubnetID, slot: number): boolean { return this.aggregatorTracker.shouldAggregate(subnet, slot); } // Gossip async publishBeaconBlock(signedBlock: SignedBeaconBlock): Promise<number> { const epoch = computeEpochAtSlot(signedBlock.message.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.beacon_block>({type: GossipType.beacon_block, boundary}, signedBlock, { ignoreDuplicatePublishError: true, }); } async publishBlobSidecar(blobSidecar: deneb.BlobSidecar): Promise<number> { const epoch = computeEpochAtSlot(blobSidecar.signedBlockHeader.message.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); const subnet = blobSidecar.index; return this.publishGossip<GossipType.blob_sidecar>({type: GossipType.blob_sidecar, boundary, subnet}, blobSidecar, { ignoreDuplicatePublishError: true, }); } async publishDataColumnSidecar(dataColumnSidecar: DataColumnSidecar): Promise<number> { const slot = isGloasDataColumnSidecar(dataColumnSidecar) ? dataColumnSidecar.slot : dataColumnSidecar.signedBlockHeader.message.slot; const epoch = computeEpochAtSlot(slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); const subnet = computeSubnetForDataColumnSidecar(this.config, dataColumnSidecar); return this.publishGossip<GossipType.data_column_sidecar>( {type: GossipType.data_column_sidecar, boundary, subnet}, dataColumnSidecar, { ignoreDuplicatePublishError: true, // we ensure having all topic peers via prioritizePeers() function // in the worse case, if there is 0 peer on the topic, the overall publish operation could be still a success // because supernode will rebuild and publish missing data column sidecars for us // hence we want to track sent peers as 0 instead of an error allowPublishToZeroTopicPeers: true, } ); } async publishBeaconAggregateAndProof(aggregateAndProof: SignedAggregateAndProof): Promise<number> { const epoch = computeEpochAtSlot(aggregateAndProof.message.aggregate.data.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.beacon_aggregate_and_proof>( {type: GossipType.beacon_aggregate_and_proof, boundary}, aggregateAndProof, {ignoreDuplicatePublishError: true} ); } async publishBeaconAttestation(attestation: SingleAttestation, subnet: SubnetID): Promise<number> { const epoch = computeEpochAtSlot(attestation.data.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.beacon_attestation>( {type: GossipType.beacon_attestation, boundary, subnet}, attestation, {ignoreDuplicatePublishError: true} ); } async publishVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise<number> { const epoch = voluntaryExit.message.epoch; const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.voluntary_exit>({type: GossipType.voluntary_exit, boundary}, voluntaryExit, { ignoreDuplicatePublishError: true, }); } async publishBlsToExecutionChange(blsToExecutionChange: capella.SignedBLSToExecutionChange): Promise<number> { const publishChanges = []; for (const boundary of getActiveForkBoundaries(this.config, this.clock.currentEpoch)) { const fork = ForkSeq[boundary.fork]; if (fork >= ForkSeq.capella) { const publishPromise = this.publishGossip<GossipType.bls_to_execution_change>( {type: GossipType.bls_to_execution_change, boundary}, blsToExecutionChange, {ignoreDuplicatePublishError: true} ); publishChanges.push(publishPromise); } } if (publishChanges.length === 0) { throw Error("No capella+ fork active yet to publish blsToExecutionChange"); } return Promise.any(publishChanges); } async publishProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise<number> { const epoch = computeEpochAtSlot(Number(proposerSlashing.signedHeader1.message.slot as bigint)); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.proposer_slashing>( {type: GossipType.proposer_slashing, boundary}, proposerSlashing ); } async publishAttesterSlashing(attesterSlashing: AttesterSlashing): Promise<number> { const epoch = computeEpochAtSlot(Number(attesterSlashing.attestation1.data.slot as bigint)); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.attester_slashing>( {type: GossipType.attester_slashing, boundary}, attesterSlashing ); } async publishSyncCommitteeSignature(signature: altair.SyncCommitteeMessage, subnet: SubnetID): Promise<number> { const epoch = computeEpochAtSlot(signature.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.sync_committee>( {type: GossipType.sync_committee, boundary, subnet}, signature, { ignoreDuplicatePublishError: true, } ); } async publishContributionAndProof(contributionAndProof: altair.SignedContributionAndProof): Promise<number> { const epoch = computeEpochAtSlot(contributionAndProof.message.contribution.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.sync_committee_contribution_and_proof>( {type: GossipType.sync_committee_contribution_and_proof, boundary}, contributionAndProof, {ignoreDuplicatePublishError: true} ); } async publishLightClientFinalityUpdate(update: LightClientFinalityUpdate): Promise<number> { const epoch = computeEpochAtSlot(update.signatureSlot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.light_client_finality_update>( {type: GossipType.light_client_finality_update, boundary}, update ); } async publishLightClientOptimisticUpdate(update: LightClientOptimisticUpdate): Promise<number> { const epoch = computeEpochAtSlot(update.signatureSlot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.light_client_optimistic_update>( {type: GossipType.light_client_optimistic_update, boundary}, update ); } async publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise<number> { const epoch = computeEpochAtSlot(signedEnvelope.message.payload.slotNumber); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.execution_payload>( {type: GossipType.execution_payload, boundary}, signedEnvelope, {ignoreDuplicatePublishError: true} ); } async publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise<number> { const epoch = computeEpochAtSlot(payloadAttestationMessage.data.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip<GossipType.payload_attestation_message>( {type: GossipType.payload_attestation_message, boundary}, payloadAttestationMessage, {ignoreDuplicatePublishError: true} ); } private async publishGossip<K extends GossipType>( topic: GossipTopicMap[K], object: GossipTypeMap[K], opts?: PublishOpts | undefined ): Promise<number> { const topicStr = stringifyGossipTopic(this.config, topic); const sszType = getGossipSSZType(topic); const messageData = (sszType.serialize as (object: GossipTypeMap[GossipType]) => Uint8Array)(object); opts = { ...opts, ignoreDuplicatePublishError: gossipTopicIgnoreDuplicatePublishError[topic.type], }; const sentPeers = await this.core.publishGossip(topicStr, messageData, opts); this.logger.verbose("Publish to topic", {topic: topicStr, sentPeers, currentSlot: this.clock.currentSlot}); return sentPeers; } // ReqResp async sendBeaconBlocksByRange( peerId: PeerIdStr, request: phase0.BeaconBlocksByRangeRequest ): Promise<SignedBeaconBlock[]> { return collectSequentialBlocksInRange( this.sendReqRespRequest( peerId, ReqRespMethod.BeaconBlocksByRange, // Before altair, prioritize V2. After altair only request V2 this.config.getForkSeq(this.clock.currentSlot) >= ForkSeq.altair ? [Version.V2] : [Version.V2, Version.V1], request ), request, this.chain.serializedCache ); } async sendBeaconBlocksByRoot(peerId: PeerIdStr, request: BeaconBlocksByRootRequest): Promise<SignedBeaconBlock[]> { return collectMaxResponseTypedWithBytes( this.sendReqRespRequest( peerId, ReqRespMethod.BeaconBlocksByRoot, // Before altair, prioritize V2. After altair only request V2 this.config.getForkSeq(this.clock.currentSlot) >= ForkSeq.altair ? [Version.V2] : [Version.V2, Version.V1], request ), request.length, responseSszTypeByMethod[ReqRespMethod.BeaconBlocksByRoot], this.chain.serializedCache ); } async sendBeaconBlocksByHead( peerId: PeerIdStr, request: fulu.BeaconBlocksByHeadRequest ): Promise<SignedBeaconBlock[]> { return collectMaxResponseTypedWithBytes( this.sendReqRespRequest(peerId, ReqRespMethod.BeaconBlocksByHead, [Version.V1], request), Math.min(request.count, this.config.MAX_REQUEST_BLOCKS_DENEB), responseSszTypeByMethod[ReqRespMethod.BeaconBlocksByHead], this.chain.serializedCache ); } async sendLightClientBootstrap(peerId: PeerIdStr, request: Root): Promise<LightClientBootstrap> { return collectExactOneTyped( this.sendReqRespRequest(peerId, ReqRespMethod.LightClientBootstrap, [Version.V1], request), responseSszTypeByMethod[ReqRespMethod.LightClientBootstrap] ); } async sendLightClientOptimisticUpdate(peerId: PeerIdStr): Promise<LightClientOptimisticUpdate> { return collectExactOneTyped( this.sendReqRespRequest(peerId, ReqRespMethod.LightClientOptimisticUpdate, [Version.V1], null), responseSszTypeByMethod[ReqRespMethod.LightClientOptimisticUpdate] ); } async sendLightClientFinalityUpdate(peerId: PeerIdStr): Promise<LightClientFinalityUpdate> { return collectExactOneTyped( this.sendReqRespRequest(peerId, ReqRespMethod.LightClientFinalityUpdate, [Version.V1], null), responseSszTypeByMethod[ReqRespMethod.LightClientFinalityUpdate] ); } async sendLightClientUpdatesByRange( peerId: PeerIdStr, request: altair.LightClientUpdatesByRange ): Promise<LightClientUpdate[]> { return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.LightClientUpdatesByRange, [Version.V1], request), request.count, responseSszTypeByMethod[ReqRespMethod.LightClientUpdatesByRange] ); } async sendBlobSidecarsByRange( peerId: PeerIdStr, request: deneb.BlobSidecarsByRangeRequest ): Promise<deneb.BlobSidecar[]> { const epoch = computeEpochAtSlot(request.startSlot); return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.BlobSidecarsByRange, [Version.V1], request), // request's count represent the slots, so the actual max count received could be slots * blobs per slot request.count * this.config.getMaxBlobsPerBlock(epoch), responseSszTypeByMethod[ReqRespMethod.BlobSidecarsByRange] ); } async sendBlobSidecarsByRoot(peerId: PeerIdStr, request: BlobSidecarsByRootRequest): Promise<deneb.BlobSidecar[]> { return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.BlobSidecarsByRoot, [Version.V1], request), request.length, responseSszTypeByMethod[ReqRespMethod.BlobSidecarsByRoot], this.chain.serializedCache ); } async sendDataColumnSidecarsByRange( peerId: PeerIdStr, request: fulu.DataColumnSidecarsByRangeRequest ): Promise<DataColumnSidecar[]> { return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.DataColumnSidecarsByRange, [Version.V1], request), request.count * request.columns.length, responseSszTypeByMethod[ReqRespMethod.DataColumnSidecarsByRange] ); } async sendDataColumnSidecarsByRoot( peerId: PeerIdStr, request: DataColumnSidecarsByRootRequest ): Promise<DataColumnSidecar[]> { return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.DataColumnSidecarsByRoot, [Version.V1], request), request.reduce((total, {columns}) => total + columns.length, 0), responseSszTypeByMethod[ReqRespMethod.DataColumnSidecarsByRoot], this.chain.serializedCache ); } async sendExecutionPayloadEnvelopesByRange( peerId: PeerIdStr, request: gloas.ExecutionPayloadEnvelopesByRangeRequest ): Promise<gloas.SignedExecutionPayloadEnvelope[]> { return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.ExecutionPayloadEnvelopesByRange, [Version.V1], request), request.count, responseSszTypeByMethod[ReqRespMethod.ExecutionPayloadEnvelopesByRange] ); } async sendExecutionPayloadEnvelopesByRoot( peerId: PeerIdStr, request: ExecutionPayloadEnvelopesByRootRequest ): Promise<gloas.SignedExecutionPayloadEnvelope[]> { return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.ExecutionPayloadEnvelopesByRoot, [Version.V1], request), request.length, responseSszTypeByMethod[ReqRespMethod.ExecutionPayloadEnvelopesByRoot], this.chain.serializedCache ); } private sendReqRespRequest<Req>( peerId: PeerIdStr, method: ReqRespMethod, versions: number[], request: Req ): AsyncIterable<ResponseIncoming> { const fork = this.config.getForkName(this.clock.currentSlot); const requestType = requestSszTypeByMethod(fork, this.config)[method]; const requestData = requestType ? requestType.serialize(request as never) : new Uint8Array(); // ReqResp outgoing request, emit from main thread to worker return this.core.sendReqRespRequest({peerId, method, versions, requestData}); } // Debug connectToPeer(peer: string, multiaddr: string[]): Promise<void> { return this.core.connectToPeer(peer, multiaddr); } disconnectPeer(peer: string): Promise<void> { return this.core.disconnectPeer(peer); } addDirectPeer(peer: routes.lodestar.DirectPeer): Promise<string | null> { return this.core.addDirectPeer(peer); } removeDirectPeer(peerId: string): Promise<boolean> { return this.core.removeDirectPeer(peerId); } getDirectPeers(): Promise<string[]> { return this.core.getDirectPeers(); } dumpPeer(peerIdStr: string): Promise<routes.lodestar.LodestarNodePeer | undefined> { return this.core.dumpPeer(peerIdStr); } dumpPeers(): Promise<routes.lodestar.LodestarNodePeer[]> { return this.core.dumpPeers(); } dumpPeerScoreStats(): Promise<PeerScoreStats> { return this.core.dumpPeerScoreStats(); } dumpGossipPeerScoreStats(): Promise<PeerScoreStatsDump> { return this.core.dumpGossipPeerScoreStats(); } dumpDiscv5KadValues(): Promise<string[]> { return this.core.dumpDiscv5KadValues(); } dumpMeshPeers(): Promise<Record<string, string[]>> { return this.core.dumpMeshPeers(); } async dumpGossipQueue(gossipType: GossipType): Promise<PendingGossipsubMessage[]> { return this.networkProcessor.dumpGossipQueue(gossipType); } async writeNetworkThreadProfile(durationMs: number, dirpath: string): Promise<string> { return this.core.writeNetworkThreadProfile(durationMs, dirpath); } async writeDiscv5Profile(durationMs: number, dirpath: string): Promise<string> { return this.core.writeDiscv5Profile(durationMs, dirpath); } async writeNetworkHeapSnapshot(prefix: string, dirpath: string): Promise<string> { return this.core.writeNetworkHeapSnapshot(prefix, dirpath); } async writeDiscv5HeapSnapshot(prefix: string, dirpath: string): Promise<string> { return this.core.writeDiscv5HeapSnapshot(prefix, dirpath); } private onLightClientFinalityUpdate = async (finalityUpdate: LightClientFinalityUpdate): Promise<void> => { // TODO: Review is OK to remove if (this.hasAttachedSyncCommitteeMember()) try { // messages SHOULD be broadcast after SYNC_MESSAGE_DUE_BPS of slot has transpired // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/altair/light-client/p2p-interface.md#sync-committee await this.waitForSyncMessageCutoff(finalityUpdate.signatureSlot); await this.publishLightClientFinalityUpdate(finalityUpdate); } catch (e) { // Non-mandatory route on most of network as of Oct 2022. May not have found any peers on topic yet // Remove once https://github.com/ChainSafe/js-libp2p-gossipsub/issues/367 if (!isPublishToZeroPeersError(e as Error)) { this.logger.debug("Error on BeaconGossipHandler.onLightclientFinalityUpdate", {}, e as Error); } } }; private onLightClientOptimisticUpdate = async (optimisticUpdate: LightClientOptimisticUpdate): Promise<void> => { // TODO: Review is OK to remove if (this.hasAttachedSyncCommitteeMember()) try { // messages SHOULD be broadcast after SYNC_MESSAGE_DUE_BPS of slot has transpired // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/altair/light-client/p2p-interface.md#sync-committee await this.waitForSyncMessageCutoff(optimisticUpdate.signatureSlot); await this.publishLightClientOptimisticUpdate(optimisticUpdate); } catch (e) { // Non-mandatory route on most of network as of Oct 2022. May not have found any peers on topic yet // Remove once https://github.com/ChainSafe/js-libp2p-gossipsub/issues/367 if (!isPublishToZeroPeersError(e as Error)) { this.logger.debug("Error on BeaconGossipHandler.onLightclientOptimisticUpdate", {}, e as Error); } } }; private waitForSyncMessageCutoff = async (slot: number): Promise<void> => { const fork = this.config.getForkName(slot); const msToCutoffTime = this.config.getSyncMessageDueMs(fork) - this.chain.clock.msFromSlot(slot); await sleep(msToCutoffTime, this.controller.signal); }; private onHead = async (): Promise<void> => { await this.onUpdateStatus(); }; private onPeerConnected = (data: NetworkEventData[NetworkEvent.peerConnected]): void => { const {peer, clientAgent, custodyColumns, status} = data; const earliestAvailableSlot = (status as fulu.Status).earliestAvailableSlot; this.logger.verbose("onPeerConnected", { peer, clientAgent, custodyColumns: prettyPrintIndices(custodyColumns), earliestAvailableSlot: earliestAvailableSlot ?? "pre-fulu", }); this.connectedPeersSyncMeta.set(peer, { client: clientAgent, custodyColumns, earliestAvailableSlot, // can be undefined pre-fulu }); }; private onPeerDisconnected = (data: NetworkEventData[NetworkEvent.peerDisconnected]): void => { this.connectedPeersSyncMeta.delete(data.peer); }; private onTargetGroupCountUpdated = (count: number): void => { this.core.setTargetGroupCount(count); }; private onPublishDataColumns = (sidecars: DataColumnSidecar[]): Promise<number[]> => { return promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishDataColumnSidecar(sidecar))); }; private onPublishBlobSidecars = (sidecars: deneb.BlobSidecar[]): Promise<number[]> => { return promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishBlobSidecar(sidecar))); }; private onUpdateStatus = async (): Promise<void> => { await this.core.updateStatus(this.chain.getStatus()); }; }