leasehold-net
Version:
Leasehold network module.
445 lines (397 loc) • 13.8 kB
JavaScript
const crypto = require('crypto');
const { lookupPeersIPs, filterByParams, consolidatePeers } = require('./utils');
const util = require('util');
const fs = require('fs');
const writeFile = util.promisify(fs.writeFile);
const readFile = util.promisify(fs.readFile);
const path = require('path');
const {
P2P,
EVENT_NETWORK_READY,
EVENT_NEW_INBOUND_PEER,
EVENT_CLOSE_INBOUND,
EVENT_CLOSE_OUTBOUND,
EVENT_CONNECT_OUTBOUND,
EVENT_DISCOVERED_PEER,
EVENT_FAILED_TO_FETCH_PEER_INFO,
EVENT_FAILED_TO_PUSH_NODE_INFO,
EVENT_OUTBOUND_SOCKET_ERROR,
EVENT_INBOUND_SOCKET_ERROR,
EVENT_UPDATED_PEER_INFO,
EVENT_FAILED_PEER_INFO_UPDATE,
EVENT_REQUEST_RECEIVED,
EVENT_MESSAGE_RECEIVED,
EVENT_BAN_PEER,
EVENT_UNBAN_PEER
} = require('lp2p');
const DEFAULT_PEER_SAVE_INTERVAL = 10 * 60 * 1000; // 10 mins in ms
const DEFAULT_PEER_LIST_FILE_PATH = path.join(__dirname, 'peers.json');
const MAX_CHANNEL_NAME_LENGTH = 200;
const DEFAULT_MODULE_ALIAS = 'leasehold_net';
const hasNamespaceReg = /:/;
class LeaseholdNet {
constructor({alias, logger, config}) {
this.options = config;
this.alias = alias || DEFAULT_MODULE_ALIAS;
this.logger = logger;
this.channel = null;
this.secret = null;
}
get dependencies() {
return ['app'];
}
get events() {
return ['event'];
}
get actions() {
return {
request: {
handler: async action => {
return this.p2p.request({
procedure: action.params.procedure,
data: action.params.data
});
}
},
emit: {
handler: action => {
return this.p2p.send({
event: action.params.event,
data: action.params.data
}, action.params.peerLimit);
}
},
requestFromPeer: {
handler: async action => {
return this.p2p.requestFromPeer(
{
procedure: action.params.procedure,
data: action.params.data,
},
action.params.peerId
);
}
},
emitToPeer: {
handler: action => {
return this.p2p.sendToPeer(
{
event: action.params.event,
data: action.params.data,
},
action.params.peerId
);
}
},
getPeers: {
handler: action => {
let peers = consolidatePeers({
connectedPeers: this.p2p.getConnectedPeers(),
disconnectedPeers: this.p2p.getDisconnectedPeers()
});
return filterByParams(peers, action.params);
}
},
getConnectedPeers: {
handler: action => {
let peers = consolidatePeers({
connectedPeers: this.p2p.getConnectedPeers()
});
return filterByParams(peers, action.params);
}
},
getPeersCount: {
handler: action => {
let peers = consolidatePeers({
connectedPeers: this.p2p.getConnectedPeers(),
disconnectedPeers: this.p2p.getDisconnectedPeers()
});
let { limit, offset, ...filterWithoutLimitOffset } = action.params;
return filterByParams(peers, filterWithoutLimitOffset).length;
}
},
getUniqueOutboundConnectedPeersCount: {
handler: action => {
let peers = consolidatePeers({
connectedPeers: this.p2p.getUniqueOutboundConnectedPeers()
});
let { limit, offset, ...filterWithoutLimitOffset } = action.params;
return filterByParams(peers, filterWithoutLimitOffset).length;
}
},
applyPenalty: {
handler: action => {
return this.p2p.applyPenalty(action.params.peerId, action.params.penalty);
}
}
};
}
async load(channel) {
this.channel = channel;
if (this.options.peerSelectionPluginPath) {
this.logger.debug(
`Using peer selection plugin at path ${this.options.peerSelectionPluginPath} relative to leasehold-net module`
);
let peerSelectionPlugin = require(this.options.peerSelectionPluginPath);
this.peerSelectionForConnection = peerSelectionPlugin.peerSelectionForConnection;
this.peerSelectionForRequest = peerSelectionPlugin.peerSelectionForRequest;
this.peerSelectionForSend = peerSelectionPlugin.peerSelectionForSend;
} else {
this.logger.debug(
`Using default peer selection`
);
}
let peerListFilePath = this.options.peerListFilePath || DEFAULT_PEER_LIST_FILE_PATH;
// Load peers from the database that were tried or connected the last time node was running
let previousPeersStr;
try {
previousPeersStr = await readFile(peerListFilePath, 'utf8');
} catch (err) {
if (err.code === 'ENOENT') {
this.logger.debug(`No existing peer list file found at ${peerListFilePath}`);
} else {
this.logger.error(`Failed to read file ${peerListFilePath} - ${err.message}`);
}
}
let previousPeers = [];
try {
previousPeers = previousPeersStr ? JSON.parse(previousPeersStr) : [];
} catch (err) {
this.logger.error(`Failed to parse JSON of previous peers - ${err.message}`);
}
this.secret = crypto.randomBytes(4).readUInt32BE(0);
let sanitizeNodeInfo = nodeInfo => ({
...nodeInfo,
wsPort: this.options.wsPort
});
let initialNodeInfo = sanitizeNodeInfo(
await this.channel.invoke('app:getApplicationState')
);
let seedPeers = await lookupPeersIPs(this.options.seedPeers, true);
let blacklistedIPs = this.options.blacklistedIPs || [];
let fixedPeers = this.options.fixedPeers
? this.options.fixedPeers.map(peer => ({
ipAddress: peer.ip,
wsPort: peer.wsPort
}))
: [];
let whitelistedPeers = this.options.whitelistedPeers
? this.options.whitelistedPeers.map(peer => ({
ipAddress: peer.ip,
wsPort: peer.wsPort
}))
: [];
let p2pConfig = {
nodeInfo: initialNodeInfo,
hostIp: this.options.hostIp,
blacklistedIPs,
fixedPeers,
whitelistedPeers,
seedPeers: seedPeers.map(peer => ({
ipAddress: peer.ip,
wsPort: peer.wsPort
})),
previousPeers,
connectTimeout: this.options.connectTimeout,
ackTimeout: this.options.ackTimeout,
maxOutboundConnections: this.options.maxOutboundConnections,
maxInboundConnections: this.options.maxInboundConnections,
peerBanTime: this.options.peerBanTime,
populatorInterval: this.options.populatorInterval,
sendPeerLimit: this.options.sendPeerLimit,
maxPeerDiscoveryResponseLength: this.options.maxPeerDiscoveryResponseLength,
maxPeerInfoSize: this.options.maxPeerInfoSize,
outboundShuffleInterval: this.options.outboundShuffleInterval,
netgroupProtectionRatio: this.options.netgroupProtectionRatio,
latencyProtectionRatio: this.options.latencyProtectionRatio,
productivityProtectionRatio: this.options.productivityProtectionRatio,
longevityProtectionRatio: this.options.longevityProtectionRatio,
wsMaxPayloadInbound: this.options.wsMaxPayloadInbound,
wsMaxPayloadOutbound: this.options.wsMaxPayloadOutbound,
wsMaxMessageRate: this.options.wsMaxMessageRate,
wsMaxMessageRatePenalty: this.options.wsMaxMessageRatePenalty,
rateCalculationInterval: this.options.rateCalculationInterval,
secret: this.secret
};
if (this.peerSelectionForConnection) {
p2pConfig.peerSelectionForConnection = this.peerSelectionForConnection;
}
if (this.peerSelectionForRequest) {
p2pConfig.peerSelectionForRequest = this.peerSelectionForRequest;
}
if (this.peerSelectionForSend) {
p2pConfig.peerSelectionForSend = this.peerSelectionForSend;
}
this.p2p = new P2P(p2pConfig);
this.channel.subscribe('app:state:updated', event => {
let newNodeInfo = sanitizeNodeInfo(event.data);
try {
this.p2p.applyNodeInfo(newNodeInfo);
} catch (error) {
this.logger.error(
`Applying NodeInfo failed because of error: ${error.message ||
error}`
);
}
});
this.p2p.on(EVENT_NETWORK_READY, () => {
this.logger.debug('Node connected to the network');
this.channel.publish(`${this.alias}:ready`);
});
this.p2p.on(EVENT_CLOSE_OUTBOUND, closePacket => {
this.logger.debug(
{
ipAddress: closePacket.peerInfo.ipAddress,
wsPort: closePacket.peerInfo.wsPort,
code: closePacket.code,
reason: closePacket.reason,
},
'EVENT_CLOSE_OUTBOUND: Close outbound peer connection'
);
});
this.p2p.on(EVENT_CLOSE_INBOUND, closePacket => {
this.logger.debug(
{
ipAddress: closePacket.peerInfo.ipAddress,
wsPort: closePacket.peerInfo.wsPort,
code: closePacket.code,
reason: closePacket.reason,
},
'EVENT_CLOSE_INBOUND: Close inbound peer connection'
);
});
this.p2p.on(EVENT_CONNECT_OUTBOUND, peerInfo => {
this.logger.debug(
{
ipAddress: peerInfo.ipAddress,
wsPort: peerInfo.wsPort,
},
'EVENT_CONNECT_OUTBOUND: Outbound peer connection'
);
});
this.p2p.on(EVENT_DISCOVERED_PEER, peerInfo => {
this.logger.trace(
{
ipAddress: peerInfo.ipAddress,
wsPort: peerInfo.wsPort,
},
'EVENT_DISCOVERED_PEER: Discovered peer connection'
);
});
this.p2p.on(EVENT_NEW_INBOUND_PEER, peerInfo => {
this.logger.debug(
{
ipAddress: peerInfo.ipAddress,
wsPort: peerInfo.wsPort,
},
'EVENT_NEW_INBOUND_PEER: Inbound peer connection'
);
});
this.p2p.on(EVENT_FAILED_TO_FETCH_PEER_INFO, error => {
this.logger.error(error.message || error);
});
this.p2p.on(EVENT_FAILED_TO_PUSH_NODE_INFO, error => {
this.logger.trace(error.message || error);
});
this.p2p.on(EVENT_OUTBOUND_SOCKET_ERROR, error => {
this.logger.debug(error.message || error);
});
this.p2p.on(EVENT_INBOUND_SOCKET_ERROR, error => {
this.logger.debug(error.message || error);
});
this.p2p.on(EVENT_UPDATED_PEER_INFO, peerInfo => {
this.logger.trace(
{
ipAddress: peerInfo.ipAddress,
wsPort: peerInfo.wsPort,
},
'EVENT_UPDATED_PEER_INFO: Update peer info',
JSON.stringify(peerInfo)
);
});
this.p2p.on(EVENT_FAILED_PEER_INFO_UPDATE, error => {
this.logger.error(error.message || error);
});
this.p2p.on(EVENT_REQUEST_RECEIVED, async request => {
this.logger.trace(
`EVENT_REQUEST_RECEIVED: Received inbound request for procedure ${request.procedure}`
);
// If the request has already been handled internally by the P2P library, we ignore.
if (request.wasResponseSent) {
return;
}
let hasTargetModule = hasNamespaceReg.test(request.procedure);
// If the request has no target module, default to chain (to support legacy protocol).
let sanitizedProcedure = hasTargetModule
? request.procedure
: `chain:${request.procedure}`;
try {
let result = await this.channel.invokePublic(sanitizedProcedure, request.data, {
peerId: request.peerId,
});
this.logger.trace(
`Peer request fulfilled event: Responded to peer request ${request.procedure}`
);
request.end(result); // Send the response back to the peer.
} catch (error) {
this.logger.error(
`Peer request not fulfilled event: Could not respond to peer request ${
request.procedure
} because of error: ${error.message || error}`
);
request.error(error); // Send an error back to the peer.
}
});
this.p2p.on(EVENT_MESSAGE_RECEIVED, async packet => {
this.logger.trace(
`EVENT_MESSAGE_RECEIVED: Received inbound message from ${packet.peerId} for event ${packet.event}`
);
let targetChannelName = `${this.alias}:event:${packet.event}`;
if (targetChannelName.length > MAX_CHANNEL_NAME_LENGTH) {
this.logger.error(
`Peer ${packet.peerId} tried to publish data to a custom channel name which exceeded the max length of ${MAX_CHANNEL_NAME_LENGTH}`
);
} else {
this.channel.publish(targetChannelName, packet.data, {peerId: packet.peerId});
}
// For backward compatibility with Lisk chain module.
this.channel.publish(`${this.alias}:event`, packet);
});
this.p2p.on(EVENT_BAN_PEER, peerId => {
this.logger.error(
{ peerId },
'EVENT_BAN_PEER: Peer has been banned temporarily'
);
});
this.p2p.on(EVENT_UNBAN_PEER, peerId => {
this.logger.error(
{ peerId },
'EVENT_UNBAN_PEER: Peer ban has expired'
);
});
setInterval(async () => {
let peersToSave = this.p2p.getConnectedPeers();
if (peersToSave.length) {
let peersString = JSON.stringify(peersToSave);
await writeFile(peerListFilePath, peersString);
}
}, DEFAULT_PEER_SAVE_INTERVAL);
try {
await this.p2p.start();
} catch (error) {
this.logger.fatal(
{
message: error.message,
stack: error.stack,
},
`Failed to initialize net module`
);
process.emit('cleanup', error);
}
}
async unload() {
this.logger.info(`Stopping net module...`);
return this.p2p.stop();
}
};
module.exports = LeaseholdNet;