UNPKG

ntrnetwork

Version:

Transledger peer to peer wire communication

283 lines (236 loc) 9.84 kB
/* ----------------------------------------------------------------------------- interblockchain wire protocol implementation example Description: This module is an interblockchain wire protocol usage example Author: Didier PH Martin 2018-06-28 Copyright: (c) Interblockchain 2018. --------------------------------------------------------------------------------*/ const net = require('net') const os = require('os') const secp256k1 = require('secp256k1') const { EventEmitter } = require('events') const ms = require('ms') const Buffer = require('safe-buffer').Buffer const LRUCache = require('lru-cache') const { pk2id, createDeferred } = require('../util') const Peer = require('./peer'); const ip = require('ip'); const pVersion = 10; const debug = (process.env.RLPX) ? process.env.RLPX : require('../environment').debug.rlpx; const nodeID = require('../environment').nodeID; class RLPx extends EventEmitter { constructor (privateKey, options) { super() this._channelID = 0x00; this._privateKey = Buffer.from(privateKey); this._id = pk2id(secp256k1.publicKeyCreate(this._privateKey, false)); // options this._timeout = options.timeout || ms('10s'); this._maxPeers = options.maxPeers || 10; this._clientId = Buffer.from(options.clientId || nodeID); this._remoteClientIdFilter = options.remoteClientIdFilter; this._capabilities = options.capabilities; this._listenPort = options.listenPort; // DPT this._dpt = options.dpt || null if (this._dpt !== null) { this._dpt.on('peer:new', (peer) => { if (this._peersLRU.has(peer.id.toString('hex'))) return else this._peersLRU.set(peer.id.toString('hex'), true) if (this._getOpenSlots() > 0) return this._connectToPeer(peer) else this._peersQueue.push({ peer: peer, ts: 0 }) // save to queue }) this._dpt.on('peer:removed', (peer) => { // remove from queue this._peersQueue = this._peersQueue.filter((item) => !item.peer.id.equals(peer.id)) }) } // internal this._server = net.createServer() this._server.once('listening', () => this.emit('listening')); this._server.once('close', () => this.emit('close')); this._server.on('error', (err) => this.emit('error', err)); this._server.on('connection', (socket) => this._onConnect(socket, null, null, null)); this._peers = new Map() this._peersQueue = [] this._peersLRU = new LRUCache({ max: 25000 }) this._refillIntervalId = setInterval(() => this._refillConnections(), ms('30s')) } static get DISCONNECT_REASONS() {return Peer.DISCONNECT_REASONS} get server() { return this._server; } set channelID(channel) { this._channelID = channel; } get channelID() { return this._channelID; } set monitor(state) { // state: boolean this._monitor = state; } get monitor() { return this._monitor; } listen (...args) { this._isAliveCheck() debug ? console.log(`${Date().toString().substring(0, 24)} rlpx/index.listen`) : null; this._server.listen(...args) } destroy (...args) { this._isAliveCheck() debug ? console.log(`${Date().toString().substring(0, 24)} rlpx.index: call .destroy`) : null; clearInterval(this._refillIntervalId) this._server.close(...args) this._server = null for (let peerKey of this._peers.keys()) this.disconnect(Buffer.from(peerKey, 'hex')) } async connect (peer) { this._isAliveCheck() if (!Buffer.isBuffer(peer.id)) throw new TypeError('Expected peer.id as Buffer') const peerKey = peer.id.toString('hex') if (this._peers.has(peerKey)) throw new Error('Already connected') if (this._getOpenSlots() === 0) throw new Error('Too much peers already connected') debug ? console.log(`${Date().toString().substring(0, 24)} rlpx.index: connect to ${peer.address}:${peer.port} (id: ${peerKey})`) : null; const deferred = createDeferred() const socket = new net.Socket() this._peers.set(peerKey, socket) socket.once('close', () => { this._peers.delete(peerKey) this._refillConnections() }) socket.once('error', deferred.reject) socket.setTimeout(this._timeout, () => deferred.reject(new Error('Connection timeout'))) socket.connect(peer.port, peer.address, deferred.resolve) await deferred.promise; this._onConnect(socket, peer.id, peer.channelID, peer.port); } getPeers () { return Array.from(this._peers.values()).filter((item) => item instanceof Peer) } disconnect (id) { const peer = this._peers.get(id.toString('hex')) if (peer instanceof Peer) peer.disconnect(Peer.DISCONNECT_REASONS.CLIENT_QUITTING) } // this function is called to broadcast a message from a client application broadcast(code, payload) { debug ? console.log(`${Date().toString().substring(0, 24)} rlpx.broadcast: broadcast message to all connected peers`) : null; this._peers.forEach( (item, index) => { if (item.getProtocols && (item._socket._peername.address != ip.address()) ) item.getProtocols()[0].sendMessage(code, payload); }) } // this function is called to broadcast a message from other nodes _broadcast(code, ipAddress, payload) { debug ? console.log(`${Date().toString().substring(0, 24)} rlpx.broadcast: broadcast message to all connected peers`) : null; this._peers.forEach( (item, index) => { if (item.getProtocols && (item._socket._peername.address != ip.address()) && (item._socket._peername.address != ipAddress) ) item.getProtocols()[0].sendMessage(code, payload); }) } addEventHandler(event, handler) { this.listener = { event: event, handler: handler } } _isAlive () { return this._server !== null } _isAliveCheck () { if (!this._isAlive()) throw new Error('Server already destroyed') } _getOpenSlots () { return Math.max(this._maxPeers - this._peers.size, 0) } _connectToPeer (peer) { const opts = { id: peer.id, channelID: peer.channelID, address: peer.address, port: peer.tcpPort } this.connect(opts).catch((err) => { if (this._dpt === null) return if (err.code === 'ECONNRESET' || err.toString().includes('Connection timeout')) { this._dpt.banPeer(opts, ms('5m')) } }) } _onConnect (socket, peerId, peerChannelID, peerPort) { debug ? console.log(`${Date().toString().substring(0, 24)} rlpx.index: onConnect() -> ${socket.remoteAddress}:${socket.remotePort}, handshake waiting..`) : null; const peer = new Peer({ socket: socket, remoteId: peerId, privateKey: this._privateKey, id: this._id, channelID: peerChannelID, timeout: this._timeout, clientId: "interblockchain", nodeID: "interblockchain", remoteClientIdFilter: this._remoteClientIdFilter, capabilities: this._capabilities, port: peerPort }) peer.on('error', (err) => this.emit('peer:error', peer, err)) // handle incoming connection if (peerId === null && this._getOpenSlots() === 0) { peer.once('connect', () => peer.disconnect(Peer.DISCONNECT_REASONS.TOO_MANY_PEERS)) socket.once('error', () => {}) return } peer.once('connect', () => { var msg = `handshake with ${socket.remoteAddress}:${socket.remotePort} was successful` if (peer._eciesSession._gotEIP8Auth === true) { msg += ` (peer eip8 auth)` } if (peer._eciesSession._gotEIP8Ack === true) { msg += ` (peer eip8 ack)` } debug ? console.log(`${Date().toString().substring(0, 24)} rlpx.index: ${msg}`) : null; if (peer.getId().equals(this._id)) { return peer.disconnect(Peer.DISCONNECT_REASONS.SAME_IDENTITY) } const peerKey = peer.getId().toString('hex') const item = this._peers.get(peerKey) if (item && item instanceof Peer) { return peer.disconnect(Peer.DISCONNECT_REASONS.ALREADY_CONNECTED) } this._peers.set(peerKey, peer) this.emit('peer:added', peer) }) peer.once('close', (reason, disconnectWe) => { if (disconnectWe) { debug ? console.log(`${Date().toString().substring(0, 24)} rlpx.index: disconnect from ${socket.remoteAddress}:${socket.remotePort}, reason: ${String(reason)}`) : null; } else { debug ? console.log(`${Date().toString().substring(0, 24)} rlpx.index: ${socket.remoteAddress}:${socket.remotePort} disconnect, reason: ${String(reason)}`) : null; } if (!disconnectWe && reason === Peer.DISCONNECT_REASONS.TOO_MANY_PEERS) { // hack this._peersQueue.push({ peer: { id: peer.getId(), address: peer._socket.remoteAddress, tcpPort: peer._socket.remotePort }, ts: Date.now() + ms('5m') }) } let peerKey = peer.getId().toString('hex') this._peers.delete(peerKey) this.emit('peer:removed', peer, reason, disconnectWe) }) } _refillConnections () { if (!this._isAlive()) return debug ? console.log(`${Date().toString().substring(0, 24)} rlpx.index: refill connections.. queue size: ${this._peersQueue.length}, open slots: ${this._getOpenSlots()}`) : null; this._peersQueue = this._peersQueue.filter((item) => { if (this._getOpenSlots() === 0) return true if (item.ts > Date.now()) return true this._connectToPeer(item.peer) return false }) } } module.exports = RLPx