UNPKG

netus-k-bucket

Version:

Kademlia DHT k-bucket implementation

349 lines (311 loc) 10.4 kB
// "MIT License" // // Copyright (c) Matthew Voss // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. var inherits = require('util').inherits var EventEmitter = require('events') // // Kadamlia DHT k-bucket for storing peer information // // All ids referred to in this class are buffers or arrays of bytes (i.e. IP addresses). // // opt: { // distance: override function that returns the XOR distance between two IDs // arbiter: override function. Given prev and new contact for an update, returns the one to be kept (stored) // root_id: the local node id. defaults to 0x800000... // metadata: optional extra data to store with the bucket. not touched by KBucket // bucket_len: number of nodes that a k-bucket can contain before being full or split. // num_to_ping: number of nodes to ping when a bucket that should not be split becomes full // } // function KBucket (opt) { opt = opt || {} EventEmitter.call(this) this.root_id = opt.root_id || opt.localNodeId || default_id(20) this.bucket_len = opt.bucket_len || opt.numberOfNodesPerKBucket || 20 this.num_to_ping = opt.num_to_ping || opt.numberOfNodesToPing || 3 this.distance = opt.distance || distance // use an arbiter from options or vectorClock arbiter by default this.arbiter = opt.arbiter || function (prev, candidate) { return prev.vtime > candidate.vtime ? prev : candidate } this.metadata = opt.metadata || {} this.root_id.length < 33 || err('ids cannot exceed 32 bytes') this.root = new Node() } KBucket.prototype = { constructor: KBucket, add: function (contact) { contact.id.length < 33 || err('ids cannot exceed 32 bytes') var bit = 0 var node = this.root var byte = 0 if (node.contacts === null) { // optimized loop var id = contact.id outer: for (byte = 0; byte < id.length; byte++) { for (bit = 0; bit < 8;) { node = (id[byte] & (0x80 >>> bit)) === 0 ? node.left : node.right bit++ if (node.contacts !== null) { break outer } } } } // update var index = node.index_of(contact.id) if (index !== -1) { var prev = node.contacts[index] var next = this.arbiter(prev, contact) if (next === prev && prev !== contact) return node.contacts.splice(index, 1) // remove old contact node.contacts.push(next) // add more recent contact version this.emit('updated', prev, next) return this } // add if (node.contacts.length < this.bucket_len) { node.contacts.push(contact) this.emit('added', contact) return this } // split if (!node.far) { node.split(byte, bit, this.root_id) this.add(contact) return this } // ping // only if one of the pinged nodes does not respond, can the new contact // be added (this prevents DoS flodding with new invalid contacts) this.emit('ping', node.contacts.slice(0, this.num_to_ping), contact) return this }, // return up to n closest contacts to the given id, according to the distance function closest: function (id, n) { if (n == null) { n = Infinity } n > 0 || err('expected positive count') var ret = [] var stack = [] var node = this.root var dist = this.distance // yes, the loop is ugly, but this version of the closest() function is running at 3x performance of the original version. outer: for (var byte = 0; byte < id.length; byte++) { for (var bit = 0; bit < 8;) { // optimized loop if (node.contacts === null) { if (id[byte] & (0x80 >>> bit)) { stack.push(node.left) node = node.right } else { stack.push(node.right) node = node.left } bit++ } else { var contracts = node.contacts for (var j=0; j<contracts.length; j++) { contracts[j]._dist = dist(contracts[j].id, id) ret.push(contracts[j]) // note: get the whole bucket of unordered contacts to sort and select below } if (ret.length >= n || stack.length === 0) { break outer } node = stack.pop() } } } ret.sort(function (a, b) { return a._dist - b._dist }) if (ret.length > n) { ret.length = n } // we slurped up more than required return ret }, // return the number of contracts count: function () { var ret = 0 var stack = [] var node = this.root while (true) { // optimized loop if (node.contacts === null) { stack.push(node.left) node = node.right } else { ret += node.contacts.length if (stack.length === 0) { return ret // RETURN } node = stack.pop() } } }, get: function (id) { var node = this.root.leaf(id) var index = node.index_of(id) return index === -1 ? null : node.contacts[index] }, remove: function (id) { var node = this.root.leaf(id) var index = node.index_of(id) if (index !== -1) { var contact = node.contacts.splice(index, 1)[0] this.emit('removed', contact) } }, // return all contacts as an array all_contacts: function () { return this.root.all_contacts() }, toArray: function () { return this.root.all_contacts() }, // backward compatible to_obj: function () { return this.root.to_obj() } } inherits(KBucket, EventEmitter) // return the XOR distance between two ids (buffers) // // According to http://www.maymounkov.org/papers/maymounkov-kademlia-lncs.pdf, the distance // measure (XOR) has these necessary properties: // // 1. d(x,x) = 0 // 2. d(x,y) > 0, if x != y // 3. forall x,y : d(x,y) = d(y,x) (symmetry) // 4. d(x,z) <= d(x,y) + d(y,z) (triangle inequality) // function distance (a, b) { // optimized. performs about 35% faster than the straight-forward version var ret = 0 var i = 0 var maxlen = a.length if (b.length === maxlen) { for(; i < maxlen; i++) { ret = (ret << 8) | (a[i] ^ b[i]) } } else { // handle different lengths var maxnode var minlen if (b.length > maxlen) { maxnode = b maxlen = b.length minlen = a.length } else { maxnode = a minlen = b.length // maxlen is set } for (; i < minlen; i++) { ret = (ret << 8) | (a[i] ^ b[i]) } for (; i < maxlen; i++) { ret = (ret << 8) | maxnode[i] } } return ret } function arr_equals (a, b) { if (a === b) { return true } if (a.length !== b.length) { return false } for (var i = 0, length = a.length; i < length; i++) { if (a[i] !== b[i]) { return false } } return true } function Node () { this.contacts = [] this.left = null this.right = null this.far = false this._dist = 0 } Node.prototype = { constructor: Node, // redistribute contacts to left/right nodes. split: function (byte, bit, root_id) { this.left = new Node() this.right = new Node() var contacts = this.contacts for (var i=0; i<contacts.length; i++) { var c = contacts[i] ;((c.id[byte] && (c.id[byte] & (0x80 >>> bit))) ? this.right : this.left).contacts.push(c) } this.contacts = null ;((root_id[byte] && (root_id[byte] & (0x80 >>> bit))) ? this.left : this.right).far = true }, index_of: function (id) { var c = this.contacts for (var i = 0; i < c.length; i++) { if (arr_equals(c[i].id, id)) return i } return -1 }, leaf: function (id) { var ret = this // optimized loop for (var byte = 0; byte < id.length; byte++) { for (var bit = 0; bit < 8;) { if (ret.contacts !== null) { return ret } ret = id[byte] & (0x80 >>> bit) ? ret.right : ret.left bit++ } } }, all_contacts: function () { var ret = [] var nodes = [this] while (nodes.length > 0) { var node = nodes.pop() if (node.contacts === null) nodes.push(node.right, node.left) else ret = ret.concat(node.contacts) } return ret }, // Converts the graph starting at the given node into condensed minimal object form with tiny names and values // to facilitate debugging and testing. to_obj: function () { var self = this var ret = {} if (self.contacts == null) { self.id == null || err('unexpected id: ' + self.id) if (!is_empty(self.left)) { ret.l = self.left.to_obj() } if (!is_empty(self.right)) { ret.r = self.right.to_obj() } } else { if (self.contacts.length) { var idstr = self.contacts.map(function (c) { !(c.right || c.left) || err('mixed node') return c.id.toString('hex').toUpperCase() }).join(',') ret.b = (self.far ? '!' : '') + idstr } } return ret } } var DEFAULT_ID = null function default_id (len) { if (!DEFAULT_ID) { var b = new Buffer(len) b[0] = 0x80 for (var i=1; i<len; i++) { b[i] = 0 } DEFAULT_ID = b } return DEFAULT_ID } function err (msg) { throw Error (msg) } function is_empty (n) { return n == null || (n.left == null && n.right == null && n.contacts.length === 0) } KBucket.distance = distance module.exports = KBucket