UNPKG

ipfs-coord

Version:

A JS library for helping IPFS peers coordinate, find a common interest, and stay connected around that interest.

477 lines (398 loc) 16 kB
/* A Use Case library for working with Circuit Relays. TODO: Build these methods: - pruneConnections() - Disconnect dead or misbehaving nodes. - addRelay() - Add a new relay to the state. */ const bootstrapCircuitRelays = require('../../config/bootstrap-circuit-relays') // const RelayEntity = require('../entities/relay-entity') class RelayUseCases { constructor (localConfig = {}) { // Dependency Injection. this.adapters = localConfig.adapters if (!this.adapters) { throw new Error( 'Must inject instance of adapters when instantiating Relay Use Cases library.' ) } this.controllers = localConfig.controllers if (!this.controllers) { throw new Error( 'Must inject instance of controllers when instantiating Relay Use Cases library.' ) } } // Connect to the pre-programmed circuit relays for the first time at startup. async initializeRelays (thisNode) { try { // Set the initial CR bootstrap nodes based on the type of IPFS node. let bootstrapRelays = [] if (thisNode.type === 'browser') { bootstrapRelays = bootstrapCircuitRelays.browser } else { bootstrapRelays = bootstrapCircuitRelays.node } // Loop through each bootstrap Relay. for (let i = 0; i < bootstrapRelays.length; i++) { const thisRelay = bootstrapRelays[i] // Attempt to connect to the Circuit Relay peer. const connectionStatus = await this.adapters.ipfs.connectToPeer( thisRelay.multiaddr ) const now = new Date() const newRelayObj = { multiaddr: thisRelay.multiaddr, connected: connectionStatus, updatedAt: now, ipfsId: thisRelay.ipfsId, isBootstrap: true, metrics: { aboutLatency: [] } } // Record relay information in thisNode entity. thisNode.relayData.push(newRelayObj) } return true } catch (err) { console.error('Error in relay-use-case.js/initializeRelays()') throw err } } // Download a list of Circuit Relays from a GitHub Gist, and try to connect // to all of the ones in the list. async getCRGist (thisNode) { try { const gistCRs = await this.adapters.gist.getCRList() let gistRelays if (thisNode.type === 'browser') { gistRelays = gistCRs.browser } else { gistRelays = gistCRs.node } console.log(`Relays from the gist: ${JSON.stringify(gistRelays, null, 2)}`) for (let i = 0; i < gistRelays.length; i++) { const thisRelay = gistRelays[i] // If this relay is already in the list of circuit relays, then skip. const alreadyExists = thisNode.relayData.filter( x => x.ipfsId === thisRelay.ipfsId ) if (alreadyExists.length) continue // Attempt to connect to the Circuit Relay peer. console.log(`getCRGist() Connecting to Circuit Relay ${thisRelay.multiaddr}`) const connectionStatus = await this.adapters.ipfs.connectToPeer( thisRelay.multiaddr ) const now = new Date() const newRelayObj = { multiaddr: thisRelay.multiaddr, connected: connectionStatus, updatedAt: now, ipfsId: thisRelay.ipfsId, isBootstrap: false, metrics: { aboutLatency: [] } } // Record relay information in thisNode entity. thisNode.relayData.push(newRelayObj) } // Remove duplicate Circuit Relays. await this.removeDuplicates(thisNode) return true } catch (err) { console.error('Error in relay-use-case.js/getCRGist()') throw err } } // Renew the connection to each circuit relay in the state. async connectToCRs (thisNode) { try { let knownRelays = thisNode.relayData // Sort the Relays by their latency. This way Relays with the lowest latency // (relative to thisNode) are used first. knownRelays = this.sortRelays(knownRelays) // console.log('knownRelays: ', knownRelays) for (let i = 0; i < knownRelays.length; i++) { const thisRelay = knownRelays[i] // console.log('thisRelay: ', thisRelay) // Get connected peers const connectedPeers = await this.adapters.ipfs.getPeers() // console.log('connectedPeers: ', connectedPeers) // Check if target circuit-relay is currently conected to the node. const connectedPeer = connectedPeers.filter(peerObj => thisRelay.multiaddr.match(peerObj.peer) ) // console.log('connectedPeer: ', connectedPeer) // Update the connection state. if (connectedPeer.length) { // Already connected. thisRelay.connected = true console.log(`Already connected to Circuit Relay ${thisRelay.multiaddr}`) } else { // If this node is not connected, try to connect again. console.log(`Connecting to Circuit Relay ${thisRelay.multiaddr}`) thisRelay.connected = await this.adapters.ipfs.connectToPeer( thisRelay.multiaddr ) console.log(`thisRelay.connected: ${thisRelay.connected}`) if (thisRelay.connected) { this.adapters.log.statusLog( 0, `Connected to Circuit Relay peer ${thisRelay.ipfsId}` ) console.log(`connectToCRs() Connected to Circuit Relay peer ${thisRelay.ipfsId}`) } } // console.log('thisRelay.connected: ', thisRelay.connected) // Update the timestamp. const now = new Date() thisRelay.updatedAt = now // Update the state tracked by This Node. thisNode.relayData[i] = thisRelay } // const now = new Date() // this.adapters.log.statusLog( // `status: ${now.toLocaleString()}: Renewed connections to all known Circuit Relay nodes.` // ) } catch (err) { console.log('Error in connectToCRs()') } } // Sort the relay data relative to the measured latency metrics. sortRelays (relayData) { try { // Loop through each element and add a latency score. for (let i = 0; i < relayData.length; i++) { const thisRelay = relayData[i] // Ensure an initial default value is assigned. thisRelay.latencyScore = 10000 // Assign bootstrap nodes the maximum latency. if (thisRelay.isBootstrap) { thisRelay.latencyScore = 10000 } else { // Average the latencies measured for this Relay. thisRelay.latencyScore = Math.floor( this._average(thisRelay.metrics.aboutLatency) ) // Handle corner-case of empty arrays. if (isNaN(thisRelay.latencyScore)) thisRelay.latencyScore = 10000 } // console.log('thisRelay: ', thisRelay) } // Sort the array by the latency score. // https://flaviocopes.com/how-to-sort-array-of-objects-by-property-javascript/ const sortedList = relayData.sort((a, b) => a.latencyScore > b.latencyScore ? 1 : -1 ) // console.log('Relay sortedList: ', sortedList) return sortedList } catch (err) { console.error('Error in sortRelays()') throw err } } // Average together the elements of an array. _average (arr) { return arr.reduce((p, c) => p + c, 0) / arr.length } // If a subnet Peer has the isCircuitRelay flag set, try to connect to them // directly. If the connection succeeds, add them to the list of Circuit Relays. // Triggered when a new subnet peer is found that has their Relay flag set. async addRelay (ipfsId, thisNode) { try { const now = new Date() // console.log(`debug: addRelay() called at ${now.toLocaleString()}`) // Check to see if peer is already in the list of Circuit Relays. const alreadyInList = thisNode.relayData.filter(x => x.ipfsId.includes(ipfsId) ) // Exit if this peer is already in the list of Relays. if (alreadyInList.length) return true // this.adapters.log.statusLog(1, 'New Circuit Relay Peer Found!') // Get the peer data. let peerData = thisNode.peerData.filter(x => x.data.ipfsId === ipfsId) peerData = peerData[0] // console.log('peerData: ', peerData) // Exit if this peer is not a circuit relay. // CT Added 9/4/21. // Added because testing was showing that some non-relay peers where getting // pings with about metrics. const isCircuitRelay = peerData.data.isCircuitRelay if (!isCircuitRelay) return false // Return true? this.adapters.log.statusLog( 1, `New Circuit Relay Peer Found: ${peerData.from}` ) // Grab the optional connection information if the Circuit Relay provides // it. If they do, construct a connection string and add it to the // list of multiaddrs. const circuitRelayInfo = peerData.data.circuitRelayInfo if (circuitRelayInfo) { if (circuitRelayInfo.ip4) { const ip4TcpMultiaddr = `/ip4/${circuitRelayInfo.ip4}/tcp/${circuitRelayInfo.tcpPort}/p2p/${peerData.from}` peerData.data.ipfsMultiaddrs.push(ip4TcpMultiaddr) } if (circuitRelayInfo.crDomain) { const dnsMultiaddr = `/dns4/${circuitRelayInfo.crDomain}/tcp/443/wss/ipfs/${peerData.from}` peerData.data.ipfsMultiaddrs.push(dnsMultiaddr) } // Debugging // console.log( // `peer multiaddrs: ${JSON.stringify( // peerData.data.ipfsMultiaddrs, // null, // 2 // )}` // ) } let multiaddr = '' let connectSuccess = false // Try to connect to one of the multiaddrs. for (let i = 0; i < peerData.data.ipfsMultiaddrs.length; i++) { const thisAddr = peerData.data.ipfsMultiaddrs[i] // Try to connect to the IPFS peer. connectSuccess = await this.adapters.ipfs.connectToPeer(thisAddr) // If connection was successful if (connectSuccess) { // Get a list of connect peers. const peers = await this.adapters.ipfs.getPeers() // console.log('addRelay() peers: ', peers) // Retrieve the multiaddr that worked for connecting. const thisRelay = peers.filter(x => x.peer === ipfsId) multiaddr = thisRelay[0].addr.toString() // console.log('multiaddr: ', multiaddr) // If multiaddr does not contain ip4, ip6, or dns, then reject it, // because the connection is happening through another Relay. // If the multiaddr has 'p2p-circuit' in it, then it's also happening // through a Relay and should be rejected. const isIp4 = multiaddr.includes('ip4') const isIp6 = multiaddr.includes('ip6') const isDns = multiaddr.includes('dns') const isBridged = multiaddr.includes('p2p-circuit') if ((!isIp4 && !isIp6 && !isDns) || isBridged) { this.adapters.log.statusLog( 1, `Rejecting Relay multiaddr ${multiaddr}` ) // Disconnect from the peer, so that a direct connection can be // attempted. await this.adapters.ipfs.disconnectFromPeer(ipfsId) connectSuccess = false continue } break } else { this.adapters.log.statusLog( 1, `Connecting to potential relay did not succeed with multiaddr ${thisAddr}` ) } } // If we could not successfully connect to the peer with any of the given // multiaddrs, then do not add it to the list of Relays. if (!connectSuccess) { this.adapters.log.statusLog( 1, `: Could not add potential Circuit Relay: ${ipfsId}` ) return false } // Add tail end of multiaddr if it's omitted. (this happens in go-ipfs v0.11.0) if (!multiaddr.includes(ipfsId)) { multiaddr = `${multiaddr}/p2p/${ipfsId}` } // On successful connection, add the Circuit Relay to the list. // const now = new Date() const newRelayObj = { multiaddr, connected: true, updatedAt: now, ipfsId, isBootstrap: false, metrics: { aboutLatency: [] } } // console.log(`newRelayObj: ${JSON.stringify(newRelayObj, null, 2)}`) thisNode.relayData.push(newRelayObj) console.log(`New Circuit Relay added: ${multiaddr}`) return true } catch (err) { console.error('Error in addRelay(): ', err) // top-level function. Do not throw an error. return false } } // Called when periodically checking the Relay connection. Maintains network // metrics about each Relay. // Called by the manageCircuitRelays() Timer Controller. async measureRelays (thisNode) { try { // console.log('Entering measureRelays()') // console.log('thisNode.relayData: ', thisNode.relayData) // console.log( // `thisNode.peerData: ${JSON.stringify(thisNode.peerData, null, 2)}` // ) // Loop through each Circuit Relay for (let i = 0; i < thisNode.relayData.length; i++) { const thisRelay = thisNode.relayData[i] // If the relay is from the bootstrap list, skip it. // if (thisRelay.isBootstrap) continue // Get the peer data. let peerData = thisNode.peerData.filter(x => { // console.log(`x.data.ipfsId: ${x.data.ipfsId}`) // console.log(`thisRelay.ipfsId: ${thisRelay.ipfsId}`) return x.data.ipfsId === thisRelay.ipfsId }) peerData = peerData[0] // console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`) // Skip if this Relay is not advertising as a relay. if (!peerData || !peerData.data.isCircuitRelay) continue // console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`) // If this node is not connected to the Relay, give it the worst score. if (!thisRelay.connected) { thisRelay.metrics.aboutLatency.push(10000) continue } // Poll the /about JSON endpoint, and track the time it takes to recieve // a response. const startTime = new Date() const testResult = await this.adapters.pubsub.about.queryAbout( thisRelay.ipfsId, thisNode ) // await this.adapters.bch.bchjs.Util.sleep(2000) const endTime = new Date() let diffTime = 10000 if (testResult) diffTime = endTime.getTime() - startTime.getTime() // console.log(`Time difference: ${diffTime} mS`) // Prune the metrics array if it's too big. if (thisRelay.metrics.aboutLatency.length > 9) { thisRelay.metrics.aboutLatency.shift() } // Save the new metric value. thisRelay.metrics.aboutLatency.push(diffTime) } } catch (err) { console.error('Error in measureRelays(): ', err) throw err } } // Remove any duplicate entries of Circuit Relays. // This is middleware that edits the thisNode.relayData in place. removeDuplicates (thisNode) { try { const startingRelays = thisNode.relayData // console.log('thisNode.relayData: ', thisNode.relayData) // https://stackoverflow.com/questions/2218999/how-to-remove-all-duplicates-from-an-array-of-objects const endingRelays = startingRelays.filter( (relay, index, self) => index === self.findIndex(t => t.ipfsId === relay.ipfsId) ) // console.log('endingRelays: ', endingRelays) thisNode.relayData = endingRelays return true } catch (err) { console.error('Error in removeDuplicates()') throw err } } } module.exports = RelayUseCases