UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

401 lines • 20 kB
import { peerIdFromPrivateKey } from "@libp2p/peer-id"; import { routes } from "@lodestar/api"; import { ForkSeq } from "@lodestar/params"; import { computeEpochAtSlot, computeTimeAtSlot } from "@lodestar/state-transition"; import { sleep } from "@lodestar/utils"; import { RegistryMetricCreator } from "../metrics/index.js"; import { peerIdToString } from "../util/peerId.js"; import { NetworkCore, WorkerNetworkCore } from "./core/index.js"; import { NetworkEvent, NetworkEventBus } from "./events.js"; import { getActiveForkBoundaries } from "./forks.js"; import { GossipType } from "./gossip/index.js"; import { getGossipSSZType, gossipTopicIgnoreDuplicatePublishError, stringifyGossipTopic } from "./gossip/topic.js"; import { AggregatorTracker } from "./processor/aggregatorTracker.js"; import { NetworkProcessor } from "./processor/index.js"; import { ReqRespMethod } from "./reqresp/index.js"; import { Version, requestSszTypeByMethod, responseSszTypeByMethod } from "./reqresp/types.js"; import { collectExactOneTyped, collectMaxResponseTyped, collectMaxResponseTypedWithBytes, } from "./reqresp/utils/collect.js"; import { collectSequentialBlocksInRange } from "./reqresp/utils/collectSequentialBlocksInRange.js"; import { isPublishToZeroPeersError } from "./util.js"; /** * 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 { constructor(modules) { this.subscribedToCoreTopics = false; this.connectedPeers = new Set(); this.onLightClientFinalityUpdate = async (finalityUpdate) => { // TODO: Review is OK to remove if (this.hasAttachedSyncCommitteeMember()) try { // messages SHOULD be broadcast after one-third of slot has transpired // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#sync-committee await this.waitOneThirdOfSlot(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)) { this.logger.debug("Error on BeaconGossipHandler.onLightclientFinalityUpdate", {}, e); } } }; this.onLightClientOptimisticUpdate = async (optimisticUpdate) => { // TODO: Review is OK to remove if (this.hasAttachedSyncCommitteeMember()) try { // messages SHOULD be broadcast after one-third of slot has transpired // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#sync-committee await this.waitOneThirdOfSlot(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)) { this.logger.debug("Error on BeaconGossipHandler.onLightclientOptimisticUpdate", {}, e); } } }; this.waitOneThirdOfSlot = async (slot) => { const secAtSlot = computeTimeAtSlot(this.config, slot + 1 / 3, this.chain.genesisTime); const msToSlot = secAtSlot * 1000 - Date.now(); await sleep(msToSlot, this.controller.signal); }; this.onHead = async () => { await this.core.updateStatus(this.chain.getStatus()); }; this.onPeerConnected = (data) => { this.connectedPeers.add(data.peer); }; this.onPeerDisconnected = (data) => { this.connectedPeers.delete(data.peer); }; this.peerId = peerIdFromPrivateKey(modules.privateKey); this.config = modules.config; 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)); } static async init({ opts, config, logger, metrics, chain, db, gossipHandlers, privateKey, peerStoreDir, getReqRespHandler, }) { const events = new NetworkEventBus(); const aggregatorTracker = new AggregatorTracker(); const activeValidatorCount = chain.getHeadState().epochCtx.currentShuffling.activeIndices.length; const initialStatus = chain.getStatus(); 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, }, 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, 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() { return this.controller.signal.aborted; } /** Destroy this instance. Can only be called once. */ async close() { 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); await this.core.close(); // Used only for sleep() statements this.controller.abort(); this.logger.debug("network core closed"); } async scrapeMetrics() { return this.core.scrapeMetrics(); } /** * Request att subnets up `toSlot`. Network will ensure to mantain some peers for each */ async prepareBeaconCommitteeSubnets(subscriptions) { 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) { 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) { return this.core.reStatusPeers(peers); } searchUnknownSlotRoot(slotRoot, peer) { this.networkProcessor.searchUnknownSlotRoot(slotRoot, peer); } async reportPeer(peer, action, actionName) { return this.core.reportPeer(peer, action, actionName); } // REST API queries getConnectedPeers() { return Array.from(this.connectedPeers.values()); } getConnectedPeerCount() { return this.connectedPeers.size; } async getNetworkIdentity() { return this.core.getNetworkIdentity(); } /** * Subscribe to all gossip events. Safe to call multiple times */ async subscribeGossipCoreTopics() { 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() { // Drop all the gossip validation queues this.networkProcessor.dropAllJobs(); await this.core.unsubscribeGossipCoreTopics(); this.subscribedToCoreTopics = false; } isSubscribedToGossipCoreTopics() { return this.subscribedToCoreTopics; } shouldAggregate(subnet, slot) { return this.aggregatorTracker.shouldAggregate(subnet, slot); } // Gossip async publishBeaconBlock(signedBlock) { const epoch = computeEpochAtSlot(signedBlock.message.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.beacon_block, boundary }, signedBlock, { ignoreDuplicatePublishError: true, }); } async publishBlobSidecar(blobSidecar) { const epoch = computeEpochAtSlot(blobSidecar.signedBlockHeader.message.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); const subnet = blobSidecar.index; return this.publishGossip({ type: GossipType.blob_sidecar, boundary, subnet }, blobSidecar, { ignoreDuplicatePublishError: true, }); } async publishBeaconAggregateAndProof(aggregateAndProof) { const epoch = computeEpochAtSlot(aggregateAndProof.message.aggregate.data.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.beacon_aggregate_and_proof, boundary }, aggregateAndProof, { ignoreDuplicatePublishError: true }); } async publishBeaconAttestation(attestation, subnet) { const epoch = computeEpochAtSlot(attestation.data.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.beacon_attestation, boundary, subnet }, attestation, { ignoreDuplicatePublishError: true }); } async publishVoluntaryExit(voluntaryExit) { const epoch = voluntaryExit.message.epoch; const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.voluntary_exit, boundary }, voluntaryExit, { ignoreDuplicatePublishError: true, }); } async publishBlsToExecutionChange(blsToExecutionChange) { 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({ 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) { const epoch = computeEpochAtSlot(Number(proposerSlashing.signedHeader1.message.slot)); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.proposer_slashing, boundary }, proposerSlashing); } async publishAttesterSlashing(attesterSlashing) { const epoch = computeEpochAtSlot(Number(attesterSlashing.attestation1.data.slot)); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.attester_slashing, boundary }, attesterSlashing); } async publishSyncCommitteeSignature(signature, subnet) { const epoch = computeEpochAtSlot(signature.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.sync_committee, boundary, subnet }, signature, { ignoreDuplicatePublishError: true, }); } async publishContributionAndProof(contributionAndProof) { const epoch = computeEpochAtSlot(contributionAndProof.message.contribution.slot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.sync_committee_contribution_and_proof, boundary }, contributionAndProof, { ignoreDuplicatePublishError: true }); } async publishLightClientFinalityUpdate(update) { const epoch = computeEpochAtSlot(update.signatureSlot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.light_client_finality_update, boundary }, update); } async publishLightClientOptimisticUpdate(update) { const epoch = computeEpochAtSlot(update.signatureSlot); const boundary = this.config.getForkBoundaryAtEpoch(epoch); return this.publishGossip({ type: GossipType.light_client_optimistic_update, boundary }, update); } async publishGossip(topic, object, opts) { const topicStr = stringifyGossipTopic(this.config, topic); const sszType = getGossipSSZType(topic); const messageData = sszType.serialize(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, request) { 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); } async sendBeaconBlocksByRoot(peerId, request) { 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]); } async sendLightClientBootstrap(peerId, request) { return collectExactOneTyped(this.sendReqRespRequest(peerId, ReqRespMethod.LightClientBootstrap, [Version.V1], request), responseSszTypeByMethod[ReqRespMethod.LightClientBootstrap]); } async sendLightClientOptimisticUpdate(peerId) { return collectExactOneTyped(this.sendReqRespRequest(peerId, ReqRespMethod.LightClientOptimisticUpdate, [Version.V1], null), responseSszTypeByMethod[ReqRespMethod.LightClientOptimisticUpdate]); } async sendLightClientFinalityUpdate(peerId) { return collectExactOneTyped(this.sendReqRespRequest(peerId, ReqRespMethod.LightClientFinalityUpdate, [Version.V1], null), responseSszTypeByMethod[ReqRespMethod.LightClientFinalityUpdate]); } async sendLightClientUpdatesByRange(peerId, request) { return collectMaxResponseTyped(this.sendReqRespRequest(peerId, ReqRespMethod.LightClientUpdatesByRange, [Version.V1], request), request.count, responseSszTypeByMethod[ReqRespMethod.LightClientUpdatesByRange]); } async sendBlobSidecarsByRange(peerId, request) { 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, request) { return collectMaxResponseTyped(this.sendReqRespRequest(peerId, ReqRespMethod.BlobSidecarsByRoot, [Version.V1], request), request.length, responseSszTypeByMethod[ReqRespMethod.BlobSidecarsByRoot]); } sendReqRespRequest(peerId, method, versions, request) { const fork = this.config.getForkName(this.clock.currentSlot); const requestType = requestSszTypeByMethod(fork, this.config)[method]; const requestData = requestType ? requestType.serialize(request) : new Uint8Array(); // ReqResp outgoing request, emit from main thread to worker return this.core.sendReqRespRequest({ peerId, method, versions, requestData }); } // Debug connectToPeer(peer, multiaddr) { return this.core.connectToPeer(peer, multiaddr); } disconnectPeer(peer) { return this.core.disconnectPeer(peer); } dumpPeer(peerIdStr) { return this.core.dumpPeer(peerIdStr); } dumpPeers() { return this.core.dumpPeers(); } dumpPeerScoreStats() { return this.core.dumpPeerScoreStats(); } dumpGossipPeerScoreStats() { return this.core.dumpGossipPeerScoreStats(); } dumpDiscv5KadValues() { return this.core.dumpDiscv5KadValues(); } dumpMeshPeers() { return this.core.dumpMeshPeers(); } async dumpGossipQueue(gossipType) { return this.networkProcessor.dumpGossipQueue(gossipType); } async writeNetworkThreadProfile(durationMs, dirpath) { return this.core.writeNetworkThreadProfile(durationMs, dirpath); } async writeDiscv5Profile(durationMs, dirpath) { return this.core.writeDiscv5Profile(durationMs, dirpath); } async writeNetworkHeapSnapshot(prefix, dirpath) { return this.core.writeNetworkHeapSnapshot(prefix, dirpath); } async writeDiscv5HeapSnapshot(prefix, dirpath) { return this.core.writeDiscv5HeapSnapshot(prefix, dirpath); } } //# sourceMappingURL=network.js.map