├── .gitignore ├── .travis.yml ├── example.js ├── bin └── cmd.js ├── test └── utils.js ├── package.json ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var Bitboot = require('./') 2 | 3 | var bb = new Bitboot('bitboot test network') 4 | 5 | bb.on('rejoin', function (nodeId) { 6 | console.log('I have a new node id:', nodeId.toString('hex')) 7 | }) 8 | 9 | bb.on('peers', function (peers) { 10 | console.log('I found peers:', peers) 11 | }) 12 | 13 | bb.on('error', function (err) { 14 | console.error(err) 15 | }) 16 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Bitboot = require('../') 4 | var args = process.argv.slice(2) 5 | 6 | if (args.length !== 1) { 7 | console.error('Usage: bitboot ') 8 | process.exit(1) 9 | } 10 | 11 | var bb = new Bitboot(args[0]) 12 | 13 | bb.on('peers', function (peers) { 14 | for (var i = 0; i < peers.length; i++) { 15 | process.stdout.write(peers[i].host + ':' + peers[i].port + '\n') 16 | } 17 | }) 18 | 19 | bb.on('error', function (err) { 20 | console.error(err) 21 | }) 22 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var Bitboot = require('../') 3 | var crypto = require('crypto') 4 | 5 | var KBucket = require('k-bucket') 6 | 7 | function newId (rallyId) { 8 | var id = crypto.randomBytes(20) 9 | id[0] = rallyId[0] 10 | id[1] = rallyId[1] 11 | id[18] = rallyId[18] 12 | id[19] = rallyId[19] 13 | return id 14 | } 15 | 16 | test('twiddle march with equal target/closest', function (t) { 17 | t.plan(1) 18 | 19 | var bb = new Bitboot('test', { bootstrap: false }) 20 | bb.on('rejoin', function () { 21 | var closeId = newId(bb.rallyId) 22 | var toBeat = KBucket.distance(closeId, bb.rallyId) 23 | var distance = KBucket.distance(bb._twiddleMarch(closeId), bb.rallyId) 24 | t.ok(distance < toBeat, 'Result of twiddle march should be closer') 25 | bb.destroy() 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitboot", 3 | "version": "0.1.0", 4 | "description": "Bootstrap a distributed p2p network", 5 | "main": "index.js", 6 | "dependencies": { 7 | "bittorrent-dht": "^7.2.2", 8 | "k-bucket": "^3.0.1", 9 | "debug": "^2.2.0", 10 | "bitwise": "^0.2.0" 11 | }, 12 | "bin": { 13 | "bitboot": "./bin/cmd.js" 14 | }, 15 | "devDependencies": { 16 | "standard": "^7.0.0", 17 | "tape": "^4.4.0" 18 | }, 19 | "scripts": { 20 | "test": "standard && tape test/*.js" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/tintfoundation/bitboot.git" 25 | }, 26 | "keywords": [ 27 | "dht", 28 | "distributed hash table", 29 | "protocol", 30 | "peer", 31 | "p2p", 32 | "peer-to-peer", 33 | "bootstrap", 34 | "torrent", 35 | "bittorrent" 36 | ], 37 | "author": "Tint Foundation", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/tintfoundation/bitboot/issues" 41 | }, 42 | "homepage": "https://github.com/tintfoundation/bitboot" 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Brian Muller and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitboot [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] 2 | 3 | [travis-image]: https://img.shields.io/travis/tintfoundation/bitboot/master.svg 4 | [travis-url]: https://travis-ci.org/tintfoundation/bitboot 5 | [npm-image]: https://img.shields.io/npm/v/bitboot.svg 6 | [npm-url]: https://npmjs.org/package/bitboot 7 | 8 | ### P2P Network Bootstrapping 9 | 10 | Bitboot allows a new node in a [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) network to find other nodes in the same network, even if the network being joined is as small as a single node. It has no local dependencies and doesn't require that any other local services be running. 11 | 12 | ### Install 13 | 14 | ``` 15 | npm install -g bitboot 16 | ``` 17 | 18 | ### Command Line Usage 19 | 20 | ``` 21 | bitboot 22 | ``` 23 | 24 | The `magic name` should be a unique string (make sure to use quotation marks if it's more than one word) for your network. If at least one other instance of bitboot is running somewhere else with the same magic name, then the program will print out other node's locations as they are found (one per line, in `host:port` format). If you're just starting a new network, this may take a minute or two before other nodes are found. 25 | 26 | ### Library Example 27 | 28 | ```js 29 | var Bitboot = require('bitboot') 30 | 31 | // The rally point name can be any string and should be unique 32 | // to your peer network 33 | var bb = new Bitboot('bitboot test network') 34 | 35 | // this is called whenever the node selects a new ID and rejoins 36 | // the BitTorrent mainline DHT network 37 | bb.on('rejoin', function (nodeId) { 38 | console.log('I have a new node id:', nodeId.toString('hex')) 39 | }) 40 | 41 | // this is called whenever a search is made for peers 42 | // peers will be the result of that search (and may be empty) 43 | bb.on('peers', function (peers) { 44 | console.log('I found peers:', peers) 45 | }) 46 | 47 | bb.on('error', function (err) { 48 | console.error(err) 49 | }) 50 | ``` 51 | 52 | ### Background 53 | 54 | Many peer-to-peer networks clients are initially bootstrapped by connecting to a handful of hard-coded, centralized nodes (yes, even [Bitcoin](https://github.com/bitcoin/bitcoin/blob/37d83bb0a980996338d9bc9dbdbf0175eeaba9a2/src/chainparams.cpp#L116) and [BitTorrent](https://github.com/qbittorrent/qBittorrent/blob/5e114c0f2ead8077061e09e8debf89dfa0d526dc/src/base/bittorrent/session.cpp#L1567)). Every new peer-to-peer network must solve this same challenge, usually by hardcoding centralized bootstrap servers. Bitboot allows you to avoid this step of having to run/maintain a new centralized server if you're creating a new p2p network. Bitboot can also be used more generally to find a single peer (for instance, if you just want to be able to find your home computer and the IP is changing frequently). 55 | 56 | When you run bitboot, you give it a [magic name](https://en.wikipedia.org/wiki/Magic_number_(programming)) to uniquely identify the network you'd like to join. Bitboot then joins the existing [BitTorrent DHT](https://en.wikipedia.org/wiki/Mainline_DHT) (perhaps the largest and most reliable/stable DHT on the planet) and finds other nodes with the same magic name. It does this by selecting a rally point to hang out near based on the magic name where it will meet other nodes with the same magic name value. Also, the ID it uses is carefully selected so other nodes can pick it out as a bitboot peer based on the value of the magic name (in case other non-member nodes are hanging out around the rally point). 57 | 58 | _Note that while bitboot uses the BitTorrent DHT, it does not harm the existing network in any way (and, in fact, strengthens it by adding additional, fully functional nodes)._ 59 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = Bitboot 2 | 3 | var EventEmitter = require('events').EventEmitter 4 | var inherits = require('util').inherits 5 | var crypto = require('crypto') 6 | 7 | var DHT = require('bittorrent-dht') 8 | var KBucket = require('k-bucket') 9 | var debug = require('debug') 10 | var bitwise = require('bitwise') 11 | 12 | inherits(Bitboot, EventEmitter) 13 | 14 | function Bitboot (rallyName, opts) { 15 | if (!opts) opts = {} 16 | 17 | var self = this 18 | this.rallyName = rallyName 19 | this.rallyId = crypto.createHash('sha1').update(this.rallyName).digest() 20 | this.destroyed = false 21 | this.dht = null 22 | this._interval = null 23 | this.debug = debug('bitboot') 24 | 25 | EventEmitter.call(this) 26 | process.nextTick(bootstrap) 27 | this.debug('Using rally point ' + this.rallyName + ' (' + this.rallyId.toString('hex') + ')') 28 | 29 | function bootstrap () { 30 | if (self.destroyed) return 31 | 32 | var id = crypto.randomBytes(20) 33 | id[0] = self.rallyId[0] 34 | id[1] = self.rallyId[1] 35 | id[18] = self.rallyId[18] 36 | id[19] = self.rallyId[19] 37 | self.dht = self._createDHT(id, opts.bootstrap !== false) 38 | } 39 | } 40 | 41 | Bitboot.prototype.destroy = function (cb) { 42 | this.debug('destroying all connections') 43 | if (this.destroyed && cb) { 44 | process.nextTick(cb) 45 | } else if (!this.destroyed) { 46 | if (this._interval) clearInterval(this._interval) 47 | this.destroyed = true 48 | this.dht.destroy(cb) 49 | } 50 | } 51 | 52 | Bitboot.prototype._createDHT = function (id, bootstrap) { 53 | var dht = new DHT({id: id, bootstrap: bootstrap}) 54 | var self = this 55 | var dmsg = 'Joining network with id ' + id.toString('hex') + 56 | ' (distance to rally ' + KBucket.distance(id, this.rallyId) + ')' 57 | this.debug(dmsg) 58 | 59 | dht.on('error', onerror) 60 | dht.on('ready', onready) 61 | 62 | function onerror (err) { 63 | self.emit('error', err) 64 | } 65 | 66 | function onready () { 67 | self.emit('rejoin', dht.nodeId) 68 | if (!self._interval && bootstrap) { 69 | // first run, so search and set interval for future searches 70 | self.debug('Searching, and creating interval for future searches') 71 | search() 72 | self._interval = setInterval(search, 1000 * 60) 73 | } 74 | } 75 | 76 | function search () { 77 | self._search() 78 | } 79 | 80 | return dht 81 | } 82 | 83 | Bitboot.prototype._search = function () { 84 | // get all nodes close to the rally point 85 | this.debug('Searching for nodes near rally point') 86 | 87 | var self = this 88 | var query = {q: 'find_node', a: {id: this.dht.nodeId, target: this.rallyId}} 89 | this.dht._rpc.closest(this.rallyId, query, null, finished) 90 | 91 | function finished (err) { 92 | if (err) self.emit('error', err) 93 | else self._attemptCommunication() 94 | } 95 | } 96 | 97 | Bitboot.prototype._attemptCommunication = function () { 98 | if (this.dht.nodes.count() === 0) { 99 | this.emit('error', 'Could not connect to any nodes on the DHT. Are you connected to the internet?') 100 | return 101 | } 102 | var closest = this.dht.nodes.closest(this.rallyId, 40) 103 | var peers = this._extractPeers(closest) 104 | var mydist = KBucket.distance(this.dht.nodeId, this.rallyId) 105 | var closestdist = KBucket.distance(closest[0].id, this.rallyId) 106 | 107 | // emit all peers found 108 | this.emit('peers', peers) 109 | 110 | if (mydist <= closestdist) { 111 | this.debug('We are already at the rally point. Keep hanging out.') 112 | } else if (peers.length === 0) { 113 | this.debug('No peers found, so it\'s up to us to hang out at the rally point') 114 | this.debug('The closest other node is ' + closest[0].id.toString('hex')) 115 | this._waddleCloser(closest) 116 | } else if (!peers[0].id.equals(closest[0].id) && mydist > closestdist) { 117 | this.debug('Peers exist but far away - it\'s up to us to go to the rally point') 118 | this._waddleCloser(closest) 119 | } else if (peers[0].id.equals(closest[0].id)) { 120 | this.debug('Another peer is already at the rally point - so we won\'t move.') 121 | } 122 | } 123 | 124 | Bitboot.prototype._waddleCloser = function (closest) { 125 | var self = this 126 | this.dht.destroy(restart) 127 | this.debug('Waddling closer to the rally point destroying old DHT connection') 128 | 129 | function restart () { 130 | self.debug('Old DHT connection destroyed') 131 | var id = self._twiddleMarch(closest[0].id) 132 | self.dht = self._createDHT(id, true) 133 | self.dht.on('ready', sayhi) 134 | } 135 | 136 | // make sure other close nodes know about us 137 | function sayhi () { 138 | self.debug('Saying hi to ' + closest.length + ' nodes so they know about us') 139 | for (var i = 0; i < closest.length; i++) { 140 | self.dht.addNode(closest[i]) 141 | } 142 | } 143 | } 144 | 145 | Bitboot.prototype._extractPeers = function (nodes) { 146 | var peers = [] 147 | for (var i = 0; i < nodes.length; i++) { 148 | var validid = nodes[i].id[18] === this.rallyId[18] && nodes[i].id[19] === this.rallyId[19] 149 | if (validid && !nodes[i].id.equals(this.dht.nodeId)) { 150 | peers.push(nodes[i]) 151 | } 152 | } 153 | return peers 154 | } 155 | 156 | // This function moves an initial node id closer to the target node id 157 | // until it beats the closest id. It returns the new id as a buffer. 158 | Bitboot.prototype._twiddleMarch = function (closest) { 159 | var xdistance = KBucket.distance(closest, this.rallyId) 160 | var targetBits = bitwise.readBuffer(this.rallyId) 161 | var resultBits = bitwise.readBuffer(this.dht.nodeId) 162 | var result = null 163 | var rdistance = null 164 | 165 | for (var leftIndex = 0; leftIndex < resultBits.length; leftIndex++) { 166 | // next line - start at length-17 because -1 is last position, then move two bytes (16 bits) 167 | // to the left since the last two bytes are already going to match the target 168 | for (var rightIndex = resultBits.length - 17; rightIndex >= leftIndex; rightIndex--) { 169 | if (resultBits[rightIndex] !== targetBits[rightIndex]) { 170 | resultBits[rightIndex] = targetBits[rightIndex] 171 | result = bitwise.createBuffer(resultBits) 172 | rdistance = KBucket.distance(result, this.rallyId) 173 | if (rdistance < xdistance) return result 174 | 175 | if (rightIndex !== leftIndex) { 176 | resultBits[rightIndex] = bitwise.not([resultBits[rightIndex]])[0] 177 | result = bitwise.createBuffer(resultBits) 178 | } 179 | } 180 | } 181 | } 182 | 183 | return result 184 | } 185 | --------------------------------------------------------------------------------