From fb0817f19d5a402fcb33f7de6e9117968d6d0ee5 Mon Sep 17 00:00:00 2001 From: Chris Aslanoglou Date: Thu, 3 Jan 2019 22:18:03 +0200 Subject: [PATCH] chore: add basic usage example Add a basic usage example whereby the client code runs on top of each libp2p DHT node. The idea is that each user has its own deaddrop, identified by her UserId (on the DHT identifiers namespace), where other users can leave messages to. The demo instantiates three application (and thus DHT) nodes. Bootstrap is handled by having an "always-on" node where others can connect to. In real world examples, this would not suffice, as a discovery mechanism needs to be in place (either by using the RandomWalk peer discovery or by switching any implementations of libp2p/interface-peer-discovery). Once the nodes are instantiated and connected, thus forming a single DHT, one deaddrop application (userA) writes a message for userB. Then, userB reads her messages (after having been informed about the message through off-channel means). For this example to make sense, any sent message is encrypted using the recipient's public key, so that only she can read them, by decrypting them with her private key. Resolves #36. Signed-off-by: Chris Aslanoglou --- examples/deaddrops/deaddrop.js | 58 +++++++++++++++++ examples/deaddrops/dht.js | 114 +++++++++++++++++++++++++++++++++ examples/deaddrops/index.js | 92 ++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 examples/deaddrops/deaddrop.js create mode 100644 examples/deaddrops/dht.js create mode 100644 examples/deaddrops/index.js diff --git a/examples/deaddrops/deaddrop.js b/examples/deaddrops/deaddrop.js new file mode 100644 index 00000000..210bf807 --- /dev/null +++ b/examples/deaddrops/deaddrop.js @@ -0,0 +1,58 @@ +'use strict' + +const waterfall = require('async').waterfall + +const DHTNode = require('./dht').DHTNode + +class DeadDrop { + constructor(userId, dht) { + this.userId = userId + this.userIdKey = Buffer.from(userId) + this.dht = dht + } + + init(cb) { + this.dht.start(cb) + } + + dial(target, cb) { + this.dht.dial(target, cb) + } + + shutdown(cb) { + this.dht.stop(cb) + } + + leaveMessageToUser(receiver, message, cb) { + const key = Buffer.from(receiver) + const value = Buffer.from(message) + this.dht.put(key, value, cb) + } + + readMyMessages(cb) { + this.dht.get(this.userIdKey, cb) + } +} + +function instantiateDeaddrop(userId, callback) { + let deaddrop + waterfall( + [ + (cb) => DHTNode.createInstance(cb), + (dht, cb) => { + deaddrop = new DeadDrop(userId, dht) + deaddrop.init(cb) + } + ], + (err) => { + if (err) callback(err, null); + + console.log(`Deaddrop[${userId}] was successfuly initialized...`); + callback(null, deaddrop) + } + ) +} + +module.exports = { + instantiateDeaddrop +} \ No newline at end of file diff --git a/examples/deaddrops/dht.js b/examples/deaddrops/dht.js new file mode 100644 index 00000000..8891ab54 --- /dev/null +++ b/examples/deaddrops/dht.js @@ -0,0 +1,114 @@ +'use strict' + +const { + series +} = require('async') + +const TCP = require('libp2p-tcp') +const Switch = require('libp2p-switch') +const Mplex = require('libp2p-mplex') +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') +const PeerBook = require('peer-book') + +const KadDHT = require('../../src'); + +function createPeerInfo(callback) { + // Creation of PeerId should be done only once, persisted and then read upon initialization. + PeerId.create({bits: 512}, (err, id) => { + if (err) callback(err, null) + + callback(null, new PeerInfo(id)) + }) +} + +function createTCPSwitch(peerInfo) { + // Required for the nodes to be able to communicate + // N.B. You cannot add solely a transport, a stream multiplexer is needed as well so that it + // allows for a TCP stream to be multiplexed over a connection (even though you have only one transport), IIUC. + const sw = new Switch(peerInfo, new PeerBook()) + sw.transport.add('tcp', new TCP()) + sw.connection.addStreamMuxer(Mplex) + sw.connection.reuse() + return sw +} + +class DHTNode { + /** + * Initializes a DHT node. + * @param {Switch} sw A libp2p-switch implementation. More info https://github.com/libp2p/js-libp2p-switch + */ + constructor(sw) { + const options = { kBucketSize: 1 } + this._sw = sw + // Discovery is enabled by default since KadDHT implements also + // https://github.com/libp2p/interface-peer-discovery through its RandomWalk module. + this._dht = new KadDHT(sw, options) + } + + /** + * Creates a new DHT Node. + * The node listens on localhost, using TCP, at any port, using the js-libp2p-switch implementation. + */ + static createInstance(callback) { + createPeerInfo((err, peerInfo) => { + if (err) callback(err, null) + + // This allows the node (its switch actually) to act as a "listener" (server). + peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/0') + + const sw = createTCPSwitch(peerInfo) + const dht = new DHTNode(sw) + callback(null, dht) + }) + } + + start(callback) { + series( + [ + (cb) => this._sw.start(cb), + (cb) => this._dht.start(cb), + ], + (err) => { + if (err) callback(err, null) + + console.log(`DHTNode[${this._dht.peerInfo.id.toB58String()}] initialized and listening on:`) + this._dht.peerInfo.multiaddrs.forEach(ma => console.log(`\tMultiAddress[${ma.toString()}]`)) + + callback() + } + ) + } + + stop(callback) { + series( + [ + (cb) => this._dht.stop(cb), + (cb) => this._sw.stop(cb), + ], + (err) => { + if (err) callback(err, null) + + callback() + } + ) + } + + dial(target, cb) { + this._sw.dial(target, cb) + } + + put(key, value, cb) { + this._dht.put(key, value, cb) + } + + get(key, cb) { + this._dht.get(key, null, cb) + } + + getPeerInfo() { + return this._dht.peerInfo + } +} + +module.exports.DHTNode = DHTNode \ No newline at end of file diff --git a/examples/deaddrops/index.js b/examples/deaddrops/index.js new file mode 100644 index 00000000..98da41f3 --- /dev/null +++ b/examples/deaddrops/index.js @@ -0,0 +1,92 @@ +'use strict' + +const _ = require('lodash') +const { + each, + series, + parallel, + waterfall, +} = require('async') + +const instantiateDeaddrop = require('./deaddrop').instantiateDeaddrop + +// Dummy example: +// Create 3 deaddrops. One that's always on (for bootstrapping) and two others. +// Once the bootstrapping process completes, userA leaves a message in +// user's B deaddrop, who then later reads it. +const userAlwaysOnId = 'userAlwaysOn' +const userAId = 'userA' +const userBId = 'userB' +let deaddropAlwaysOn, deaddropA, deaddropB + +// Utility method for instantiating the three deaddrops. +function instantiateAll(callback) { + parallel( + [ + (cb) => instantiateDeaddrop(userAlwaysOnId, cb), + (cb) => instantiateDeaddrop(userAId, cb), + (cb) => instantiateDeaddrop(userBId, cb), + ], + (err, deaddrops) => { + if (err) callback(err) + + deaddropAlwaysOn = deaddrops[0] + deaddropA = deaddrops[1] + deaddropB = deaddrops[2] + + console.log('Deaddrops were instantiated.\n') + callback() + } + ) +} + +// Adapted from https://github.com/libp2p/js-libp2p-kad-dht/blob/master/test/kad-dht.spec.js#L34-L39 +function connectNoSync(a, b, callback) { + const target = _.cloneDeep(b.dht.getPeerInfo()) + target.id._pubKey = target.id.pubKey + target.id._privKey = null + a.dht.dial(target, callback) +} + +// Utility method for connecting the two deaddrops with the always-on one. +function bootstrap(callback) { + series( + [ + (cb) => connectNoSync(deaddropA, deaddropAlwaysOn, cb), + (cb) => connectNoSync(deaddropB, deaddropAlwaysOn, cb), + ], + (err) => { + if (err) callback(err) + + console.log('Deaddrops connected to the always-on one.') + callback() + } + ) +} + +waterfall( + [ + (cb) => instantiateAll(cb), + (cb) => bootstrap(cb), + (cb) => deaddropA.leaveMessageToUser(userBId, `Greetings from ${userAId}`, cb), + (cb) => deaddropB.readMyMessages(cb), + (message, cb) => { + console.log(`${userBId}: received message[${message}]`) + cb() + }, + (cb) => { + each( + [deaddropAlwaysOn, deaddropA, deaddropB], + (deaddrop, callback) => { + deaddrop.shutdown(callback) + }, + (err) => { + if (err) cb(err) + + console.log('\nAll deaddrops were shutdown.') + cb() + } + ) + } + ] +)