# dht-rpc Make RPC calls over a [Kademlia](https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) based DHT. ``` npm install dht-rpc@next ``` ## NOTE: v5 Release Candidate Note that this is the README for the v5 release candidate. To see the v4 documentation/code go to https://github.com/mafintosh/dht-rpc/tree/v4 ## Key Features * Remote IP / firewall detection * Easily add any command to your DHT * Streaming queries and updates Note that internally V5 of dht-rpc differs significantly from V4, due to a series of improvements to NAT detection, secure routing IDs and more. ## Usage Here is an example implementing a simple key value store First spin up a bootstrap node. You can make multiple if you want for redundancy. ``` js import DHT from 'dht-rpc' // If the bootstrap node doesn't implement the same commands as your other nodes // remember to set ephemeral: true so it isn't added to the routing table. const bootstrap = DHT.bootstrapper(10001, { ephemeral: true }) ``` Now lets make some dht nodes that can store values in our key value store. ``` js import DHT from 'dht-rpc' import crypto from 'crypto' // Let's create 100 dht nodes for our example. for (var i = 0; i < 100; i++) createNode() function createNode () { const node = new DHT({ bootstrap: [ 'localhost:10001' ] }) const values = new Map() node.on('request', function (req) { if (req.command === 'values') { if (req.token) { // if we are the closest node store the value (ie the node sent a valid roundtrip token) const key = hash(req.value).toString('hex') values.set(key, req.value) console.log('Storing', key, '-->', req.value.toString()) return req.reply(null) } const value = values.get(req.target.toString('hex')) req.reply(value) } }) } function hash (value) { return crypto.createHash('sha256').update(value).digest() } ``` To insert a value into this dht make another script that does this following ``` js const node = new DHT() const q = node.query({ target: hash(val), command: 'values', value }, { // commit true will make the query re-reuqest the 20 closest // nodes with a valid round trip token to update the values commit: true }) await q.finished() ``` Then after inserting run this script to query for a value ``` js const target = Buffer.from(hexFromAbove, 'hex') for await (const data of node.query({ target, command: 'values' })) { if (data.value && hash(data.value).toString('hex') === hexFromAbove) { // We found the value! Destroy the query stream as there is no need to continue. console.log(val, '-->', data.value.toString()) break } } console.log('(query finished)') ``` ## API #### `const node = new DHT([options])` Create a new DHT node. Options include: ``` js { // Whether or not this node is ephemeral or should join the routing table ephemeral: false, // If you don't explicitly specific the ephemerality, the node will automatically // figure it out in adaptive mode, based on your NAT settings, uptime and some other heuristics adaptive: true, // A list of bootstrap nodes bootstrap: [ 'bootstrap-node.com:24242', ... ], // Optionally pass in your own UDP socket to use. socket: udpSocket, // Optionally pass in array of { host, port } to add to the routing table if you know any peers nodes: [{ host, port }, ...], // Optionally pass a port you prefer to bind to instead of a random one bind: 0, // dht-rpc will automatically detect if you are firewalled. If you know that you are not set this to false firewalled: true } ``` Note that adaptive mode is very conservative, so it might take ~20-30 mins for the node to turn persistent. For the majority of use-cases you should always use adaptive mode to ensure good DHT health. Your DHT routing id is `hash(publicIp + publicPort)` and will be autoconfigured internally. #### `const node = DHT.boostrapper(bind, [options])` Sugar for the options needed to run a bootstrap node, ie ```js { firewalled: false, // a bootstrapper can never be firewalled bootstrap: [] // force set no other bootstrappers. } ``` Additionally since you'll want a known port for a bootstrap node it adds the bind option as a primary argument. #### `await node.ready()` Wait for the node to be fully bootstrapped etc. You don't have to wait for this method, but can be useful during testing. #### `node.id` Get your own routing ID. Only available when the node is not ephemeral. #### `node.ephemeral` A boolean indicating if you are currently epheremal or not #### `node.on('bootstrap')` Emitted when the routing table is fully bootstrapped. Emitted as a conveinience. #### `node.on('listening')` Emitted when the underlying UDP socket is listening. Emitted as a conveinience. #### `node.on('persistent')` Emitted when the node is no longer in ephemeral mode. All nodes start in ephemeral mode, as they figure out their NAT settings. If you set `ephemeral: false` then this is emitted during the bootstrap phase, assuming you are on an open NAT. #### `node.on('wake-up')` Emitted when the node has detected that the computer has gone to sleep. If this happens, it will switch from persistent mode to ephemeral again. #### `node.refresh()` Refresh the routing table by looking up a random node in the background. This is called internally periodically, but exposed in-case you want to force a refresh. #### `node.host` Get your node's public ip, inferred from other nodes in the DHT. If the ip cannot be determined, this is set to `null`. #### `node.port` Get your node's public port, inferred from other nodes in the DHT. If your node does not have a consistent port, this is set to 0. #### `node.firewalled` Boolean indicated if your node is behind a firewall. This is auto detected by having other node's trying to do a PING to you without you contacting them first. #### `const udpAddr = node.address()` Get the local address of the UDP socket bound. Note that if you are in ephemeral mode, this will return a different port than the one you provided in the constructor (under bind), as ephemeral mode always uses a random port. #### `node.on('request', req)` Emitted when an incoming DHT request is received. This is where you can add your own RPC methods. * `req.target` - the dht target the peer is looking (routing is handled behind the scene) * `req.command` - the RPC command name * `req.value` - the RPC value buffer * `req.token` - If the remote peer echoed back a valid roundtrip token, proving their "from address" this is set * `req.from` - who sent this request (host, port) To reply to a request use the `req.reply(value)` method and to reply with an error code use `req.error(errorCode)`. In general error codes are up to the user to define, with the general suggestion to start application specific errors from error code `16` and up, to avoid future clashes with `dht-rpc` internals. Currently dht-rpc defines the following errors ``` js DHT.OK = 0 // ie no error DHT.ERROR_UNKNOWN_COMMAND = 1 // the command requested does not exist DHT.ERROR_INVALID_TOKEN = 2 // the round trip token sent is invalid ``` The DHT has a couple of built in commands for bootstrapping and general DHT health management. Those are: * `find_node` - Find the closest DHT nodes to a specific target with no side-effects. * `ping` - Ping another node to see if it is alive. * `ping_nat` - Ping another node, but have it reply on a different UDP session to see if you are firewalled. * `down_hint` - Gossiped internally to hint that a specific node might be down. #### `reply = await node.request({ token, target, command, value }, to, [options])` Send a request to a specific node specified by the to address (`{ host, port }`). Options include: ```js { retry: true, // whether the request should retry on timeout socket: udpSocket // request on this specific socket } ``` Normally you'd set the token when commiting to the dht in the query's commit hook. #### `reply = await node.ping(to)` Sugar for `dht.request({ command: 'ping' }, to)` #### `stream = node.query({ target, command, value }, [options])` Query the DHT. Will move as close as possible to the `target` provided, which should be a 32-byte uniformly distributed buffer (ie a hash). * `target` - find nodes close to this * `command` - the method you want to invoke * `value` - optional binary payload to send with it If you want to modify state stored in the dht, you can use the commit flag to signal the closest nodes. ``` js { // "commit" the query to the 20 closest nodes so they can modify/update their state commit: true } ``` Commiting a query will just re-request your command to the closest nodes once those are verified. If you want to do some more specific logic with the closest nodes you can specify a function instead, that is called for each close reply. ``` js { async commit (reply, dht, query) { // normally you'd send back the roundtrip token here, to prove to the remote that you own // your ip/port await dht.request({ token: reply.token, target, command, value }, reply.from) } } ``` Other options include: ``` js { nodes: [ // start the query by querying these nodes // useful if you are re-doing a query from a set of closest nodes. ], replies: [ // similar to nodes, but if you useful if you have an array of closest replies instead // from a previous query. ], map (reply) { // map the reply into what you want returned on the stram return { onlyValue: reply.value } } } ``` The query method returns a stream encapsulating the query, that is also an async iterator. Each `data` event contain a DHT reply. If you just want to wait for the query to finish, you can use the `await stream.finished()` helper. After completion the closest nodes are stored in `stream.closestNodes` array. If you want to access the closest replies to your provided target you can see those at `stream.closestReplies`. #### `node.destroy()` Shutdown the DHT node. #### `node.destroyed` Boolean indicating if this has been destroyed. #### `node.toArray()` Get the routing table peers out as an array of `{ host, port}` #### `node.addNode({ host, port })` Manually add a node to the routing table. ## License MIT