UNPKG

libp2p-gossipsub

Version:
295 lines (294 loc) 13.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Heartbeat = void 0; const constants = __importStar(require("./constants")); const get_gossip_peers_1 = require("./get-gossip-peers"); const utils_1 = require("./utils"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment class Heartbeat { /** * @param {Object} gossipsub * @constructor */ constructor(gossipsub) { this.gossipsub = gossipsub; } start() { if (this._heartbeatTimer) { return; } const heartbeat = this._heartbeat.bind(this); const timeout = setTimeout(() => { heartbeat(); this._heartbeatTimer.runPeriodically(heartbeat, this.gossipsub._options.heartbeatInterval); }, constants.GossipsubHeartbeatInitialDelay); this._heartbeatTimer = { _intervalId: undefined, runPeriodically: (fn, period) => { this._heartbeatTimer._intervalId = setInterval(fn, period); }, cancel: () => { clearTimeout(timeout); clearInterval(this._heartbeatTimer._intervalId); } }; } /** * Unmounts the gossipsub protocol and shuts down every connection * @override * @returns {void} */ stop() { if (!this._heartbeatTimer) { return; } this._heartbeatTimer.cancel(); this._heartbeatTimer = null; } /** * Maintains the mesh and fanout maps in gossipsub. * * @returns {void} */ _heartbeat() { const { D, Dlo, Dhi, Dscore, Dout, fanoutTTL } = this.gossipsub._options; this.gossipsub.heartbeatTicks++; // cache scores throught the heartbeat const scores = new Map(); const getScore = (id) => { let s = scores.get(id); if (s === undefined) { s = this.gossipsub.score.score(id); scores.set(id, s); } return s; }; // peer id => topic[] const tograft = new Map(); // peer id => topic[] const toprune = new Map(); // peer id => don't px const noPX = new Map(); // clean up expired backoffs this.gossipsub._clearBackoff(); // clean up peerhave/iasked counters this.gossipsub.peerhave.clear(); this.gossipsub.iasked.clear(); // apply IWANT request penalties this.gossipsub._applyIwantPenalties(); // ensure direct peers are connected this.gossipsub._directConnect(); // maintain the mesh for topics we have joined this.gossipsub.mesh.forEach((peers, topic) => { // prune/graft helper functions (defined per topic) const prunePeer = (id) => { this.gossipsub.log('HEARTBEAT: Remove mesh link to %s in %s', id, topic); // update peer score this.gossipsub.score.prune(id, topic); // add prune backoff record this.gossipsub._addBackoff(id, topic); // remove peer from mesh peers.delete(id); // add to toprune const topics = toprune.get(id); if (!topics) { toprune.set(id, [topic]); } else { topics.push(topic); } }; const graftPeer = (id) => { this.gossipsub.log('HEARTBEAT: Add mesh link to %s in %s', id, topic); // update peer score this.gossipsub.score.graft(id, topic); // add peer to mesh peers.add(id); // add to tograft const topics = tograft.get(id); if (!topics) { tograft.set(id, [topic]); } else { topics.push(topic); } }; // drop all peers with negative score, without PX peers.forEach(id => { const score = getScore(id); if (score < 0) { this.gossipsub.log('HEARTBEAT: Prune peer %s with negative score: score=%d, topic=%s', id, score, topic); prunePeer(id); noPX.set(id, true); } }); // do we have enough peers? if (peers.size < Dlo) { const backoff = this.gossipsub.backoff.get(topic); const ineed = D - peers.size; const peersSet = get_gossip_peers_1.getGossipPeers(this.gossipsub, topic, ineed, id => { // filter out mesh peers, direct peers, peers we are backing off, peers with negative score return !peers.has(id) && !this.gossipsub.direct.has(id) && (!backoff || !backoff.has(id)) && getScore(id) >= 0; }); peersSet.forEach(graftPeer); } // do we have to many peers? if (peers.size > Dhi) { let peersArray = Array.from(peers); // sort by score peersArray.sort((a, b) => getScore(b) - getScore(a)); // We keep the first D_score peers by score and the remaining up to D randomly // under the constraint that we keep D_out peers in the mesh (if we have that many) peersArray = peersArray.slice(0, Dscore).concat(utils_1.shuffle(peersArray.slice(Dscore))); // count the outbound peers we are keeping let outbound = 0; peersArray.slice(0, D).forEach(p => { if (this.gossipsub.outbound.get(p)) { outbound++; } }); // if it's less than D_out, bubble up some outbound peers from the random selection if (outbound < Dout) { const rotate = (i) => { // rotate the peersArray to the right and put the ith peer in the front const p = peersArray[i]; for (let j = i; j > 0; j--) { peersArray[j] = peersArray[j - 1]; } peersArray[0] = p; }; // first bubble up all outbound peers already in the selection to the front if (outbound > 0) { let ihave = outbound; for (let i = 1; i < D && ihave > 0; i++) { if (this.gossipsub.outbound.get(peersArray[i])) { rotate(i); ihave--; } } } // now bubble up enough outbound peers outside the selection to the front let ineed = D - outbound; for (let i = D; i < peersArray.length && ineed > 0; i++) { if (this.gossipsub.outbound.get(peersArray[i])) { rotate(i); ineed--; } } } // prune the excess peers peersArray.slice(D).forEach(prunePeer); } // do we have enough outbound peers? if (peers.size >= Dlo) { // count the outbound peers we have let outbound = 0; peers.forEach(p => { if (this.gossipsub.outbound.get(p)) { outbound++; } }); // if it's less than D_out, select some peers with outbound connections and graft them if (outbound < Dout) { const ineed = Dout - outbound; const backoff = this.gossipsub.backoff.get(topic); get_gossip_peers_1.getGossipPeers(this.gossipsub, topic, ineed, (id) => { // filter our current mesh peers, direct peers, peers we are backing off, peers with negative score return !peers.has(id) && !this.gossipsub.direct.has(id) && (!backoff || !backoff.has(id)) && getScore(id) >= 0; }).forEach(graftPeer); } } // should we try to improve the mesh with opportunistic grafting? if (this.gossipsub.heartbeatTicks % constants.GossipsubOpportunisticGraftTicks === 0 && peers.size > 1) { // Opportunistic grafting works as follows: we check the median score of peers in the // mesh; if this score is below the opportunisticGraftThreshold, we select a few peers at // random with score over the median. // The intention is to (slowly) improve an underperforming mesh by introducing good // scoring peers that may have been gossiping at us. This allows us to get out of sticky // situations where we are stuck with poor peers and also recover from churn of good peers. // now compute the median peer score in the mesh const peersList = Array.from(peers) .sort((a, b) => getScore(a) - getScore(b)); const medianIndex = Math.floor(peers.size / 2); const medianScore = getScore(peersList[medianIndex]); // if the median score is below the threshold, select a better peer (if any) and GRAFT if (medianScore < this.gossipsub._options.scoreThresholds.opportunisticGraftThreshold) { const backoff = this.gossipsub.backoff.get(topic); const peersToGraft = get_gossip_peers_1.getGossipPeers(this.gossipsub, topic, constants.GossipsubOpportunisticGraftPeers, (id) => { // filter out current mesh peers, direct peers, peers we are backing off, peers below or at threshold return peers.has(id) && !this.gossipsub.direct.has(id) && (!backoff || !backoff.has(id)) && getScore(id) > medianScore; }); peersToGraft.forEach(id => { this.gossipsub.log('HEARTBEAT: Opportunistically graft peer %s on topic %s', id, topic); graftPeer(id); }); } } // 2nd arg are mesh peers excluded from gossip. We have already pushed // messages to them, so its redundant to gossip IHAVEs. this.gossipsub._emitGossip(topic, peers); }); // expire fanout for topics we haven't published to in a while const now = this.gossipsub._now(); this.gossipsub.lastpub.forEach((lastpb, topic) => { if ((lastpb + fanoutTTL) < now) { this.gossipsub.fanout.delete(topic); this.gossipsub.lastpub.delete(topic); } }); // maintain our fanout for topics we are publishing but we have not joined this.gossipsub.fanout.forEach((fanoutPeers, topic) => { // checks whether our peers are still in the topic and have a score above the publish threshold const topicPeers = this.gossipsub.topics.get(topic); fanoutPeers.forEach(id => { if (!topicPeers.has(id) || getScore(id) < this.gossipsub._options.scoreThresholds.publishThreshold) { fanoutPeers.delete(id); } }); // do we need more peers? if (fanoutPeers.size < D) { const ineed = D - fanoutPeers.size; const peersSet = get_gossip_peers_1.getGossipPeers(this.gossipsub, topic, ineed, (id) => { // filter out existing fanout peers, direct peers, and peers with score above the publish threshold return !fanoutPeers.has(id) && !this.gossipsub.direct.has(id) && getScore(id) >= this.gossipsub._options.scoreThresholds.publishThreshold; }); peersSet.forEach(id => { fanoutPeers.add(id); }); } // 2nd arg are fanout peers excluded from gossip. // We have already pushed messages to them, so its redundant to gossip IHAVEs this.gossipsub._emitGossip(topic, fanoutPeers); }); // send coalesced GRAFT/PRUNE messages (will piggyback gossip) this.gossipsub._sendGraftPrune(tograft, toprune, noPX); // flush pending gossip that wasn't piggybacked above this.gossipsub._flush(); // advance the message history window this.gossipsub.messageCache.shift(); this.gossipsub.emit('gossipsub:heartbeat'); } } exports.Heartbeat = Heartbeat;