├── .github └── workflows │ └── test-node.yml ├── .gitignore ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── test.js /.github/workflows/test-node.yml: -------------------------------------------------------------------------------- 1 | name: Build Status 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | node-version: [lts/*] 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sandbox.js 3 | sandbox/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Mathias Buus 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kademlia-routing-table 2 | 3 | XOR distance based routing table used for P2P networks such as a Kademlia DHT. 4 | 5 | ``` 6 | npm install kademlia-routing-table 7 | ``` 8 | 9 | Similar to k-buckets, but implemented using the simplifications described in https://github.com/ethereum/wiki/wiki/Kademlia-Peer-Selection 10 | 11 | To understand the concept behind peer routing, DHTs, and the terms used here, 12 | I recommend reading the [Kademlia DHT paper](https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) as well. 13 | 14 | ## Usage 15 | 16 | ``` js 17 | const RoutingTable = require('kademlia-routing-table') 18 | const { randomBytes } = require('crypto') 19 | 20 | // Create a new table that stores nodes "close" to the passed in id. 21 | // The id should be uniformily distributed, ie a hash, random bytes etc. 22 | const table = new RoutingTable(randomBytes(32)) 23 | 24 | // Add a node to the routing table 25 | table.add({ 26 | id: randomBytes(32), // this field is required 27 | // populate with any other data you want to store 28 | }) 29 | 30 | table.on('row', function (row) { 31 | // A new row has been added to the routing table 32 | // This row represents row.index similar bits to the table.id 33 | 34 | row.on('full', function (node) { 35 | // The row is full and cannot be split, so node cannot be added. 36 | // If any of the nodes in the row are "worse", based on 37 | // some application specific metric then we should remove 38 | // the worst node from the row and re-add the node. 39 | }) 40 | }) 41 | 42 | // Get the 20 nodes "closest" to a passed in id 43 | const closest = table.closest(randomBytes(32), 20) 44 | ``` 45 | 46 | ## API 47 | 48 | #### `table = new RoutingTable(id, [options])` 49 | 50 | Create a new routing table. 51 | 52 | `id` should be a Buffer that is uniformily distributed. `options` include: 53 | 54 | ``` js 55 | { 56 | k: 20 // The max row size 57 | } 58 | ``` 59 | 60 | #### `bool = table.add(node)` 61 | 62 | Insert a new node. `node.id` must be a Buffer of same length as `table.id`. 63 | When inserting a node the XOR distance between the node and the table.id is 64 | calculated and used to figure which table row this node should be inserted into. 65 | 66 | Returns `true` if the node could be added to the corresponding row or `false` if not. 67 | If `false` is returned the onfullrow function is invoked for the corresponding row and node. 68 | 69 | #### `node = table.get(id)` 70 | 71 | Get a node from the table using its id. Returns `null` if no node has the passed in `id`. 72 | 73 | #### `bool = table.has(id)` 74 | 75 | Returns `true` if a node exists for the passed in `id` and `false` otherwise. 76 | 77 | #### `nodes = table.closest(id, [maxNodes])` 78 | 79 | Returns an array of the closest (in XOR distance) nodes to the passed in id. 80 | 81 | `id` should be Buffer of same length as `table.id`. Per default at max `k` 82 | nodes are returned, but this can be configured using the `maxNodes` argument. 83 | 84 | This method is normally used in a routing context, i.e. figuring out which nodes 85 | in a DHT should store a value based on its id. 86 | 87 | #### `bool = table.remove(id)` 88 | 89 | Remove a node using its id. Returns `true` if a node existed for the id and 90 | was removed and `false` otherwise. 91 | 92 | #### `node = table.random()` 93 | 94 | Get a random node from the table. 95 | 96 | #### `nodes = table.toArray()` 97 | 98 | Returns all nodes from table as an array. If you create a new routing table 99 | from these nodes it will be identical to the used here. 100 | 101 | #### `table.on('row', row)` 102 | 103 | Emitted when a new row is added to the routing table. At max, `bitLength(table.id)` 104 | will exist. 105 | 106 | #### `table.rows` 107 | 108 | A fixed size array of all rows in the table. Normally you would not need to worry 109 | about accessing rows directly outside the row event. 110 | 111 | ## Row API 112 | 113 | For the row passed in the the `onfullrow` function the following API exists. 114 | 115 | #### `row.index` 116 | 117 | The row index. Represents how many prefix bits are shared between nodes in the row 118 | and the table id. 119 | 120 | #### `row.nodes` 121 | 122 | A list of all the nodes in the row, sorted by their `id`. 123 | 124 | #### `row.data` 125 | 126 | Property set to null initially you can use if you want to store optional data on the row. 127 | 128 | #### `bool = row.add(node)` 129 | 130 | Same as `table.add` but for a specific row. Only use this to add the `newNode` 131 | passed in `onfullrow` function. 132 | 133 | #### `bool = row.remove(node)` 134 | 135 | Same as `table.remove` but for a specific row. Only use this to remove the 136 | "worst" node from the row when wanting to add the newNode. 137 | 138 | #### `row.on('add', node)` 139 | 140 | Emitted when a new node is added to this row. 141 | 142 | #### `row.on('remove', node)` 143 | 144 | Emitted when a node has been removed from this row. 145 | 146 | #### `row.on('full', node)` 147 | 148 | Emitted when a node wants to be added to this row, but the row is full (stores `k` nodes). 149 | 150 | When this happens you should check if any of the nodes already in the row (`row.nodes`) are 151 | "worse" than the passed node. If that is the case, remove the "worst" one and re-add the node passed in the arguments. 152 | 153 | Various algorithms can be implemented to handle full rows, which is why the routing table leaves most of this logic 154 | up to the user. These kind of algorithms include adding the rejected node to a cache and wait for another node in the 155 | row to be removed before trying to insert it again, or using an LRU cache to determine which node already in the row 156 | has been heard from yet. 157 | 158 | ## License 159 | 160 | MIT 161 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const RoutingTable = require('./') 2 | const { randomBytes } = require('crypto') 3 | 4 | const table = new RoutingTable(randomBytes(32)) 5 | 6 | table.on('row', function (row) { 7 | row.on('remove', function (node) { 8 | console.log('remove node', node, 'from row', row.index) 9 | }) 10 | 11 | row.on('add', function (node) { 12 | console.log('add node', node, 'to row', row.index) 13 | }) 14 | 15 | row.on('full', function (node) { 16 | console.log('row', row.index, 'is full, cannot add', node) 17 | }) 18 | }) 19 | 20 | for (let i = 0; i < 40; i++) { 21 | table.add({ 22 | id: randomBytes(32) 23 | }) 24 | } 25 | 26 | console.log('The 20 closest nodes to', table.id, 'are:', table.closest(20)) 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events') 2 | 3 | module.exports = class RoutingTable extends EventEmitter { 4 | constructor (id, opts) { 5 | if (!opts) opts = {} 6 | 7 | super() 8 | 9 | this.id = id 10 | this.k = opts.k || 20 11 | this.size = 0 12 | this.rows = new Array(id.length * 8) 13 | } 14 | 15 | add (node) { 16 | const i = this._diff(node.id) 17 | 18 | let row = this.rows[i] 19 | 20 | if (!row) { 21 | row = this.rows[i] = new Row(this, i) 22 | this.emit('row', row) 23 | } 24 | 25 | const len = row.nodes.length 26 | if (!row.add(node, this.k)) return false 27 | 28 | this.size += row.nodes.length - len 29 | return true 30 | } 31 | 32 | remove (id) { 33 | const i = this._diff(id) 34 | const row = this.rows[i] 35 | if (!row) return false 36 | if (!row.remove(id)) return false 37 | this.size-- 38 | return true 39 | } 40 | 41 | get (id) { 42 | const i = this._diff(id) 43 | const row = this.rows[i] 44 | if (!row) return null 45 | return row.get(id) 46 | } 47 | 48 | has (id) { 49 | return this.get(id) !== null 50 | } 51 | 52 | random () { 53 | let n = (Math.random() * this.size) | 0 54 | 55 | for (let i = 0; i < this.rows.length; i++) { 56 | const r = this.rows[i] 57 | if (!r) continue 58 | if (n < r.nodes.length) return r.nodes[n] 59 | n -= r.nodes.length 60 | } 61 | 62 | return null 63 | } 64 | 65 | closest (id, k) { 66 | if (!k) k = this.k 67 | 68 | const result = [] 69 | const d = this._diff(id) 70 | 71 | // push close nodes 72 | for (let i = d; i >= 0 && result.length < k; i--) this._pushNodes(i, k, result) 73 | 74 | // if we don't have enough close nodes, populate from other rows, re the paper 75 | for (let i = d + 1; i < this.rows.length && result.length < k; i++) this._pushNodes(i, k, result) 76 | 77 | return result 78 | } 79 | 80 | _pushNodes (i, k, result) { 81 | const row = this.rows[i] 82 | if (!row) return 83 | 84 | const missing = Math.min(k - result.length, row.nodes.length) 85 | for (let j = 0; j < missing; j++) result.push(row.nodes[j]) 86 | } 87 | 88 | toArray () { 89 | return this.closest(this.id, Infinity) 90 | } 91 | 92 | _diff (id) { 93 | for (let i = 0; i < id.length; i++) { 94 | const a = id[i] 95 | const b = this.id[i] 96 | 97 | if (a !== b) return i * 8 + Math.clz32(a ^ b) - 24 98 | } 99 | 100 | return this.rows.length - 1 101 | } 102 | } 103 | 104 | class Row extends EventEmitter { 105 | constructor (table, index) { 106 | super() 107 | 108 | this.data = null // can be used be upstream for whatevs 109 | this.byteOffset = index >> 3 110 | this.index = index 111 | this.table = table 112 | this.nodes = [] 113 | } 114 | 115 | add (node) { 116 | const id = node.id 117 | 118 | let l = 0 119 | let r = this.nodes.length - 1 120 | 121 | while (l <= r) { 122 | const m = (l + r) >> 1 123 | const c = this.compare(id, this.nodes[m].id) 124 | 125 | if (c === 0) { 126 | this.nodes[m] = node 127 | return true 128 | } 129 | 130 | if (c < 0) r = m - 1 131 | else l = m + 1 132 | } 133 | 134 | if (this.nodes.length >= this.table.k) { 135 | this.emit('full', node) 136 | return false 137 | } 138 | 139 | this.insert(l, node) 140 | return true 141 | } 142 | 143 | remove (id) { 144 | let l = 0 145 | let r = this.nodes.length - 1 146 | 147 | while (l <= r) { 148 | const m = (l + r) >> 1 149 | const c = this.compare(id, this.nodes[m].id) 150 | 151 | if (c === 0) { 152 | this.splice(m) 153 | return true 154 | } 155 | 156 | if (c < 0) r = m - 1 157 | else l = m + 1 158 | } 159 | 160 | return false 161 | } 162 | 163 | get (id) { 164 | let l = 0 165 | let r = this.nodes.length - 1 166 | 167 | while (l <= r) { 168 | const m = (l + r) >> 1 169 | const node = this.nodes[m] 170 | const c = this.compare(id, node.id) 171 | 172 | if (c === 0) return node 173 | if (c < 0) r = m - 1 174 | else l = m + 1 175 | } 176 | 177 | return null 178 | } 179 | 180 | insert (i, node) { 181 | this.nodes.push(node) // push node or null or whatevs, just trying to not be polymorphic 182 | for (let j = this.nodes.length - 1; j > i; j--) this.nodes[j] = this.nodes[j - 1] 183 | this.nodes[i] = node 184 | this.emit('add', node) 185 | } 186 | 187 | splice (i) { 188 | for (; i < this.nodes.length - 1; i++) this.nodes[i] = this.nodes[i + 1] 189 | this.emit('remove', this.nodes.pop()) 190 | } 191 | 192 | // very likely they diverge after a couple of bytes so a simple impl, like this is prop fastest vs Buffer.compare 193 | compare (a, b) { 194 | for (let i = this.byteOffset; i < a.length; i++) { 195 | const ai = a[i] 196 | const bi = b[i] 197 | if (ai === bi) continue 198 | return ai < bi ? -1 : 1 199 | } 200 | return 0 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kademlia-routing-table", 3 | "version": "1.0.6", 4 | "description": "XOR based routing table used for P2P networks such as a Kademlia DHT.", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "imports": { 10 | "events": { 11 | "bare": "bare-events", 12 | "default": "events" 13 | } 14 | }, 15 | "dependencies": { 16 | "bare-events": "^2.2.0" 17 | }, 18 | "devDependencies": { 19 | "brittle": "^3.3.2", 20 | "standard": "^17.1.0" 21 | }, 22 | "scripts": { 23 | "test": "standard && brittle test.js" 24 | }, 25 | "keywords": [ 26 | "kademlia", 27 | "p2p", 28 | "k-bucket", 29 | "k-buckets", 30 | "xor", 31 | "routing", 32 | "distributed", 33 | "systems" 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/mafintosh/kademlia-routing-table.git" 38 | }, 39 | "author": "Mathias Buus (@mafintosh)", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/mafintosh/kademlia-routing-table/issues" 43 | }, 44 | "homepage": "https://github.com/mafintosh/kademlia-routing-table" 45 | } 46 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const { randomBytes } = require('crypto') 3 | const RoutingTable = require('./') 4 | 5 | test('basic', function (t) { 6 | const table = new RoutingTable(id()) 7 | const node = { id: id() } 8 | 9 | t.ok(table.add(node)) 10 | t.alike(table.closest(id()), [node]) 11 | }) 12 | 13 | test('random', function (t) { 14 | const table = new RoutingTable(id()) 15 | 16 | for (let i = 0; i < 1000; i++) { 17 | table.add({ id: id() }) 18 | } 19 | 20 | t.is(table.size, table.toArray().length) 21 | 22 | for (let i = 0; i < 1000; i++) { 23 | if (!table.random()) { 24 | t.fail('should always be a node in there') 25 | break 26 | } 27 | } 28 | }) 29 | 30 | test('insert very shared', function (t) { 31 | const table = new RoutingTable(Buffer.alloc(32)) 32 | 33 | const a = Buffer.alloc(32) 34 | const b = Buffer.alloc(32) 35 | 36 | a[31] = 0b10 37 | b[31] = 0b11 38 | 39 | table.add({ id: a }) 40 | table.add({ id: b }) 41 | 42 | t.is(table.size, 2) 43 | }) 44 | 45 | function id () { 46 | return randomBytes(32) 47 | } 48 | --------------------------------------------------------------------------------