├── .gitignore ├── benchmarks ├── distance.js ├── add.js └── closest.js ├── test ├── indexOf.js ├── defaultDistance.js ├── createKBucket.js ├── toArray.js ├── count.js ├── toIterable.js ├── get.js ├── determineNode.js ├── remove.js ├── update.js ├── split.js ├── add.js └── closest.js ├── .github └── FUNDING.yml ├── LICENSE ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /benchmarks/distance.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const KBucket = require('../index') 3 | 4 | const _0000000100100100 = Buffer.from('0124', 'hex') 5 | const _0100000000100100 = Buffer.from('4024', 'hex') 6 | 7 | const hrtime = process.hrtime() 8 | for (let i = 0; i < 1e7; i++) { 9 | KBucket.distance(_0000000100100100, _0100000000100100) 10 | } 11 | const diff = process.hrtime(hrtime) 12 | console.log(diff[0] * 1e9 + diff[1]) 13 | -------------------------------------------------------------------------------- /benchmarks/add.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | const KBucket = require('../') 5 | 6 | function getNextId () { 7 | seed = crypto.createHash('sha256').update(seed).digest() 8 | return seed 9 | } 10 | 11 | let seed = process.env.SEED || crypto.randomBytes(32).toString('hex') 12 | console.log('Seed: ' + seed) 13 | getNextId() 14 | 15 | console.time('KBucket.add') 16 | for (let i = 0; i < 1e1; ++i) { 17 | const bucket = new KBucket() 18 | for (let j = 0; j < 1e4; ++j) bucket.add({ id: getNextId() }) 19 | } 20 | console.timeEnd('KBucket.add') 21 | console.log('Memory: ', process.memoryUsage()) 22 | -------------------------------------------------------------------------------- /test/indexOf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | test('indexOf returns a contact with id that contains the same byte sequence as the test contact', function (t) { 6 | const kBucket = new KBucket() 7 | kBucket.add({ id: Buffer.from('a') }) 8 | t.same(kBucket._indexOf(kBucket.root, Buffer.from('a')), 0) 9 | t.end() 10 | }) 11 | 12 | test('indexOf returns -1 if contact is not found', function (t) { 13 | const kBucket = new KBucket() 14 | kBucket.add({ id: Buffer.from('a') }) 15 | t.same(kBucket._indexOf(kBucket.root, Buffer.from('b')), -1) 16 | t.end() 17 | }) 18 | -------------------------------------------------------------------------------- /benchmarks/closest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | const KBucket = require('../') 5 | 6 | function getNextId () { 7 | seed = crypto.createHash('sha256').update(seed).digest() 8 | return seed 9 | } 10 | 11 | let seed = process.env.SEED || crypto.randomBytes(32).toString('hex') 12 | console.log('Seed: ' + seed) 13 | getNextId() 14 | const bucket = new KBucket() 15 | for (let j = 0; j < 1e4; ++j) bucket.add({ id: getNextId() }) 16 | 17 | console.time('KBucket.closest') 18 | for (let i = 0; i < 1e4; i++) { 19 | bucket.closest(seed, 10) 20 | } 21 | console.timeEnd('KBucket.closest') 22 | console.log('Memory: ', process.memoryUsage()) 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [tristanls] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /test/defaultDistance.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | const bucket = new KBucket() 6 | 7 | test('distance between 00000000 and 00000000 is 00000000', function (t) { 8 | t.same(bucket.distance(Buffer.from([0x00]), Buffer.from([0x00])), 0) 9 | t.end() 10 | }) 11 | 12 | test('distance between 00000000 and 00000001 is 00000001', function (t) { 13 | t.same(bucket.distance(Buffer.from([0x00]), Buffer.from([0x01])), 1) 14 | t.end() 15 | }) 16 | 17 | test('distance between 00000010 and 00000001 is 00000011', function (t) { 18 | t.same(bucket.distance(Buffer.from([0x02]), Buffer.from([0x01])), 3) 19 | t.end() 20 | }) 21 | 22 | test('distance between 00000000 and 0000000000000000 is 0000000011111111', function (t) { 23 | t.same(bucket.distance(Buffer.from([0x00]), Buffer.from([0x00, 0x00])), 255) 24 | t.end() 25 | }) 26 | 27 | test('distance between 0000000100100100 and 0100000000100100 is 0100000100000000', function (t) { 28 | t.same(bucket.distance(Buffer.from([0x01, 0x24]), Buffer.from([0x40, 0x24])), 16640) 29 | t.end() 30 | }) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | "MIT License" 2 | 3 | Copyright (c) Tristan Slominski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/createKBucket.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const EventEmitter = require('events').EventEmitter 3 | const test = require('tape') 4 | const KBucket = require('../') 5 | 6 | test('localNodeId should be a random SHA-1 if not provided', function (t) { 7 | const kBucket = new KBucket() 8 | t.true(kBucket.localNodeId instanceof Buffer) 9 | t.same(kBucket.localNodeId.length, 20) // SHA-1 is 160 bits (20 bytes) 10 | t.end() 11 | }) 12 | 13 | test('localNodeId is a Buffer populated from options if options.localNodeId Buffer is provided', function (t) { 14 | const localNodeId = Buffer.from('some length') 15 | const kBucket = new KBucket({ localNodeId: localNodeId }) 16 | t.true(kBucket.localNodeId instanceof Buffer) 17 | t.true(localNodeId.equals(kBucket.localNodeId)) 18 | t.end() 19 | }) 20 | 21 | test('throws exception if options.localNodeId is a String', function (t) { 22 | t.throws(function () { 23 | return new KBucket({ localNodeId: 'some identifier' }) 24 | }) 25 | t.end() 26 | }) 27 | 28 | test('check root node', function (t) { 29 | const kBucket = new KBucket() 30 | t.same(kBucket.root, { contacts: [], dontSplit: false, left: null, right: null }) 31 | t.end() 32 | }) 33 | 34 | test('inherits from EventEmitter', function (t) { 35 | const kBucket = new KBucket() 36 | t.true(kBucket instanceof EventEmitter) 37 | t.end() 38 | }) 39 | -------------------------------------------------------------------------------- /test/toArray.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | test('toArray should return empty array if no contacts', function (t) { 6 | const kBucket = new KBucket() 7 | t.same(kBucket.toArray().length, 0) 8 | t.end() 9 | }) 10 | 11 | test('toArray should return all contacts in an array arranged from low to high buckets', function (t) { 12 | t.plan(22) 13 | const kBucket = new KBucket({ localNodeId: Buffer.from([0x00, 0x00]) }) 14 | const expectedIds = [] 15 | let i 16 | for (i = 0; i < kBucket.numberOfNodesPerKBucket; ++i) { 17 | kBucket.add({ id: Buffer.from([0x80, i]) }) // make sure all go into "far away" bucket 18 | expectedIds.push(0x80 * 256 + i) 19 | } 20 | // cause a split to happen 21 | kBucket.add({ id: Buffer.from([0x00, 0x80, i - 1]) }) 22 | // console.log(require('util').inspect(kBucket, {depth: null})) 23 | const contacts = kBucket.toArray() 24 | // console.log(require('util').inspect(contacts, {depth: null})) 25 | t.same(contacts.length, kBucket.numberOfNodesPerKBucket + 1) 26 | t.same(parseInt(contacts[0].id.toString('hex'), 16), 0x80 * 256 + i - 1) 27 | contacts.shift() // get rid of low bucket contact 28 | for (i = 0; i < kBucket.numberOfNodesPerKBucket; ++i) { 29 | t.same(parseInt(contacts[i].id.toString('hex'), 16), expectedIds[i]) 30 | } 31 | t.end() 32 | }) 33 | -------------------------------------------------------------------------------- /test/count.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | test('count returns 0 when no contacts in bucket', function (t) { 6 | const kBucket = new KBucket() 7 | t.same(kBucket.count(), 0) 8 | t.end() 9 | }) 10 | 11 | test('count returns 1 when 1 contact in bucket', function (t) { 12 | const kBucket = new KBucket() 13 | const contact = { id: Buffer.from('a') } 14 | kBucket.add(contact) 15 | t.same(kBucket.count(), 1) 16 | t.end() 17 | }) 18 | 19 | test('count returns 1 when same contact added to bucket twice', function (t) { 20 | const kBucket = new KBucket() 21 | const contact = { id: Buffer.from('a') } 22 | kBucket.add(contact) 23 | kBucket.add(contact) 24 | t.same(kBucket.count(), 1) 25 | t.end() 26 | }) 27 | 28 | test('count returns number of added unique contacts', function (t) { 29 | const kBucket = new KBucket() 30 | kBucket.add({ id: Buffer.from('a') }) 31 | kBucket.add({ id: Buffer.from('a') }) 32 | kBucket.add({ id: Buffer.from('b') }) 33 | kBucket.add({ id: Buffer.from('b') }) 34 | kBucket.add({ id: Buffer.from('c') }) 35 | kBucket.add({ id: Buffer.from('d') }) 36 | kBucket.add({ id: Buffer.from('c') }) 37 | kBucket.add({ id: Buffer.from('d') }) 38 | kBucket.add({ id: Buffer.from('e') }) 39 | kBucket.add({ id: Buffer.from('f') }) 40 | t.same(kBucket.count(), 6) 41 | t.end() 42 | }) 43 | -------------------------------------------------------------------------------- /test/toIterable.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | function collect (iterable) { 6 | const out = [] 7 | 8 | for (const thing of iterable) { 9 | out.push(thing) 10 | } 11 | 12 | return out 13 | } 14 | 15 | test('toIterable should return empty iterable if no contacts', function (t) { 16 | const kBucket = new KBucket() 17 | t.same(collect(kBucket.toIterable()).length, 0) 18 | t.end() 19 | }) 20 | 21 | test('toIterable should return all contacts in an iterable arranged from low to high buckets', function (t) { 22 | t.plan(22) 23 | const kBucket = new KBucket({ localNodeId: Buffer.from([0x00, 0x00]) }) 24 | const expectedIds = [] 25 | let i 26 | for (i = 0; i < kBucket.numberOfNodesPerKBucket; ++i) { 27 | kBucket.add({ id: Buffer.from([0x80, i]) }) // make sure all go into "far away" bucket 28 | expectedIds.push(0x80 * 256 + i) 29 | } 30 | // cause a split to happen 31 | kBucket.add({ id: Buffer.from([0x00, 0x80, i - 1]) }) 32 | // console.log(require('util').inspect(kBucket, {depth: null})) 33 | const contacts = collect(kBucket.toArray()) 34 | // console.log(require('util').inspect(contacts, {depth: null})) 35 | t.same(contacts.length, kBucket.numberOfNodesPerKBucket + 1) 36 | t.same(parseInt(contacts[0].id.toString('hex'), 16), 0x80 * 256 + i - 1) 37 | contacts.shift() // get rid of low bucket contact 38 | for (i = 0; i < kBucket.numberOfNodesPerKBucket; ++i) { 39 | t.same(parseInt(contacts[i].id.toString('hex'), 16), expectedIds[i]) 40 | } 41 | t.end() 42 | }) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "k-bucket", 3 | "version": "5.1.0", 4 | "description": "Kademlia DHT K-bucket implementation as a binary tree", 5 | "keywords": [ 6 | "k-bucket", 7 | "kademlia", 8 | "dht" 9 | ], 10 | "bugs": { 11 | "url": "https://github.com/tristanls/k-bucket/issues" 12 | }, 13 | "license": "MIT", 14 | "contributors": [ 15 | "Tristan Slominski ", 16 | "Mike de Boer ", 17 | "Conrad Pankoff ", 18 | "Feross Aboukhadijeh ", 19 | "Nathan Hernandez ", 20 | "Fabien O'Carroll ", 21 | "Kirill Fomichev ", 22 | "Robert Kowalski ", 23 | "Nazar Mokrynskyi ", 24 | "Jimmy Wärting ", 25 | "Alex Potsides " 26 | ], 27 | "main": "./index.js", 28 | "repository": { 29 | "type": "git", 30 | "url": "git@github.com:tristanls/k-bucket.git" 31 | }, 32 | "scripts": { 33 | "benchmark:add": "node benchmarks/add.js", 34 | "benchmark:closest": "node benchmarks/closest.js", 35 | "benchmark:distance": "node benchmarks/distance.js", 36 | "coverage": "nyc tape test/*.js", 37 | "lint": "standard", 38 | "test": "npm run lint && npm run unit", 39 | "unit": "tape test/*.js" 40 | }, 41 | "dependencies": { 42 | "randombytes": "^2.1.0" 43 | }, 44 | "devDependencies": { 45 | "nyc": "^15.1.0", 46 | "standard": "^16.0.3", 47 | "tape": "^5.1.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/get.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | test('throws TypeError if id is not a Uint8Array', function (t) { 6 | const kBucket = new KBucket() 7 | t.throws(function () { 8 | kBucket.get('foo') 9 | }) 10 | t.end() 11 | }) 12 | 13 | test('get retrieves null if no contacts', function (t) { 14 | const kBucket = new KBucket() 15 | t.same(kBucket.get(Buffer.from('foo')), null) 16 | t.end() 17 | }) 18 | 19 | test('get retrieves a contact that was added', function (t) { 20 | const kBucket = new KBucket() 21 | const contact = { id: Buffer.from('a') } 22 | kBucket.add(contact) 23 | t.true(Buffer.from('a').equals(kBucket.get(Buffer.from('a')).id)) 24 | t.end() 25 | }) 26 | 27 | test('get retrieves most recently added contact if same id', function (t) { 28 | const kBucket = new KBucket() 29 | const contact = { id: Buffer.from('a'), foo: 'foo', bar: ':p', vectorClock: 0 } 30 | const contact2 = { id: Buffer.from('a'), foo: 'bar', vectorClock: 1 } 31 | kBucket.add(contact) 32 | kBucket.add(contact2) 33 | t.true(Buffer.from('a').equals(kBucket.get(Buffer.from('a')).id)) 34 | t.same(kBucket.get(Buffer.from('a')).foo, 'bar') 35 | t.same(kBucket.get(Buffer.from('a')).bar, undefined) 36 | t.end() 37 | }) 38 | 39 | test('get retrieves contact from nested leaf node', function (t) { 40 | const kBucket = new KBucket({ localNodeId: Buffer.from([0x00, 0x00]) }) 41 | let i 42 | for (i = 0; i < kBucket.numberOfNodesPerKBucket; ++i) { 43 | kBucket.add({ id: Buffer.from([0x80, i]) }) // make sure all go into "far away" bucket 44 | } 45 | // cause a split to happen 46 | kBucket.add({ id: Buffer.from([0x00, i]), find: 'me' }) 47 | t.same(kBucket.get(Buffer.from([0x00, i])).find, 'me') 48 | t.end() 49 | }) 50 | -------------------------------------------------------------------------------- /test/determineNode.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | const LEFT_NODE = 0 6 | const RIGHT_NODE = 1 7 | const ROOT_NODE = { left: LEFT_NODE, right: RIGHT_NODE } 8 | 9 | test('id 00000000, bitIndex 0, should be low', function (t) { 10 | const kBucket = new KBucket() 11 | t.same(kBucket._determineNode(ROOT_NODE, Buffer.from([0x00]), 0), LEFT_NODE) 12 | t.end() 13 | }) 14 | 15 | test('id 01000000, bitIndex 0, should be low', function (t) { 16 | const kBucket = new KBucket() 17 | t.same(kBucket._determineNode(ROOT_NODE, Buffer.from([0x40]), 0), LEFT_NODE) 18 | t.end() 19 | }) 20 | 21 | test('id 01000000, bitIndex 1, should be high', function (t) { 22 | const kBucket = new KBucket() 23 | t.same(kBucket._determineNode(ROOT_NODE, Buffer.from([0x40]), 1), RIGHT_NODE) 24 | t.end() 25 | }) 26 | 27 | test('id 01000000, bitIndex 2, should be low', function (t) { 28 | const kBucket = new KBucket() 29 | t.same(kBucket._determineNode(ROOT_NODE, Buffer.from([0x40]), 2), LEFT_NODE) 30 | t.end() 31 | }) 32 | 33 | test('id 01000000, bitIndex 9, should be low', function (t) { 34 | const kBucket = new KBucket() 35 | t.same(kBucket._determineNode(ROOT_NODE, Buffer.from([0x40]), 9), LEFT_NODE) 36 | t.end() 37 | }) 38 | 39 | test('id 01000001, bitIndex 7, should be high', function (t) { 40 | const kBucket = new KBucket() 41 | t.same(kBucket._determineNode(ROOT_NODE, Buffer.from([0x41]), 7), RIGHT_NODE) 42 | t.end() 43 | }) 44 | 45 | test('id 0100000100000000, bitIndex 7, should be high', function (t) { 46 | const kBucket = new KBucket() 47 | t.same(kBucket._determineNode(ROOT_NODE, Buffer.from([0x41, 0x00]), 7), RIGHT_NODE) 48 | t.end() 49 | }) 50 | 51 | test('id 000000000100000100000000, bitIndex 15, should be high', function (t) { 52 | const kBucket = new KBucket() 53 | t.same(kBucket._determineNode(ROOT_NODE, Buffer.from([0x00, 0x41, 0x00]), 15), RIGHT_NODE) 54 | t.end() 55 | }) 56 | -------------------------------------------------------------------------------- /test/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | test('throws TypeError if contact.id is not a Uint8Array', function (t) { 6 | const kBucket = new KBucket() 7 | const contact = { id: 'foo' } 8 | t.throws(function () { 9 | kBucket.remove(contact.id) 10 | }) 11 | t.end() 12 | }) 13 | 14 | test('removing a contact should remove contact from nested buckets', function (t) { 15 | const kBucket = new KBucket({ localNodeId: Buffer.from([0x00, 0x00]) }) 16 | let i 17 | for (i = 0; i < kBucket.numberOfNodesPerKBucket; ++i) { 18 | kBucket.add({ id: Buffer.from([0x80, i]) }) // make sure all go into "far away" bucket 19 | } 20 | // cause a split to happen 21 | kBucket.add({ id: Buffer.from([0x00, i]) }) 22 | // console.log(require('util').inspect(kBucket, false, null)) 23 | const contactToDelete = { id: Buffer.from([0x80, 0x00]) } 24 | t.same(kBucket._indexOf(kBucket.root.right, contactToDelete.id), 0) 25 | kBucket.remove(Buffer.from([0x80, 0x00])) 26 | t.same(kBucket._indexOf(kBucket.root.right, contactToDelete.id), -1) 27 | t.end() 28 | }) 29 | 30 | test('should generate "removed"', function (t) { 31 | t.plan(1) 32 | const kBucket = new KBucket() 33 | const contact = { id: Buffer.from('a') } 34 | kBucket.on('removed', function (removedContact) { 35 | t.same(removedContact, contact) 36 | t.end() 37 | }) 38 | kBucket.add(contact) 39 | kBucket.remove(contact.id) 40 | }) 41 | 42 | test('should generate event "removed" when removing from a split bucket', function (t) { 43 | t.plan(2) 44 | const kBucket = new KBucket({ 45 | localNodeId: Buffer.from('') // need non-random localNodeId for deterministic splits 46 | }) 47 | for (let i = 0; i < kBucket.numberOfNodesPerKBucket + 1; ++i) { 48 | kBucket.add({ id: Buffer.from('' + i) }) 49 | } 50 | t.false(kBucket.bucket) 51 | const contact = { id: Buffer.from('a') } 52 | kBucket.on('removed', function (removedContact) { 53 | t.same(removedContact, contact) 54 | t.end() 55 | }) 56 | kBucket.add(contact) 57 | kBucket.remove(contact.id) 58 | }) 59 | -------------------------------------------------------------------------------- /test/update.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | test('invalid index results in exception', function (t) { 6 | const kBucket = new KBucket() 7 | const contact = { id: Buffer.from('a') } 8 | kBucket.add(contact) 9 | t.throws(function () { 10 | kBucket._update(contact, 1) 11 | }) 12 | t.end() 13 | }) 14 | 15 | test('deprecated vectorClock results in contact drop', function (t) { 16 | const kBucket = new KBucket() 17 | const contact = { id: Buffer.from('a'), vectorClock: 3 } 18 | kBucket.add(contact) 19 | kBucket._update(kBucket.root, 0, { id: Buffer.from('a'), vectorClock: 2 }) 20 | t.same(kBucket.root.contacts[0].vectorClock, 3) 21 | t.end() 22 | }) 23 | 24 | test('equal vectorClock results in contact marked as most recent', function (t) { 25 | const kBucket = new KBucket() 26 | const contact = { id: Buffer.from('a'), vectorClock: 3 } 27 | kBucket.add(contact) 28 | kBucket.add({ id: Buffer.from('b') }) 29 | kBucket._update(kBucket.root, 0, contact) 30 | t.same(kBucket.root.contacts[1], contact) 31 | t.end() 32 | }) 33 | 34 | test('more recent vectorClock results in contact update and contact being marked as most recent', function (t) { 35 | const kBucket = new KBucket() 36 | const contact = { id: Buffer.from('a'), old: 'property', vectorClock: 3 } 37 | kBucket.add(contact) 38 | kBucket.add({ id: Buffer.from('b') }) 39 | kBucket._update(kBucket.root, 0, { id: Buffer.from('a'), newer: 'property', vectorClock: 4 }) 40 | t.true(contact.id.equals(kBucket.root.contacts[1].id)) 41 | t.same(kBucket.root.contacts[1].vectorClock, 4) 42 | t.same(kBucket.root.contacts[1].old, undefined) 43 | t.same(kBucket.root.contacts[1].newer, 'property') 44 | t.end() 45 | }) 46 | 47 | test('should generate "updated"', function (t) { 48 | t.plan(2) 49 | const kBucket = new KBucket() 50 | const contact1 = { id: Buffer.from('a'), vectorClock: 1 } 51 | const contact2 = { id: Buffer.from('a'), vectorClock: 2 } 52 | kBucket.on('updated', function (oldContact, newContact) { 53 | t.same(oldContact, contact1) 54 | t.same(newContact, contact2) 55 | t.end() 56 | }) 57 | kBucket.add(contact1) 58 | kBucket.add(contact2) 59 | }) 60 | 61 | test('should generate event "updated" when updating a split node', function (t) { 62 | t.plan(3) 63 | const kBucket = new KBucket({ 64 | localNodeId: Buffer.from('') // need non-random localNodeId for deterministic splits 65 | }) 66 | for (let i = 0; i < kBucket.numberOfNodesPerKBucket + 1; ++i) { 67 | kBucket.add({ id: Buffer.from('' + i) }) 68 | } 69 | t.false(kBucket.bucket) 70 | const contact1 = { id: Buffer.from('a'), vectorClock: 1 } 71 | const contact2 = { id: Buffer.from('a'), vectorClock: 2 } 72 | kBucket.on('updated', function (oldContact, newContact) { 73 | t.same(oldContact, contact1) 74 | t.same(newContact, contact2) 75 | t.end() 76 | }) 77 | kBucket.add(contact1) 78 | kBucket.add(contact2) 79 | }) 80 | -------------------------------------------------------------------------------- /test/split.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | test('adding a contact does not split node', function (t) { 6 | const kBucket = new KBucket() 7 | kBucket.add({ id: Buffer.from('a') }) 8 | t.same(kBucket.root.left, null) 9 | t.same(kBucket.root.right, null) 10 | t.notSame(kBucket.root.contacts, null) 11 | t.end() 12 | }) 13 | 14 | test('adding maximum number of contacts (per node) [20] into node does not split node', function (t) { 15 | const kBucket = new KBucket() 16 | for (let i = 0; i < kBucket.numberOfNodesPerKBucket; ++i) { 17 | kBucket.add({ id: Buffer.from('' + i) }) 18 | } 19 | t.same(kBucket.root.left, null) 20 | t.same(kBucket.root.right, null) 21 | t.notSame(kBucket.root.contacts, null) 22 | t.end() 23 | }) 24 | 25 | test('adding maximum number of contacts (per node) + 1 [21] into node splits the node', function (t) { 26 | const kBucket = new KBucket() 27 | for (let i = 0; i < kBucket.numberOfNodesPerKBucket + 1; ++i) { 28 | kBucket.add({ id: Buffer.from('' + i) }) 29 | } 30 | t.notSame(kBucket.root.left, null) 31 | t.notSame(kBucket.root.right, null) 32 | t.same(kBucket.root.contacts, null) 33 | t.end() 34 | }) 35 | 36 | test('split nodes contain all added contacts', function (t) { 37 | t.plan(20 /* numberOfNodesPerKBucket */ + 2) 38 | const kBucket = new KBucket({ localNodeId: Buffer.from([0x00]) }) 39 | const foundContact = {} 40 | for (let i = 0; i < kBucket.numberOfNodesPerKBucket + 1; ++i) { 41 | kBucket.add({ id: Buffer.from([i]) }) 42 | foundContact[i] = false 43 | } 44 | const traverse = function (node) { 45 | if (node.contacts === null) { 46 | traverse(node.left) 47 | traverse(node.right) 48 | } else { 49 | node.contacts.forEach(function (contact) { 50 | foundContact[parseInt(contact.id.toString('hex'), 16)] = true 51 | }) 52 | } 53 | } 54 | traverse(kBucket.root) 55 | Object.keys(foundContact).forEach(function (key) { t.true(foundContact[key], key) }) 56 | t.same(kBucket.root.contacts, null) 57 | t.end() 58 | }) 59 | 60 | test('when splitting nodes the "far away" node should be marked to prevent splitting "far away" node', function (t) { 61 | t.plan(5) 62 | const kBucket = new KBucket({ localNodeId: Buffer.from([0x00]) }) 63 | for (let i = 0; i < kBucket.numberOfNodesPerKBucket + 1; ++i) { 64 | kBucket.add({ id: Buffer.from([i]) }) 65 | } 66 | // above algorithm will split left node 4 times and put 0x00 through 0x0f 67 | // in the left node, and put 0x10 through 0x14 in right node 68 | // since localNodeId is 0x00, we expect every right node to be "far" and 69 | // therefore marked as "dontSplit = true" 70 | // there will be one "left" node and four "right" nodes (t.expect(5)) 71 | const traverse = function (node, dontSplit) { 72 | if (node.contacts === null) { 73 | traverse(node.left, false) 74 | traverse(node.right, true) 75 | } else { 76 | if (dontSplit) t.true(node.dontSplit) 77 | else t.false(node.dontSplit) 78 | } 79 | } 80 | traverse(kBucket.root) 81 | t.end() 82 | }) 83 | -------------------------------------------------------------------------------- /test/add.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | test('throws TypeError if contact has not property id', function (t) { 6 | t.throws(function () { 7 | (new KBucket()).add(null) 8 | }, /^TypeError: contact.id is not a Uint8Array$/) 9 | t.end() 10 | }) 11 | 12 | test('throws TypeError if contact.id is not a Uint8Array', function (t) { 13 | t.throws(function () { 14 | (new KBucket()).add({ id: 'foo' }) 15 | }, /^TypeError: contact.id is not a Uint8Array$/) 16 | t.end() 17 | }) 18 | 19 | test('adding a contact places it in root node', function (t) { 20 | const kBucket = new KBucket() 21 | const contact = { id: Buffer.from('a') } 22 | kBucket.add(contact) 23 | t.same(kBucket.root.contacts, [contact]) 24 | t.end() 25 | }) 26 | 27 | test('adding an existing contact does not increase number of contacts in root node', function (t) { 28 | const kBucket = new KBucket() 29 | const contact = { id: Buffer.from('a') } 30 | kBucket.add(contact) 31 | kBucket.add({ id: Buffer.from('a') }) 32 | t.same(kBucket.root.contacts.length, 1) 33 | t.end() 34 | }) 35 | 36 | test('adding same contact moves it to the end of the root node (most-recently-contacted end)', function (t) { 37 | const kBucket = new KBucket() 38 | const contact = { id: Buffer.from('a') } 39 | kBucket.add(contact) 40 | t.same(kBucket.root.contacts.length, 1) 41 | kBucket.add({ id: Buffer.from('b') }) 42 | t.same(kBucket.root.contacts.length, 2) 43 | t.true(kBucket.root.contacts[0] === contact) // least-recently-contacted end 44 | kBucket.add(contact) 45 | t.same(kBucket.root.contacts.length, 2) 46 | t.true(kBucket.root.contacts[1] === contact) // most-recently-contacted end 47 | t.end() 48 | }) 49 | 50 | test('adding contact to bucket that can\'t be split results in calling "ping" callback', function (t) { 51 | t.plan(3 /* numberOfNodesToPing */ + 2) 52 | const kBucket = new KBucket({ localNodeId: Buffer.from([0x00, 0x00]) }) 53 | let j 54 | kBucket.on('ping', function (contacts, replacement) { 55 | t.same(contacts.length, kBucket.numberOfNodesToPing) 56 | // console.dir(kBucket.root.right.contacts[0]) 57 | for (let i = 0; i < kBucket.numberOfNodesToPing; ++i) { 58 | // the least recently contacted end of the node should be pinged 59 | t.true(contacts[i] === kBucket.root.right.contacts[i]) 60 | } 61 | t.same(replacement, { id: Buffer.from([0x80, j]) }) 62 | t.end() 63 | }) 64 | for (j = 0; j < kBucket.numberOfNodesPerKBucket + 1; ++j) { 65 | kBucket.add({ id: Buffer.from([0x80, j]) }) // make sure all go into "far away" node 66 | } 67 | }) 68 | 69 | test('should generate event "added" once', function (t) { 70 | t.plan(1) 71 | const kBucket = new KBucket() 72 | const contact = { id: Buffer.from('a') } 73 | kBucket.on('added', function (newContact) { 74 | t.same(newContact, contact) 75 | }) 76 | kBucket.add(contact) 77 | kBucket.add(contact) 78 | t.end() 79 | }) 80 | 81 | test('should generate event "added" when adding to a split node', function (t) { 82 | t.plan(2) 83 | const kBucket = new KBucket({ 84 | localNodeId: Buffer.from('') // need non-random localNodeId for deterministic splits 85 | }) 86 | for (let i = 0; i < kBucket.numberOfNodesPerKBucket + 1; ++i) { 87 | kBucket.add({ id: Buffer.from('' + i) }) 88 | } 89 | t.same(kBucket.root.contacts, null) 90 | const contact = { id: Buffer.from('a') } 91 | kBucket.on('added', function (newContact) { 92 | t.same(newContact, contact) 93 | }) 94 | kBucket.add(contact) 95 | t.end() 96 | }) 97 | -------------------------------------------------------------------------------- /test/closest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const KBucket = require('../') 4 | 5 | test('throws TypeError if contact.id is not a Uint8Array', function (t) { 6 | t.throws(function () { 7 | (new KBucket()).closest('foo', 4) 8 | }, /^TypeError: id is not a Uint8Array$/) 9 | t.end() 10 | }) 11 | 12 | test('throw TypeError if n is not number', function (t) { 13 | t.throws(function () { 14 | (new KBucket()).closest(Buffer.from([42]), null) 15 | }, /^TypeError: n is not positive number$/) 16 | t.end() 17 | }) 18 | 19 | test('closest nodes are returned', function (t) { 20 | const kBucket = new KBucket() 21 | for (let i = 0; i < 0x12; ++i) kBucket.add({ id: Buffer.from([i]) }) 22 | const contact = { id: Buffer.from([0x15]) } // 00010101 23 | const contacts = kBucket.closest(contact.id, 3) 24 | t.same(contacts.length, 3) 25 | t.same(contacts[0].id, Buffer.from([0x11])) // distance: 00000100 26 | t.same(contacts[1].id, Buffer.from([0x10])) // distance: 00000101 27 | t.same(contacts[2].id, Buffer.from([0x05])) // distance: 00010000 28 | t.end() 29 | }) 30 | 31 | test('n is Infinity by default', function (t) { 32 | const kBucket = new KBucket({ localNodeId: Buffer.from([0x00, 0x00]) }) 33 | for (let i = 0; i < 1e3; ++i) kBucket.add({ id: Buffer.from([~~(i / 256), i % 256]) }) 34 | t.true(kBucket.closest(Buffer.from([0x80, 0x80])).length > 100) 35 | t.end() 36 | }) 37 | 38 | test('closest nodes are returned (including exact match)', function (t) { 39 | const kBucket = new KBucket() 40 | for (let i = 0; i < 0x12; ++i) kBucket.add({ id: Buffer.from([i]) }) 41 | const contact = { id: Buffer.from([0x11]) } // 00010001 42 | const contacts = kBucket.closest(contact.id, 3) 43 | t.same(contacts[0].id, Buffer.from([0x11])) // distance: 00000000 44 | t.same(contacts[1].id, Buffer.from([0x10])) // distance: 00000001 45 | t.same(contacts[2].id, Buffer.from([0x01])) // distance: 00010000 46 | t.end() 47 | }) 48 | 49 | test('closest nodes are returned even if there isn\'t enough in one bucket', function (t) { 50 | const kBucket = new KBucket({ localNodeId: Buffer.from([0x00, 0x00]) }) 51 | for (let i = 0; i < kBucket.numberOfNodesPerKBucket; i++) { 52 | kBucket.add({ id: Buffer.from([0x80, i]) }) 53 | kBucket.add({ id: Buffer.from([0x01, i]) }) 54 | } 55 | kBucket.add({ id: Buffer.from([0x00, 0x01]) }) 56 | const contact = { id: Buffer.from([0x00, 0x03]) } // 0000000000000011 57 | const contacts = kBucket.closest(contact.id, 22) 58 | t.same(contacts[0].id, Buffer.from([0x00, 0x01])) // distance: 0000000000000010 59 | t.same(contacts[1].id, Buffer.from([0x01, 0x03])) // distance: 0000000100000000 60 | t.same(contacts[2].id, Buffer.from([0x01, 0x02])) // distance: 0000000100000010 61 | t.same(contacts[3].id, Buffer.from([0x01, 0x01])) 62 | t.same(contacts[4].id, Buffer.from([0x01, 0x00])) 63 | t.same(contacts[5].id, Buffer.from([0x01, 0x07])) 64 | t.same(contacts[6].id, Buffer.from([0x01, 0x06])) 65 | t.same(contacts[7].id, Buffer.from([0x01, 0x05])) 66 | t.same(contacts[8].id, Buffer.from([0x01, 0x04])) 67 | t.same(contacts[9].id, Buffer.from([0x01, 0x0b])) 68 | t.same(contacts[10].id, Buffer.from([0x01, 0x0a])) 69 | t.same(contacts[11].id, Buffer.from([0x01, 0x09])) 70 | t.same(contacts[12].id, Buffer.from([0x01, 0x08])) 71 | t.same(contacts[13].id, Buffer.from([0x01, 0x0f])) 72 | t.same(contacts[14].id, Buffer.from([0x01, 0x0e])) 73 | t.same(contacts[15].id, Buffer.from([0x01, 0x0d])) 74 | t.same(contacts[16].id, Buffer.from([0x01, 0x0c])) 75 | t.same(contacts[17].id, Buffer.from([0x01, 0x13])) 76 | t.same(contacts[18].id, Buffer.from([0x01, 0x12])) 77 | t.same(contacts[19].id, Buffer.from([0x01, 0x11])) 78 | t.same(contacts[20].id, Buffer.from([0x01, 0x10])) 79 | t.same(contacts[21].id, Buffer.from([0x80, 0x03])) // distance: 1000000000000000 80 | // console.log(require('util').inspect(kBucket, false, null)) 81 | t.end() 82 | }) 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k-bucket 2 | 3 | _Stability: 2 - [Stable](https://github.com/tristanls/stability-index#stability-2---stable)_ 4 | 5 | [![NPM version](https://badge.fury.io/js/k-bucket.png)](http://npmjs.org/package/k-bucket) 6 | 7 | Kademlia DHT K-bucket implementation as a binary tree. 8 | 9 | ## Contributors 10 | 11 | [@tristanls](https://github.com/tristanls), [@mikedeboer](https://github.com/mikedeboer), [@deoxxa](https://github.com/deoxxa), [@feross](https://github.com/feross), [@nathanph](https://github.com/nathanph), [@allouis](https://github.com/allouis), [@fanatid](https://github.com/fanatid), [@robertkowalski](https://github.com/robertkowalski), [@nazar-pc](https://github.com/nazar-pc), [@jimmywarting](https://github.com/jimmywarting), [@achingbrain](https://github.com/achingbrain) 12 | 13 | ## Installation 14 | 15 | npm install k-bucket 16 | 17 | ## Tests 18 | 19 | npm test 20 | 21 | ## Usage 22 | 23 | ```javascript 24 | const KBucket = require('k-bucket') 25 | 26 | const kBucket1 = new KBucket({ 27 | localNodeId: Buffer.from('my node id') // default: random data 28 | }) 29 | // or without using Buffer (for example, in the browser) 30 | const id = 'my node id' 31 | const nodeId = new Uint8Array(id.length) 32 | for (let i = 0, len = nodeId.length; i < len; ++i) { 33 | nodeId[i] = id.charCodeAt(i) 34 | } 35 | const kBucket2 = new KBucket({ 36 | localNodeId: nodeId // default: random data 37 | }) 38 | ``` 39 | 40 | ## Overview 41 | 42 | A [*Distributed Hash Table (DHT)*](http://en.wikipedia.org/wiki/Distributed_hash_table) is a decentralized distributed system that provides a lookup table similar to a hash table. 43 | 44 | *k-bucket* is an implementation of a storage mechanism for keys within a DHT. It stores `contact` objects which represent locations and addresses of nodes in the decentralized distributed system. `contact` objects are typically identified by a SHA-1 hash, however this restriction is lifted in this implementation. Additionally, node ids of different lengths can be compared. 45 | 46 | This Kademlia DHT k-bucket implementation is meant to be as minimal as possible. It assumes that `contact` objects consist only of `id`. It is useful, and necessary, to attach other properties to a `contact`. For example, one may want to attach `ip` and `port` properties, which allow an application to send IP traffic to the `contact`. However, this information is extraneous and irrelevant to the operation of a k-bucket. 47 | 48 | ### arbiter function 49 | 50 | This *k-bucket* implementation implements a conflict resolution mechanism using an `arbiter` function. The purpose of the `arbiter` is to choose between two `contact` objects with the same `id` but perhaps different properties and determine which one should be stored. As the `arbiter` function returns the actual object to be stored, it does not need to make an either/or choice, but instead could perform some sort of operation and return the result as a new object that would then be stored. See [kBucket._update(node, index, contact)](#kbucket_updatenode-index-contact) for detailed semantics of which `contact` (`incumbent` or `candidate`) is selected. 51 | 52 | For example, an `arbiter` function implementing a `vectorClock` mechanism would look something like: 53 | 54 | ```javascript 55 | // contact example 56 | var contact = { 57 | id: Buffer.from('contactId'), 58 | vectorClock: 0 59 | }; 60 | 61 | function arbiter(incumbent, candidate) { 62 | if (incumbent.vectorClock > candidate.vectorClock) { 63 | return incumbent; 64 | } 65 | return candidate; 66 | }; 67 | ``` 68 | 69 | Alternatively, consider an arbiter that implements a Grow-Only-Set CRDT mechanism: 70 | 71 | ```javascript 72 | // contact example 73 | const contact = { 74 | id: Buffer.from('workerService'), 75 | workerNodes: { 76 | '17asdaf7effa2': { host: '127.0.0.1', port: 1337 }, 77 | '17djsyqeryasu': { host: '127.0.0.1', port: 1338 } 78 | } 79 | }; 80 | 81 | function arbiter(incumbent, candidate) { 82 | // we create a new object so that our selection is guaranteed to replace 83 | // the incumbent 84 | const merged = { 85 | id: incumbent.id, // incumbent.id === candidate.id within an arbiter 86 | workerNodes: incumbent.workerNodes 87 | } 88 | 89 | Object.keys(candidate.workerNodes).forEach(workerNodeId => { 90 | merged.workerNodes[workerNodeId] = candidate.workerNodes[workerNodeId]; 91 | }) 92 | 93 | return merged 94 | } 95 | ``` 96 | 97 | Notice that in the above case, the Grow-Only-Set assumes that each worker node has a globally unique id. 98 | 99 | ## Documentation 100 | 101 | ### KBucket 102 | 103 | Implementation of a Kademlia DHT k-bucket used for storing contact (peer node) information. 104 | 105 | For a step by step example of k-bucket operation you may find the following slideshow useful: [Distribute All The Things](https://docs.google.com/presentation/d/11qGZlPWu6vEAhA7p3qsQaQtWH7KofEC9dMeBFZ1gYeA/edit#slide=id.g1718cc2bc_0661). 106 | 107 | KBucket starts off as a single k-bucket with capacity of _k_. As contacts are added, once the _k+1_ contact is added, the k-bucket is split into two k-buckets. The split happens according to the first bit of the contact node id. The k-bucket that would contain the local node id is the "near" k-bucket, and the other one is the "far" k-bucket. The "far" k-bucket is marked as _don't split_ in order to prevent further splitting. The contact nodes that existed are then redistributed along the two new k-buckets and the old k-bucket becomes an inner node within a tree data structure. 108 | 109 | As even more contacts are added to the "near" k-bucket, the "near" k-bucket will split again as it becomes full. However, this time it is split along the second bit of the contact node id. Again, the two newly created k-buckets are marked "near" and "far" and the "far" k-bucket is marked as _don't split_. Again, the contact nodes that existed in the old bucket are redistributed. This continues as long as nodes are being added to the "near" k-bucket, until the number of splits reaches the length of the local node id. 110 | 111 | As more contacts are added to the "far" k-bucket and it reaches its capacity, it does not split. Instead, the k-bucket emits a "ping" event (register a listener: `kBucket.on('ping', function (oldContacts, newContact) {...});` and includes an array of old contact nodes that it hasn't heard from in a while and requires you to confirm that those contact nodes still respond (literally respond to a PING RPC). If an old contact node still responds, it should be re-added (`kBucket.add(oldContact)`) back to the k-bucket. This puts the old contact on the "recently heard from" end of the list of nodes in the k-bucket. If the old contact does not respond, it should be removed (`kBucket.remove(oldContact.id)`) and the new contact being added now has room to be stored (`kBucket.add(newContact)`). 112 | 113 | **Public API** 114 | * [KBucket.arbiter(incumbent, candidate)](#kbucketarbiterincumbent-candidate) 115 | * [KBucket.distance(firstId, secondId)](#kbucketdistancefirstid-secondid) 116 | * [new KBucket(options)](#new-kbucketoptions) 117 | * [kBucket.add(contact)](#kbucketaddcontact) 118 | * [kBucket.closest(id [, n = Infinity])](#kbucketclosestid--n--infinity) 119 | * [kBucket.count()](#kbucketcount) 120 | * [kBucket.get(id)](#kbucketgetid) 121 | * [kBucket.metadata](#kbucketmetadata) 122 | * [kBucket.remove(id)](#kbucketremoveid) 123 | * [kBucket.toArray()](#kbuckettoarray) 124 | * [kBucket.toIterable()](#kbuckettoiterable) 125 | * [Event 'added'](#event-added) 126 | * [Event 'ping'](#event-ping) 127 | * [Event 'removed'](#event-removed) 128 | * [Event 'updated'](#event-updated) 129 | 130 | #### KBucket.arbiter(incumbent, candidate) 131 | 132 | * `incumbent`: _Object_ Contact currently stored in the k-bucket. 133 | * `candidate`: _Object_ Contact being added to the k-bucket. 134 | * Return: _Object_ Contact to updated the k-bucket with. 135 | 136 | Default arbiter function for contacts with the same `id`. Uses `contact.vectorClock` to select which contact to update the k-bucket with. Contact with larger `vectorClock` field will be selected. If `vectorClock` is the same, `candidat` will be selected. 137 | 138 | #### KBucket.distance(firstId, secondId) 139 | 140 | * `firstId`: _Uint8Array_ Uint8Array containing first id. 141 | * `secondId`: _Uint8Array_ Uint8Array containing second id. 142 | * Return: _Integer_ The XOR distance between `firstId` and `secondId`. 143 | 144 | Default distance function. Finds the XOR distance between firstId and secondId. 145 | 146 | #### new KBucket(options) 147 | 148 | * `options`: 149 | * `arbiter`: _Function_ _(Default: vectorClock arbiter)_ 150 | `function (incumbent, candidate) { return contact; }` An optional `arbiter` function that given two `contact` objects with the same `id` returns the desired object to be used for updating the k-bucket. For more details, see [arbiter function](#arbiter-function). 151 | * `distance`: _Function_ 152 | `function (firstId, secondId) { return distance }` An optional `distance` function that gets two `id` Uint8Arrays and return distance (as number) between them. 153 | * `localNodeId`: _Uint8Array_ An optional Uint8Array representing the local node id. If not provided, a local node id will be created via `crypto.randomBytes(20)`. 154 | * `metadata`: _Object_ _(Default: {})_ Optional satellite data to include with the k-bucket. `metadata` property is guaranteed not be altered, it is provided as an explicit container for users of k-bucket to store implementation-specific data. 155 | * `numberOfNodesPerKBucket`: _Integer_ _(Default: 20)_ The number of nodes that a k-bucket can contain before being full or split. 156 | * `numberOfNodesToPing`: _Integer_ _(Default: 3)_ The number of nodes to ping when a bucket that should not be split becomes full. KBucket will emit a `ping` event that contains `numberOfNodesToPing` nodes that have not been contacted the longest. 157 | 158 | Creates a new KBucket. 159 | 160 | #### kBucket.add(contact) 161 | 162 | * `contact`: _Object_ The contact object to add. 163 | * `id`: _Uint8Array_ Contact node id. 164 | * Any satellite data that is part of the `contact` object will not be altered, only `id` is used. 165 | * Return: _Object_ The k-bucket itself. 166 | 167 | Adds a `contact` to the k-bucket. 168 | 169 | #### kBucket.closest(id [, n = Infinity]) 170 | 171 | * `id`: _Uint8Array_ Contact node id. 172 | * `n`: _Integer_ _(Default: Infinity)_ The maximum number of closest contacts to return. 173 | * Return: _Array_ Maximum of `n` closest contacts to the node id. 174 | 175 | Get the `n` closest contacts to the provided node id. "Closest" here means: closest according to the XOR metric of the `contact` node id. 176 | 177 | #### kBucket.count() 178 | 179 | * Return: _Number_ The number of contacts held in the tree 180 | 181 | Counts the total number of contacts in the tree. 182 | 183 | #### kBucket.get(id) 184 | 185 | * `id`: _Uint8Array_ The ID of the `contact` to fetch. 186 | * Return: _Object_ The `contact` if available, otherwise null 187 | 188 | Retrieves the `contact`. 189 | 190 | #### kBucket.metadata 191 | 192 | * `metadata`: _Object_ _(Default: {})_ 193 | 194 | The `metadata` property serves as a container that can be used by implementations using k-bucket. One example is storing a timestamp to indicate the last time when a node in the bucket was responding to a ping. 195 | 196 | #### kBucket.remove(id) 197 | 198 | * `id`: _Uint8Array_ The ID of the `contact` to remove. 199 | * Return: _Object_ The k-bucket itself. 200 | 201 | Removes `contact` with the provided `id`. 202 | 203 | #### kBucket.toArray() 204 | 205 | * Return: _Array_ All of the contacts in the tree, as an array 206 | 207 | Traverses the tree, putting all the contacts into one array. 208 | 209 | #### kBucket.toIterable() 210 | 211 | * Return: _Iterable_ All of the contacts in the tree, as an iterable 212 | 213 | Traverses the tree, yielding contacts as they are encountered. 214 | 215 | #### kBucket._determineNode(node, id [, bitIndex = 0]) 216 | 217 | _**CAUTION: reserved for internal use**_ 218 | 219 | * `node`: internal object that has 2 leafs: left and right 220 | * `id`: _Uint8Array_ Id to compare `localNodeId` with. 221 | * `bitIndex`: _Integer_ _(Default: 0)_ The bit index to which bit to check in the `id` Uint8Array. 222 | * Return: _Object_ left leaf if `id` at `bitIndex` is 0, right leaf otherwise. 223 | 224 | #### kBucket._indexOf(id) 225 | 226 | _**CAUTION: reserved for internal use**_ 227 | 228 | * `node`: internal object that has 2 leafs: left and right 229 | * `id`: _Uint8Array_ Contact node id. 230 | * Return: _Integer_ Index of `contact` with provided `id` if it exists, -1 otherwise. 231 | 232 | Returns the index of the `contact` with provided `id` if it exists, returns -1 otherwise. 233 | 234 | #### kBucket._split(node [, bitIndex]) 235 | 236 | _**CAUTION: reserved for internal use**_ 237 | 238 | * `node`: _Object_ node for splitting 239 | * `bitIndex`: _Integer_ _(Default: 0)_ The bit index to which bit to check in the `id` Uint8Array. 240 | 241 | Splits the node, redistributes contacts to the new nodes, and marks the node that was split as an inner node of the binary tree of nodes by setting `self.contacts = null`. Also, marks the "far away" node as `dontSplit`. 242 | 243 | #### kBucket._update(node, index, contact) 244 | 245 | _**CAUTION: reserved for internal use**_ 246 | 247 | * `node`: internal object that has 2 leafs: left and right 248 | * `index`: _Integer_ The index in the bucket where contact exists (index has already been computed in previous calculation). 249 | * `contact`: _Object_ The contact object to update. 250 | * `id`: _Uint8Array_ Contact node id 251 | * Any satellite data that is part of the `contact` object will not be altered, only `id` is used. 252 | 253 | Updates the `contact` by using the `arbiter` function to compare the incumbent and the candidate. If `arbiter` function selects the old `contact` but the candidate is some new `contact`, then the new `contact` is abandoned. If `arbiter` function selects the old `contact` and the candidate is that same old `contact`, the `contact` is marked as most recently contacted (by being moved to the right/end of the bucket array). If `arbiter` function selects the new `contact`, the old `contact` is removed and the new `contact` is marked as most recently contacted. 254 | 255 | #### Event: 'added' 256 | 257 | * `newContact`: _Object_ The new contact that was added. 258 | 259 | Emitted only when `newContact` was added to bucket and it was not stored in the bucket before. 260 | 261 | #### Event: 'ping' 262 | 263 | * `oldContacts`: _Array_ The array of contacts to ping. 264 | * `newContact`: _Object_ The new contact to be added if one of old contacts does not respond. 265 | 266 | Emitted every time a contact is added that would exceed the capacity of a _don't split_ k-bucket it belongs to. 267 | 268 | #### Event: 'removed' 269 | 270 | * `contact`: _Object_ The contact that was removed. 271 | 272 | Emitted when `contact` was removed from the bucket. 273 | 274 | #### Event: 'updated' 275 | 276 | * `oldContact`: _Object_ The contact that was stored prior to the update. 277 | * `newContact`: _Object_ The new contact that is now stored after the update. 278 | 279 | Emitted when a previously existing ("previously existing" means `oldContact.id` equals `newContact.id`) contact was added to the bucket and it was replaced with `newContact`. 280 | 281 | ## Releases 282 | 283 | [Current releases](https://github.com/tristanls/k-bucket/releases). 284 | 285 | ### Policy 286 | 287 | We follow the semantic versioning policy ([semver.org](http://semver.org/)) with a caveat: 288 | 289 | > Given a version number MAJOR.MINOR.PATCH, increment the: 290 | > 291 | >MAJOR version when you make incompatible API changes,
292 | >MINOR version when you add functionality in a backwards-compatible manner, and
293 | >PATCH version when you make backwards-compatible bug fixes. 294 | 295 | **caveat**: Major version zero is a special case indicating development version that may make incompatible API changes without incrementing MAJOR version. 296 | 297 | ## Sources 298 | 299 | The implementation has been sourced from: 300 | 301 | - [A formal specification of the Kademlia distributed hash table](http://maude.sip.ucm.es/kademlia/files/pita_kademlia.pdf) 302 | - [Distributed Hash Tables (part 2)](https://web.archive.org/web/20140217064545/http://offthelip.org/?p=157) 303 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | index.js - Kademlia DHT K-bucket implementation as a binary tree. 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2013-2021 Tristan Slominski 7 | 8 | Permission is hereby granted, free of charge, to any person 9 | obtaining a copy of this software and associated documentation 10 | files (the "Software"), to deal in the Software without 11 | restriction, including without limitation the rights to use, 12 | copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following 15 | conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 'use strict' 30 | 31 | const randomBytes = require('randombytes') 32 | const { EventEmitter } = require('events') 33 | 34 | /** 35 | * @param {Uint8Array} array1 36 | * @param {Uint8Array} array2 37 | * @return {Boolean} 38 | */ 39 | function arrayEquals (array1, array2) { 40 | if (array1 === array2) { 41 | return true 42 | } 43 | if (array1.length !== array2.length) { 44 | return false 45 | } 46 | for (let i = 0, length = array1.length; i < length; ++i) { 47 | if (array1[i] !== array2[i]) { 48 | return false 49 | } 50 | } 51 | return true 52 | } 53 | 54 | function createNode () { 55 | return { contacts: [], dontSplit: false, left: null, right: null } 56 | } 57 | 58 | function ensureInt8 (name, val) { 59 | if (!(val instanceof Uint8Array)) { 60 | throw new TypeError(name + ' is not a Uint8Array') 61 | } 62 | } 63 | 64 | /** 65 | * Implementation of a Kademlia DHT k-bucket used for storing 66 | * contact (peer node) information. 67 | * 68 | * @extends EventEmitter 69 | */ 70 | class KBucket extends EventEmitter { 71 | /** 72 | * `options`: 73 | * `distance`: _Function_ 74 | * `function (firstId, secondId) { return distance }` An optional 75 | * `distance` function that gets two `id` Uint8Arrays 76 | * and return distance (as number) between them. 77 | * `arbiter`: _Function_ _(Default: vectorClock arbiter)_ 78 | * `function (incumbent, candidate) { return contact; }` An optional 79 | * `arbiter` function that givent two `contact` objects with the same `id` 80 | * returns the desired object to be used for updating the k-bucket. For 81 | * more details, see [arbiter function](#arbiter-function). 82 | * `localNodeId`: _Uint8Array_ An optional Uint8Array representing the local node id. 83 | * If not provided, a local node id will be created via `randomBytes(20)`. 84 | * `metadata`: _Object_ _(Default: {})_ Optional satellite data to include 85 | * with the k-bucket. `metadata` property is guaranteed not be altered by, 86 | * it is provided as an explicit container for users of k-bucket to store 87 | * implementation-specific data. 88 | * `numberOfNodesPerKBucket`: _Integer_ _(Default: 20)_ The number of nodes 89 | * that a k-bucket can contain before being full or split. 90 | * `numberOfNodesToPing`: _Integer_ _(Default: 3)_ The number of nodes to 91 | * ping when a bucket that should not be split becomes full. KBucket will 92 | * emit a `ping` event that contains `numberOfNodesToPing` nodes that have 93 | * not been contacted the longest. 94 | * 95 | * @param {Object=} options optional 96 | */ 97 | constructor (options = {}) { 98 | super() 99 | 100 | this.localNodeId = options.localNodeId || randomBytes(20) 101 | this.numberOfNodesPerKBucket = options.numberOfNodesPerKBucket || 20 102 | this.numberOfNodesToPing = options.numberOfNodesToPing || 3 103 | this.distance = options.distance || KBucket.distance 104 | // use an arbiter from options or vectorClock arbiter by default 105 | this.arbiter = options.arbiter || KBucket.arbiter 106 | this.metadata = Object.assign({}, options.metadata) 107 | 108 | ensureInt8('option.localNodeId as parameter 1', this.localNodeId) 109 | 110 | this.root = createNode() 111 | } 112 | 113 | /** 114 | * Default arbiter function for contacts with the same id. Uses 115 | * contact.vectorClock to select which contact to update the k-bucket with. 116 | * Contact with larger vectorClock field will be selected. If vectorClock is 117 | * the same, candidat will be selected. 118 | * 119 | * @param {Object} incumbent Contact currently stored in the k-bucket. 120 | * @param {Object} candidate Contact being added to the k-bucket. 121 | * @return {Object} Contact to updated the k-bucket with. 122 | */ 123 | static arbiter (incumbent, candidate) { 124 | return incumbent.vectorClock > candidate.vectorClock ? incumbent : candidate 125 | } 126 | 127 | /** 128 | * Default distance function. Finds the XOR 129 | * distance between firstId and secondId. 130 | * 131 | * @param {Uint8Array} firstId Uint8Array containing first id. 132 | * @param {Uint8Array} secondId Uint8Array containing second id. 133 | * @return {Number} Integer The XOR distance between firstId 134 | * and secondId. 135 | */ 136 | static distance (firstId, secondId) { 137 | let distance = 0 138 | let i = 0 139 | const min = Math.min(firstId.length, secondId.length) 140 | const max = Math.max(firstId.length, secondId.length) 141 | for (; i < min; ++i) { 142 | distance = distance * 256 + (firstId[i] ^ secondId[i]) 143 | } 144 | for (; i < max; ++i) distance = distance * 256 + 255 145 | return distance 146 | } 147 | 148 | /** 149 | * Adds a contact to the k-bucket. 150 | * 151 | * @param {Object} contact the contact object to add 152 | */ 153 | add (contact) { 154 | ensureInt8('contact.id', (contact || {}).id) 155 | 156 | let bitIndex = 0 157 | let node = this.root 158 | 159 | while (node.contacts === null) { 160 | // this is not a leaf node but an inner node with 'low' and 'high' 161 | // branches; we will check the appropriate bit of the identifier and 162 | // delegate to the appropriate node for further processing 163 | node = this._determineNode(node, contact.id, bitIndex++) 164 | } 165 | 166 | // check if the contact already exists 167 | const index = this._indexOf(node, contact.id) 168 | if (index >= 0) { 169 | this._update(node, index, contact) 170 | return this 171 | } 172 | 173 | if (node.contacts.length < this.numberOfNodesPerKBucket) { 174 | node.contacts.push(contact) 175 | this.emit('added', contact) 176 | return this 177 | } 178 | 179 | // the bucket is full 180 | if (node.dontSplit) { 181 | // we are not allowed to split the bucket 182 | // we need to ping the first this.numberOfNodesToPing 183 | // in order to determine if they are alive 184 | // only if one of the pinged nodes does not respond, can the new contact 185 | // be added (this prevents DoS flodding with new invalid contacts) 186 | this.emit('ping', node.contacts.slice(0, this.numberOfNodesToPing), contact) 187 | return this 188 | } 189 | 190 | this._split(node, bitIndex) 191 | return this.add(contact) 192 | } 193 | 194 | /** 195 | * Get the n closest contacts to the provided node id. "Closest" here means: 196 | * closest according to the XOR metric of the contact node id. 197 | * 198 | * @param {Uint8Array} id Contact node id 199 | * @param {Number=} n Integer (Default: Infinity) The maximum number of 200 | * closest contacts to return 201 | * @return {Array} Array Maximum of n closest contacts to the node id 202 | */ 203 | closest (id, n = Infinity) { 204 | ensureInt8('id', id) 205 | 206 | if ((!Number.isInteger(n) && n !== Infinity) || n <= 0) { 207 | throw new TypeError('n is not positive number') 208 | } 209 | 210 | let contacts = [] 211 | 212 | for (let nodes = [this.root], bitIndex = 0; nodes.length > 0 && contacts.length < n;) { 213 | const node = nodes.pop() 214 | if (node.contacts === null) { 215 | const detNode = this._determineNode(node, id, bitIndex++) 216 | nodes.push(node.left === detNode ? node.right : node.left) 217 | nodes.push(detNode) 218 | } else { 219 | contacts = contacts.concat(node.contacts) 220 | } 221 | } 222 | 223 | return contacts 224 | .map(a => [this.distance(a.id, id), a]) 225 | .sort((a, b) => a[0] - b[0]) 226 | .slice(0, n) 227 | .map(a => a[1]) 228 | } 229 | 230 | /** 231 | * Counts the total number of contacts in the tree. 232 | * 233 | * @return {Number} The number of contacts held in the tree 234 | */ 235 | count () { 236 | // return this.toArray().length 237 | let count = 0 238 | for (const nodes = [this.root]; nodes.length > 0;) { 239 | const node = nodes.pop() 240 | if (node.contacts === null) nodes.push(node.right, node.left) 241 | else count += node.contacts.length 242 | } 243 | return count 244 | } 245 | 246 | /** 247 | * Determines whether the id at the bitIndex is 0 or 1. 248 | * Return left leaf if `id` at `bitIndex` is 0, right leaf otherwise 249 | * 250 | * @param {Object} node internal object that has 2 leafs: left and right 251 | * @param {Uint8Array} id Id to compare localNodeId with. 252 | * @param {Number} bitIndex Integer (Default: 0) The bit index to which bit 253 | * to check in the id Uint8Array. 254 | * @return {Object} left leaf if id at bitIndex is 0, right leaf otherwise. 255 | */ 256 | _determineNode (node, id, bitIndex) { 257 | // **NOTE** remember that id is a Uint8Array and has granularity of 258 | // bytes (8 bits), whereas the bitIndex is the _bit_ index (not byte) 259 | 260 | // id's that are too short are put in low bucket (1 byte = 8 bits) 261 | // (bitIndex >> 3) finds how many bytes the bitIndex describes 262 | // bitIndex % 8 checks if we have extra bits beyond byte multiples 263 | // if number of bytes is <= no. of bytes described by bitIndex and there 264 | // are extra bits to consider, this means id has less bits than what 265 | // bitIndex describes, id therefore is too short, and will be put in low 266 | // bucket 267 | const bytesDescribedByBitIndex = bitIndex >> 3 268 | const bitIndexWithinByte = bitIndex % 8 269 | if ((id.length <= bytesDescribedByBitIndex) && (bitIndexWithinByte !== 0)) { 270 | return node.left 271 | } 272 | 273 | const byteUnderConsideration = id[bytesDescribedByBitIndex] 274 | 275 | // byteUnderConsideration is an integer from 0 to 255 represented by 8 bits 276 | // where 255 is 11111111 and 0 is 00000000 277 | // in order to find out whether the bit at bitIndexWithinByte is set 278 | // we construct (1 << (7 - bitIndexWithinByte)) which will consist 279 | // of all bits being 0, with only one bit set to 1 280 | // for example, if bitIndexWithinByte is 3, we will construct 00010000 by 281 | // (1 << (7 - 3)) -> (1 << 4) -> 16 282 | if (byteUnderConsideration & (1 << (7 - bitIndexWithinByte))) { 283 | return node.right 284 | } 285 | 286 | return node.left 287 | } 288 | 289 | /** 290 | * Get a contact by its exact ID. 291 | * If this is a leaf, loop through the bucket contents and return the correct 292 | * contact if we have it or null if not. If this is an inner node, determine 293 | * which branch of the tree to traverse and repeat. 294 | * 295 | * @param {Uint8Array} id The ID of the contact to fetch. 296 | * @return {Object|Null} The contact if available, otherwise null 297 | */ 298 | get (id) { 299 | ensureInt8('id', id) 300 | 301 | let bitIndex = 0 302 | 303 | let node = this.root 304 | while (node.contacts === null) { 305 | node = this._determineNode(node, id, bitIndex++) 306 | } 307 | 308 | // index of uses contact id for matching 309 | const index = this._indexOf(node, id) 310 | return index >= 0 ? node.contacts[index] : null 311 | } 312 | 313 | /** 314 | * Returns the index of the contact with provided 315 | * id if it exists, returns -1 otherwise. 316 | * 317 | * @param {Object} node internal object that has 2 leafs: left and right 318 | * @param {Uint8Array} id Contact node id. 319 | * @return {Number} Integer Index of contact with provided id if it 320 | * exists, -1 otherwise. 321 | */ 322 | _indexOf (node, id) { 323 | for (let i = 0; i < node.contacts.length; ++i) { 324 | if (arrayEquals(node.contacts[i].id, id)) return i 325 | } 326 | 327 | return -1 328 | } 329 | 330 | /** 331 | * Removes contact with the provided id. 332 | * 333 | * @param {Uint8Array} id The ID of the contact to remove. 334 | * @return {Object} The k-bucket itself. 335 | */ 336 | remove (id) { 337 | ensureInt8('the id as parameter 1', id) 338 | 339 | let bitIndex = 0 340 | let node = this.root 341 | 342 | while (node.contacts === null) { 343 | node = this._determineNode(node, id, bitIndex++) 344 | } 345 | 346 | const index = this._indexOf(node, id) 347 | if (index >= 0) { 348 | const contact = node.contacts.splice(index, 1)[0] 349 | this.emit('removed', contact) 350 | } 351 | 352 | return this 353 | } 354 | 355 | /** 356 | * Splits the node, redistributes contacts to the new nodes, and marks the 357 | * node that was split as an inner node of the binary tree of nodes by 358 | * setting this.root.contacts = null 359 | * 360 | * @param {Object} node node for splitting 361 | * @param {Number} bitIndex the bitIndex to which byte to check in the 362 | * Uint8Array for navigating the binary tree 363 | */ 364 | _split (node, bitIndex) { 365 | node.left = createNode() 366 | node.right = createNode() 367 | 368 | // redistribute existing contacts amongst the two newly created nodes 369 | for (const contact of node.contacts) { 370 | this._determineNode(node, contact.id, bitIndex).contacts.push(contact) 371 | } 372 | 373 | node.contacts = null // mark as inner tree node 374 | 375 | // don't split the "far away" node 376 | // we check where the local node would end up and mark the other one as 377 | // "dontSplit" (i.e. "far away") 378 | const detNode = this._determineNode(node, this.localNodeId, bitIndex) 379 | const otherNode = node.left === detNode ? node.right : node.left 380 | otherNode.dontSplit = true 381 | } 382 | 383 | /** 384 | * Returns all the contacts contained in the tree as an array. 385 | * If this is a leaf, return a copy of the bucket. If this is not a leaf, 386 | * return the union of the low and high branches (themselves also as arrays). 387 | * 388 | * @return {Array} All of the contacts in the tree, as an array 389 | */ 390 | toArray () { 391 | let result = [] 392 | for (const nodes = [this.root]; nodes.length > 0;) { 393 | const node = nodes.pop() 394 | if (node.contacts === null) nodes.push(node.right, node.left) 395 | else result = result.concat(node.contacts) 396 | } 397 | return result 398 | } 399 | 400 | /** 401 | * Similar to `toArray()` but instead of buffering everything up into an 402 | * array before returning it, yields contacts as they are encountered while 403 | * walking the tree. 404 | * 405 | * @return {Iterable} All of the contacts in the tree, as an iterable 406 | */ 407 | * toIterable () { 408 | for (const nodes = [this.root]; nodes.length > 0;) { 409 | const node = nodes.pop() 410 | if (node.contacts === null) { 411 | nodes.push(node.right, node.left) 412 | } else { 413 | yield * node.contacts 414 | } 415 | } 416 | } 417 | 418 | /** 419 | * Updates the contact selected by the arbiter. 420 | * If the selection is our old contact and the candidate is some new contact 421 | * then the new contact is abandoned (not added). 422 | * If the selection is our old contact and the candidate is our old contact 423 | * then we are refreshing the contact and it is marked as most recently 424 | * contacted (by being moved to the right/end of the bucket array). 425 | * If the selection is our new contact, the old contact is removed and the new 426 | * contact is marked as most recently contacted. 427 | * 428 | * @param {Object} node internal object that has 2 leafs: left and right 429 | * @param {Number} index the index in the bucket where contact exists 430 | * (index has already been computed in a previous 431 | * calculation) 432 | * @param {Object} contact The contact object to update. 433 | */ 434 | _update (node, index, contact) { 435 | // sanity check 436 | if (!arrayEquals(node.contacts[index].id, contact.id)) { 437 | throw new Error('wrong index for _update') 438 | } 439 | 440 | const incumbent = node.contacts[index] 441 | const selection = this.arbiter(incumbent, contact) 442 | // if the selection is our old contact and the candidate is some new 443 | // contact, then there is nothing to do 444 | if (selection === incumbent && incumbent !== contact) return 445 | 446 | node.contacts.splice(index, 1) // remove old contact 447 | node.contacts.push(selection) // add more recent contact version 448 | this.emit('updated', incumbent, selection) 449 | } 450 | } 451 | 452 | module.exports = KBucket 453 | --------------------------------------------------------------------------------