Skip to content

Commit

Permalink
feat: initial implementation (#1)
Browse files Browse the repository at this point in the history
* feat: initial implementation

* chore: remove bundlesize for now

* test: add query test and fix related bugs

* fix: add discovery tag

* chore: add logging

* fix: add support for removeListener

* test: improve coverage

* chore: apply suggestions from code review

Co-Authored-By: Vasco Santos <vasco.santos@ua.pt>

* chore: add lead maintainer to package.json

Co-authored-by: Vasco Santos <vasco.santos@ua.pt>
  • Loading branch information
jacobheun and vasco-santos authored Mar 31, 2020
1 parent d7111aa commit d6f7a31
Show file tree
Hide file tree
Showing 7 changed files with 465 additions and 0 deletions.
42 changes: 42 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
language: node_js
cache: npm
stages:
- check
- test
- cov

node_js:
- '10'
- '12'

os:
- linux
- osx
- windows

script: npx nyc -s npm run test:node -- --bail
after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov

jobs:
include:
- stage: check
script:
- npx aegir dep-check
- npm run lint

- stage: test
name: chrome
addons:
chrome: stable
script:
- npx aegir test -t browser -t webworker

- stage: test
name: firefox
addons:
firefox: latest
script:
- npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless

notifications:
email: false
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# js-libp2p Pubsub Peer Discovery

[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai)
[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p)
[![](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io)

> A js-libp2p module that uses pubsub for mdns like peer discovery
## Lead Maintainer

[Jacob Heun](https://github.com/jacobheun).

## Design

This module takes a similar approach to [MulticastDNS (MDNS)](https://github.com/libp2p/specs/blob/master/discovery/mdns.md) queries, except it leverages pubsub to "query" peers on the pubsub topic. A `Query` is performed by publishing a unique Query ID, along with your peers information (Peer ID, PublicKey, Multiaddrs). Each peer that receives the Query will submit a `QueryResponse`, consisting of their peer information and the Query ID being responded to.

### Flow
- When the discovery module is started by libp2p it subscribes to the discovery pubsub topic
- Once subscribed, the peer will "query" the network by publishing a `Query`
- The `Query` will also include your peers `QueryResponse`, so that other nodes can learn about you without needing to poll
- Whenever another pubsub discovery peer joins the pubsub mesh, it will post its `Query`

### Security Considerations
It is worth noting that this module does not include any message signing for queries. The reason for this is that libp2p-pubsub supports message signing and enables it by default, which means the message you received has been verified to be from the originator, so we can trust that the peer information we have received is indeed from the peer who owns it. This doesn't mean the peer can't falsify its own records, but this module isn't currently concerned with that scenario.

## Usage

### Requirements

This module *MUST* be used on a libp2p node that is running [Pubsub](https://github.com/libp2p/js-libp2p-pubsub). If Pubsub does not exist, or is not running, this module will not work.

## Contribute

Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-pubsub-peer-discovery/issues)!

This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).

[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md)

## License

MIT - Protocol Labs 2020
58 changes: 58 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "libp2p-pubsub-peer-discovery",
"version": "0.0.0",
"description": "A libp2p module that uses pubsub for mdns like peer discovery",
"leadMaintainer": "Jacob Heun <jacobheun@gmail.com>",
"main": "src/index.js",
"files": [
"dist",
"src"
],
"repository": {
"type": "git",
"url": "https://github.com/libp2p/js-libp2p-pubsub-peer-discovery.git"
},
"keywords": [
"libp2p",
"peer",
"discovery",
"pubsub"
],
"bugs": {
"url": "https://github.com/libp2p/js-libp2p-pubsub-peer-discovery/issues"
},
"homepage": "https://libp2p.io",
"license": "MIT",
"engines": {
"node": ">=10.0.0",
"npm": ">=6.0.0"
},
"scripts": {
"lint": "aegir lint",
"build": "aegir build",
"test": "aegir test",
"test:node": "aegir test -t node",
"test:browser": "aegir test -t browser",
"release": "aegir release",
"release-minor": "aegir release --type minor",
"release-major": "aegir release --type major",
"coverage": "nyc --reporter=text --reporter=lcov npm test"
},
"devDependencies": {
"aegir": "^21.4.5",
"chai": "^4.2.0",
"dirty-chai": "^2.0.1",
"libp2p-interfaces": "^0.2.7",
"p-defer": "^3.0.0",
"sinon": "^9.0.1"
},
"dependencies": {
"debug": "^4.1.1",
"emittery": "^0.6.0",
"libp2p-crypto": "^0.17.5",
"multiaddr": "^7.4.3",
"peer-id": "^0.13.11",
"peer-info": "^0.17.5",
"protons": "^1.0.2"
}
}
143 changes: 143 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use strict'

const Emittery = require('emittery')
const debug = require('debug')

const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
const randomBytes = require('libp2p-crypto/src/random-bytes')
const multiaddr = require('multiaddr')

const PB = require('./query')

const log = debug('libp2p:discovery:pubsub')
log.error = debug('libp2p:discovery:pubsub:error')
const TOPIC = '_peer-discovery._p2p._pubsub'

/**
* @typedef Message
* @property {string} from
* @property {Buffer} data
* @property {Buffer} seqno
* @property {Array<string>} topicIDs
* @property {Buffer} signature
* @property {key} Buffer
*/

/**
* A Peer Discovery Service that leverages libp2p Pubsub to find peers.
*/
class PubsubPeerDiscovery extends Emittery {
/**
*
* @param {Libp2p} param0.libp2p Our libp2p node
* @param {Number} [param0.delay] How long to wait (ms) after startup before publishing our Query. Default: 1000ms
*/
constructor ({ libp2p, delay = 1000 }) {
super()
this.libp2p = libp2p
this.delay = delay
this._timeout = null
this.removeListener = this.off.bind(this)
}

/**
* Subscribes to the discovery topic on `libp2p.pubsub` and peforms a query
* after `this.delay` milliseconds
*/
start () {
if (this._timeout) return

// Subscribe to pubsub
this.libp2p.pubsub.subscribe(TOPIC, (msg) => this._onMessage(msg))
// Perform a delayed publish to give pubsub time to do its thing
this._timeout = setTimeout(() => {
this._query()
}, this.delay)
}

/**
* Unsubscribes from the discovery topic
*/
stop () {
clearTimeout(this._timeout)
this._timeout = null
this.libp2p.pubsub.unsubscribe(TOPIC)
}

/**
* Performs a Query via Pubsub publish
* @private
*/
_query () {
const id = randomBytes(32)
const query = {
id,
queryResponse: {
queryID: id,
publicKey: this.libp2p.peerInfo.id.pubKey.bytes,
addrs: this.libp2p.peerInfo.multiaddrs.toArray().map(ma => ma.buffer)
}
}
const encodedQuery = PB.Query.encode(query)
this.libp2p.pubsub.publish(TOPIC, encodedQuery)
}

/**
* Handles incoming pubsub messages for our discovery topic
* @private
* @async
* @param {Message} message
*/
async _onMessage (message) {
if (await this._handleQuery(message.data)) return

this._handleQueryResponse(message.data)
}

/**
* Attempts to decode a QueryResponse from the given data. Any errors will be logged and ignored.
* @private
* @async
* @param {Buffer} data The Pubsub message data to decode
*/
async _handleQuery (data) {
try {
const query = PB.Query.decode(data)
const peerId = await PeerId.createFromPubKey(query.queryResponse.publicKey)
// Ignore if we received our own response
if (peerId.equals(this.libp2p.peerInfo.id)) return
const peerInfo = new PeerInfo(peerId)
query.queryResponse.addrs.forEach(buffer => peerInfo.multiaddrs.add(multiaddr(buffer)))
this.emit('peer', peerInfo)
log('discovered peer', peerInfo.id)
return true
} catch (err) {
log.error(err)
return false
}
}

/**
* Attempts to decode a QueryResponse from the given data. Any errors will be logged and ignored.
* @private
* @async
* @param {Buffer} data The Pubsub message data to decode
*/
async _handleQueryResponse (data) {
try {
const queryResponse = PB.QueryResponse.decode(data)
const peerId = await PeerId.createFromPubKey(queryResponse.publicKey)
const peerInfo = new PeerInfo(peerId)
queryResponse.addrs.forEach(buffer => peerInfo.multiaddrs.add(multiaddr(buffer)))
this.emit('peer', peerInfo)
log('discovered peer', peerInfo.id)
} catch (err) {
log.error(err)
}
}
}

module.exports = PubsubPeerDiscovery
module.exports.TOPIC = TOPIC
module.exports.tag = 'PubsubPeerDiscovery'
18 changes: 18 additions & 0 deletions src/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'

const protons = require('protons')
const schema = `
message Query {
// id is 32 random bytes
required bytes id = 0;
required QueryResponse queryResponse = 1;
}
message QueryResponse {
required bytes queryID = 0;
required bytes publicKey = 1;
repeated bytes addrs = 2;
}
`

module.exports = protons(schema)
31 changes: 31 additions & 0 deletions test/compliance.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* eslint-env mocha */
'use strict'

const tests = require('libp2p-interfaces/src/peer-discovery/tests')
const PubsubPeerDiscovery = require('../src')

const PeerID = require('peer-id')
const PeerInfo = require('peer-info')

describe('compliance tests', () => {
tests({
async setup () {
const peerId = await PeerID.create({ bits: 512 })
const peerInfo = new PeerInfo(peerId)
await new Promise(resolve => setTimeout(resolve, 10))
return new PubsubPeerDiscovery({
libp2p: {
peerInfo,
pubsub: {
subscribe: () => {},
unsubscribe: () => {},
publish: () => {}
}
}
})
},
async teardown () {
await new Promise(resolve => setTimeout(resolve, 10))
}
})
})
Loading

0 comments on commit d6f7a31

Please sign in to comment.