evdevkit
Version:
Developer toolkit for Evernode smart contract deployment
1,418 lines (1,173 loc) • 7.75 MB
JavaScript
#! /usr/bin/env node
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ 41175:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
var map = {
"./build/Release/secp256k1.node": 63416
};
function webpackContext(req) {
var id = webpackContextResolve(req);
return __nccwpck_require__(id);
}
function webpackContextResolve(req) {
if(!__nccwpck_require__.o(map, req)) {
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
return map[req];
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id = 41175;
/***/ }),
/***/ 62902:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
var map = {
"./prebuilds/linux-x64/node.napi1.node": 25643
};
function webpackContext(req) {
var id = webpackContextResolve(req);
return __nccwpck_require__(id);
}
function webpackContextResolve(req) {
if(!__nccwpck_require__.o(map, req)) {
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
return map[req];
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id = 62902;
/***/ }),
/***/ 48279:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
var map = {
"./prebuilds/linux-x64/node.napi.node": 31884
};
function webpackContext(req) {
var id = webpackContextResolve(req);
return __nccwpck_require__(id);
}
function webpackContextResolve(req) {
if(!__nccwpck_require__.o(map, req)) {
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
return map[req];
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id = 48279;
/***/ }),
/***/ 73771:
/***/ ((module) => {
function webpackEmptyAsyncContext(req) {
// Here Promise.resolve().then() is used instead of new Promise() to prevent
// uncaught exception popping up in devtools
return Promise.resolve().then(() => {
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
});
}
webpackEmptyAsyncContext.keys = () => ([]);
webpackEmptyAsyncContext.resolve = webpackEmptyAsyncContext;
webpackEmptyAsyncContext.id = 73771;
module.exports = webpackEmptyAsyncContext;
/***/ }),
/***/ 63416:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
module.exports = require(__nccwpck_require__.ab + "build/Release/secp256k1.node")
/***/ }),
/***/ 31884:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
module.exports = require(__nccwpck_require__.ab + "prebuilds/linux-x64/node.napi.node")
/***/ }),
/***/ 25643:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
module.exports = require(__nccwpck_require__.ab + "prebuilds/linux-x64/node.napi1.node")
/***/ }),
/***/ 2785:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const process = __nccwpck_require__(932);
const fs = __nccwpck_require__(79896);
const MAINNET = 'mainnet';
// Throw errors if the env value is required.
const appenv = {
instanceImage: 'evernode/sashimono:hp.0.6.4-ubt.20.04-njs.20',
get tenantSecret() {
if (!process.env.EV_TENANT_SECRET)
throw 'EV_TENANT_SECRET environment variable has not been set.';
return process.env.EV_TENANT_SECRET;
},
get userPrivateKey() {
if (!process.env.EV_USER_PRIVATE_KEY)
throw 'EV_USER_PRIVATE_KEY environment variable has not been set.';
return process.env.EV_USER_PRIVATE_KEY;
},
get hpInitCfg() {
if (process.env.EV_HP_INIT_CFG_PATH && !fs.existsSync(process.env.EV_HP_INIT_CFG_PATH))
throw `HotPocket config file does not exist in EV_HP_INIT_CFG_PATH=${process.env.EV_HP_INIT_CFG_PATH}`;
if (!process.env.EV_HP_INIT_CFG_PATH) {
return {};
}
try {
return JSON.parse(fs.readFileSync(process.env.EV_HP_INIT_CFG_PATH));
}
catch (e) {
throw `EV_HP_INIT_CFG_PATH=${process.env.EV_HP_INIT_CFG_PATH} - ${e}`;
}
},
get hpOverrideCfg() {
if (process.env.EV_HP_OVERRIDE_CFG_PATH && !fs.existsSync(process.env.EV_HP_OVERRIDE_CFG_PATH))
throw `HotPocket override config file does not exist in EV_HP_OVERRIDE_CFG_PATH=${process.env.EV_HP_OVERRIDE_CFG_PATH}`;
if (!process.env.EV_HP_OVERRIDE_CFG_PATH) {
return {};
}
try {
return JSON.parse(fs.readFileSync(process.env.EV_HP_OVERRIDE_CFG_PATH));
}
catch (e) {
throw `EV_HP_OVERRIDE_CFG_PATH=${process.env.EV_HP_OVERRIDE_CFG_PATH} - ${e}`;
}
},
get network() {
// TODO: Default will be changed to MAINNET after the launch.
return process.env.EV_NETWORK || MAINNET;
},
get xahaudServer() {
return process.env.EV_XAHAUD_SERVER || null;
},
}
Object.freeze(appenv);
module.exports = appenv
/***/ }),
/***/ 68310:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const { execSync } = __nccwpck_require__(35317);
function exec(commad, streamOut = false) {
return execSync(commad, streamOut ? { stdio: 'inherit' } : {stdio : 'pipe' });
}
module.exports = {
exec
};
/***/ }),
/***/ 24475:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const fs = __nccwpck_require__(79896);
const os = __nccwpck_require__(70857);
const crypto = __nccwpck_require__(76982);
const uuid = __nccwpck_require__(76193);
const path = __nccwpck_require__(16928);
const { EvernodeManager } = __nccwpck_require__(81917);
const { info, log, error } = __nccwpck_require__(48035);
const appenv = __nccwpck_require__(2785);
const { generateKeys } = __nccwpck_require__(51034);
const BLACKLIST_SCORE_THRESHOLD = 2;
const NodeStatus = {
NONE: 0,
CREATED: 1,
CONFIGURED: 2,
ACKNOWLEDGED: 3,
ADDED_TO_UNL: 4
}
const ClusterOwner = {
NONE: 0,
SELF_MANAGER: 1
}
const LifePlan = {
STATIC: 'stat',
INCREMENTAL: 'inc',
RANDOM: 'rand',
}
class ClusterManager {
#size;
#tenantSecret;
#ownerPrivateKey;
#moments;
#contractId;
#instanceImage;
#config;
#hosts;
multisig;
#quorum;
#signerCount;
#signerMoments;
#lifePlan
#lifeGap
#minLifeMoments;
#maxLifeMoments;
#nodes;
#evernodeMgr;
#signers;
#evrBalance;
#leaseAmounts;
constructor(options = {}) {
this.#size = options.size || 3;
this.#tenantSecret = options.tenantSecret;
this.#ownerPrivateKey = options.ownerPrivateKey;
this.#moments = options.moments || 1;
this.#contractId = options.contractId || uuid.v4();
this.#instanceImage = options.instanceImage || appenv.instanceImage;
this.#config = options.config || {};
this.multisig = options.multisig || false;
this.#lifePlan = options.lifePlan || LifePlan.STATIC;
this.#evrBalance = options.evrLimit || null;
this.#leaseAmounts = [];
if (this.#lifePlan == LifePlan.RANDOM) {
this.#minLifeMoments = options.minLifeMoments || this.#moments;
this.#maxLifeMoments = options.maxLifeMoments || this.#moments;
} else if (this.#lifePlan == LifePlan.INCREMENTAL) {
this.#lifeGap = options.lifeGap;
}
if (this.multisig) {
this.#signers = options.signers || [];
this.#signerCount = this.#signers.length || options.signerCount || this.#size;
// Required minimum accumulated weight for txn submission.
// Quorum = quorum as a ratio * sum of weights
this.#quorum = (this.#signers.length > 0)
? Math.ceil(options.quorum * (this.#signers.reduce(function (acc, s) { return acc + s.weight }, 0)))
: Math.ceil(options.quorum * this.#signerCount); // Here we consider the signer weight as 1. Hence the sum of weight will be equal to `this.#signerCount`.
this.#signerMoments = options.signerMoments || this.#moments;
}
}
async init(hostAddresses) {
if (!this.#tenantSecret)
throw "Tenant secret is missing!";
else if (!this.#ownerPrivateKey)
throw "Owner private key is missing!";
else if (!this.#instanceImage)
throw "Instance image is missing!";
this.#evernodeMgr = new EvernodeManager({
tenantSecret: this.#tenantSecret
});
await this.#evernodeMgr.init();
const hostList = (await this.#evernodeMgr.getActiveHosts(hostAddresses)).filter(h => h.maxInstances > h.activeInstances);
this.#hosts = hostList.reduce((map, host) => {
host.blacklistScore = 0;
map[host.address] = host;
return map;
}, {});
this.#nodes = [];
}
async terminate() {
if (this.#evernodeMgr)
await this.#evernodeMgr.terminate();
}
getClusterInfoCachePath() {
const createRef = crypto.createHash('md5').update(process.argv.join()).digest('hex');
const tmpClusterDir = `${os.tmpdir()}/evdevkit-cluster`;
const tmpClusterName = `partial-cluster-${createRef}.json`;
return `${tmpClusterDir}/${tmpClusterName}`;
}
cacheClusterInfo() {
const cachePath = this.getClusterInfoCachePath();
const cacheDir = path.dirname(cachePath);
if (!fs.existsSync(cacheDir))
fs.mkdirSync(cacheDir);
fs.writeFileSync(cachePath, JSON.stringify(this.#nodes, null, 4));
}
getClusterInfoCache() {
const cachePath = this.getClusterInfoCachePath();
return fs.existsSync(cachePath) ? JSON.parse(fs.readFileSync(cachePath)) : [];
}
clearClusterInfoCache() {
const cachePath = this.getClusterInfoCachePath();
if (fs.existsSync(cachePath))
fs.rmSync(cachePath);
}
async #createNode(hostAddress, ownerPubKey, config = {}, leaseOfferIndex = null, tenantSequence = null) {
// Set consensus mode to public since primary node need to send proposals to others.
if (!config.contract)
config.contract = {};
if (!config.contract.consensus)
config.contract.consensus = {};
config.contract.consensus.mode = "public";
return await this.#evernodeMgr.acquire(
hostAddress,
1,
ownerPubKey,
this.#contractId,
this.#instanceImage,
config,
{ tenantSequence: tenantSequence, leaseOfferIndex: leaseOfferIndex });
}
async #extendCluster() {
let tenantSequence = await this.#evernodeMgr.getTenantSequence();
const assignedLives = [];
let success = true;
await Promise.all(this.#nodes.map(async (node, i) => {
await new Promise(resolve => setTimeout(resolve, 500 * i));
try {
let life;
switch (this.#lifePlan) {
case LifePlan.RANDOM: {
const isFarEnough = (lifeValue, lifeArray, minimumDistance) => {
if (lifeArray.length == 0)
return true;
for (let i = 0; i < lifeArray.length; i++) {
if (Math.abs(lifeValue - lifeArray[i]) < minimumDistance) {
return false;
}
}
return true;
}
life = Math.floor(
Math.random() * (this.#maxLifeMoments - this.#minLifeMoments) + this.#minLifeMoments);
while (!isFarEnough(life, assignedLives, 1)) {
life = Math.floor(
Math.random() * (this.#maxLifeMoments - this.#minLifeMoments) + this.#minLifeMoments);
}
assignedLives.push(life)
break;
}
case LifePlan.INCREMENTAL: {
life = 1 + i * this.#lifeGap;
break;
}
default: {
life = node.signer_detail ? this.#signerMoments : this.#moments;
break;
}
}
if (life > 1) {
info(`Extending ${node.name} by ${life - 1} moments...`);
await this.#evernodeMgr.extend(node.host, node.name, life - 1, { tenantSequence: tenantSequence++ });
}
node.life_moments = life;
}
catch (e) {
success = false;
error(e.reason || e);
}
}));
return success;
}
async #createClusterChunk(chunkSize = 1, optimalNodes) {
const nodes = [];
const curNodeCount = this.#nodes.length;
let tenantSequence = await this.#evernodeMgr.getTenantSequence();
await Promise.all(Array(chunkSize).fill(0).map(async (v, i) => {
await new Promise(resolve => setTimeout(resolve, 1000 * i));
const host = optimalNodes[(curNodeCount + i) % optimalNodes.length];
const nodeNumber = curNodeCount + i + 1;
let nodeNumberText = nodeNumber;
if (Math.floor(nodeNumber / 10) != 1 && nodeNumber % 10 === 1)
nodeNumberText += 'st';
else if (Math.floor(nodeNumber / 10) != 1 && nodeNumber % 10 === 2)
nodeNumberText += 'nd';
else if (Math.floor(nodeNumber / 10) != 1 && nodeNumber % 10 === 3)
nodeNumberText += 'rd';
else
nodeNumberText += 'th';
try {
info(`Creating ${nodeNumberText} node on host ${host.address}...`);
const hostLeases = await this.#evernodeMgr.getHostLeases(host.address);
const selectedLeaseIndex = hostLeases && hostLeases[0] && hostLeases[0].index;
if (!selectedLeaseIndex)
throw "No offers available.";
let config = JSON.parse(JSON.stringify(this.#config));
if (!config.mesh) config.mesh = {}
if (this.#nodes.length > 0) {
const primaryNode = this.#nodes[0];
config.mesh.known_peers = [`${primaryNode.domain}:${primaryNode.peer_port}`];
// If the cluster does not require multi-sig feature, then we can allow other nodes to sync with primary node from the beginning.
if (!this.multisig) {
if (!config.contract) config.contract = {}
config.contract.unl = [primaryNode.pubkey];
}
}
// Set random user keys for the secondary nodes.
let userKeys = await generateKeys((this.multisig && this.#nodes.length > 0) ? null : this.#ownerPrivateKey, 'hex');
const result = await this.#createNode(host.address, userKeys.publicKey, config, selectedLeaseIndex, tenantSequence++);
info(`${nodeNumberText} node created! Name: ${result.name}`);
nodes.push({ host: host.address, userKeys: userKeys, ...result });
this.#hosts[host.address].activeInstances++;
}
catch (e) {
log(`${nodeNumberText} node creation failed!`, e.reason || e);
this.#hosts[host.address].blacklistScore += this.#incrementBlacklistScore(e.reason);
}
if (this.#evrBalance != null) {
this.#evrBalance -= host.leaseAmount;
}
}));
return nodes;
}
#incrementBlacklistScore(reason = null) {
const instantBanReasons = ['max_alloc_reached', 'HOST_INVALID'];
if (instantBanReasons.includes(reason))
return BLACKLIST_SCORE_THRESHOLD;
else
return 1;
}
#estimateCost(preferredHosts, targetSize, returnNodes = false) {
if (!preferredHosts || preferredHosts.length == 0)
throw `All hosts are invalid or occupied.`;
let optimalCost = 0;
let optimalNodes = [];
let totalInstances = 0;
for (const host of preferredHosts) {
totalInstances += host.availableInstances;
}
if (targetSize > totalInstances)
throw `Number of available instances of the preferred hosts is insufficient to create the cluster.`
while (optimalNodes.length < targetSize) {
for (let hostIndex in preferredHosts) {
let host = preferredHosts[hostIndex]
if (host.availableInstances > 0 && optimalNodes.length < targetSize) {
optimalNodes.push(host);
optimalCost += host.leaseAmount;
host.availableInstances -= 1;
}
}
}
optimalCost *= this.#moments;
if (this.#evrBalance != null && optimalCost > this.#evrBalance)
throw `Defined EVR limit is insufficient. Estimated cost for remaining node creation: ${optimalCost}`
if (returnNodes)
return optimalNodes;
else
info(`EVR cost estimated for cluster creation: ${optimalCost}`);
}
async #checkFeasibility(targetSize, preferredHostsArray) {
let hosts = [];
hosts = Object.values(this.#hosts).filter((host) => host.maxInstances > host.activeInstances);
let preferredHosts = preferredHostsArray.map(ph => hosts.find(h => h.address === ph)).filter(h => h);
for (const host of preferredHosts) {
host.availableInstances = host.maxInstances - host.activeInstances;
this.#leaseAmounts.push({ host: host.address, leaseAmount: host.leaseAmount })
}
preferredHosts = preferredHosts.map(({ address, availableInstances, leaseAmount }) => ({ address, availableInstances, leaseAmount }))
.filter(h => h.leaseAmount != null).sort((a, b) => a.leaseAmount - b.leaseAmount);
this.#estimateCost(preferredHosts, targetSize);
}
#getOptimalNodesList(targetSize, preferredHostsArray) {
const hosts = Object.values(this.#hosts).filter((host) => host.blacklistScore < BLACKLIST_SCORE_THRESHOLD &&
host.maxInstances > host.activeInstances);
let preferredHosts = preferredHostsArray.map(ph => hosts.find(h => h.address === ph)).filter(h => h);
for (const host of preferredHosts) {
host.availableInstances = host.maxInstances - host.activeInstances;
if (this.#evrBalance)
host.leaseAmount = this.#leaseAmounts.find(la => la.host === host.address)?.leaseAmount || null;
host.nodes = this.#nodes.filter((node) => node.host === host.address).length;
}
preferredHosts = preferredHosts.map(({ address, availableInstances, leaseAmount, nodes }) => ({ address, availableInstances, leaseAmount, nodes }));
if (this.#evrBalance)
preferredHosts = preferredHosts.filter(h => h.leaseAmount != null).sort((a, b) => a.leaseAmount - b.leaseAmount);
const optimalNodes = this.#estimateCost(preferredHosts, targetSize, true);
return optimalNodes;
}
async createCluster(preferredHostsArray, recover = false) {
if (recover) {
const nodes = this.getClusterInfoCache();
if (nodes.length > 0) {
this.#nodes.push(...nodes);
this.#contractId = nodes[0].contract_id;
}
}
let targetSize = this.#size - this.#nodes.length;
//Initial feasibility check before cluster creation
if (this.#evrBalance) {
await this.#checkFeasibility(targetSize, preferredHostsArray);
}
while (targetSize > 0) {
const chunkSize = Math.min((targetSize == this.#size ? 1 : targetSize), preferredHostsArray.length)
const optimalNodes = this.#getOptimalNodesList(chunkSize, preferredHostsArray);
if (optimalNodes.length == 0)
throw 'No available optimal nodes to acquire.';
const nodes = await this.#createClusterChunk(chunkSize, optimalNodes);
this.#nodes.push(...nodes);
this.cacheClusterInfo();
targetSize -= nodes.length;
}
// Set the signers if the cluster requires multi-sig feature.
if (this.multisig) {
this.#signers = await this.#evernodeMgr.setSigners(this.#signers, this.#quorum, this.#signerCount);
// Map the signers to nodes. (No any order)
for (let i = 0; i < this.#signerCount; i++) {
this.#nodes[i].signer_detail = this.#signers[i];
}
this.cacheClusterInfo();
}
info(`Extending the nodes life...`);
if (!(await this.#extendCluster())) {
this.cacheClusterInfo();
throw 'Error occurred while extending the nodes.';
}
else {
this.cacheClusterInfo();
}
return this.#nodes;
}
async writeSigner(destination, nodePubkey) {
const normalizedPath = path.normalize(destination);
const node = this.#nodes.find(n => n.pubkey === nodePubkey && n.hasOwnProperty('signer_detail'));
if (node)
fs.writeFileSync(normalizedPath, JSON.stringify(node.signer_detail, null, 4));
}
getTenantAddress() {
return this.#evernodeMgr.getTenantAddress();
}
}
module.exports = {
ClusterManager,
NodeStatus,
ClusterOwner,
LifePlan
};
/***/ }),
/***/ 97447:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const fs = __nccwpck_require__(79896);
const path = __nccwpck_require__(16928);
const uuid = __nccwpck_require__(76193);
const appenv = __nccwpck_require__(2785);
const { exec } = __nccwpck_require__(68310);
const { ClusterManager, NodeStatus, ClusterOwner, LifePlan } = __nccwpck_require__(24475);
const { CONSTANTS, bundleContract, generateKeys, validateArrayElements, removeDirectorySync, questionSync } = __nccwpck_require__(51034);
const { EvernodeManager } = __nccwpck_require__(81917);
const { InstanceManager } = __nccwpck_require__(57482);
const { error, info, success, log } = __nccwpck_require__(48035);
const { TimeTracker } = __nccwpck_require__(40884);
const NODES_BUNDLE_PATH = `./nodes/`;
const DEFAULT_QUORUM = 0.8;
const MAX_UPLOAD_TRIES = 5;
const DEFAULT_LIFE_GAP = 2; // Number of Moments
const MAX_LIFE_UPPER_BOUND = 48; // Number of Moments
const DEFAULT_OPERATIONAL_TIME_BOUND = 48; // Number of Hours
function version() {
info(`command: version`);
try {
const res = exec(`npm -g list ${CONSTANTS.npmPackageName} --depth=0`);
const splitted = res.toString().split('\n');
if (splitted.length > 1) {
success(`\n${splitted[1].split('@')[1]}\n`);
return;
}
}
catch (e) {
error('Error getting the version info:', e);
}
error(`\n${CONSTANTS.npmPackageName} is not installed.`);
}
async function list(options) {
info(`command: list`);
if (options.desc && !options.orderBy) {
error('orderBy option is required to order in descending manner.');
return;
}
let evernodeMgr;
try {
evernodeMgr = new EvernodeManager();
await evernodeMgr.init();
const hosts = await evernodeMgr.getActiveHostsFromLedger().catch(error);
if (hosts) {
let formatted = hosts.map(h => {
return {
address: h.address,
domain: h.domain,
ram: `${h.ramMb} MB`,
storage: `${h.diskMb} MB`,
cpu: {
model: h.cpuModelName,
time: `${h.cpuMicrosec} us`,
cores: h.cpuCount,
speed: `${h.cpuMHz} MHz`
},
sashimonoVersion: h.version,
countryCode: h.countryCode,
totalInstanceSlots: h.maxInstances,
availableInstanceSlots: h.maxInstances - h.activeInstances,
leaseFee: h.leaseAmount,
reputation: h.hostReputation,
}
});
if (formatted.length > 0 && options.orderBy && !(options.orderBy in formatted[0]))
error(`Host info does not contain a key named ${options.orderBy}.`);
if (options.orderBy)
formatted = formatted.filter(h => h[options.orderBy]).sort((a, b) => ((a[options.orderBy] - b[options.orderBy]) * (options.desc ? -1 : 1)));
log(formatted.slice(0, options.limit).map(h => {
if (!options.props)
return h;
const keys = options.props.split(',').filter(k => k in h);
let obj = {};
for (const key of keys) {
obj[key] = h[key];
}
return obj;
}));
}
}
catch (e) {
error('Error occurred while getting the host list:', e);
}
finally {
if (evernodeMgr)
await evernodeMgr.terminate();
}
}
async function hostInfo(options) {
info(`command: info`);
let evernodeMgr;
try {
let hostsToSearch = [];
let tempFileName = `hostDetails_${Date.now()}.csv`;
if (!options.filePath && !options.hostAddress)
throw 'Either host address or file path is required to search for host info.';
if (options.filePath) {
if (!fs.existsSync(options.filePath) || !fs.statSync(options.filePath).isFile())
throw `Hosts file ${options.filePath} does not exists.`;
hostsToSearch = options.filePath ? fs.readFileSync(options.filePath, 'UTF-8').split(/\r?\n/).filter(h => h) : [];
}
if (options.hostAddress && (typeof options.hostAddress !== 'string' || options.hostAddress.trim() === ''))
throw 'Host address should be a non-empty string.';
hostsToSearch.push(options.hostAddress);
if (options.output && (!(fs.existsSync(options.output) && fs.statSync(options.output).isDirectory())))
throw `Output path ${options.output} does not exist or is not a directory.`;
evernodeMgr = new EvernodeManager();
await evernodeMgr.init();
// Fetch all host details in parallel
const allHostDetails = await Promise.all(
hostsToSearch.map(async (hostAddress) => {
try {
const host = await evernodeMgr.getHostInfo(hostAddress);
if (host) {
return {
address: host.address,
domain: host.domain,
ram: `${host.ramMb} MB`,
storage: `${host.diskMb} MB`,
cpu: {
model: host.cpuModelName,
time: `${host.cpuMicrosec} us`,
cores: host.cpuCount,
speed: `${host.cpuMHz} MHz`
},
sashimonoVersion: host.version,
countryCode: host.countryCode,
totalInstanceSlots: host.maxInstances,
availableInstanceSlots: host.maxInstances - host.activeInstances,
active: host.active
};
}
} catch (error) {
console.error(`Error fetching details for host ${hostAddress}:`, error);
}
})
);
const validHostDetails = allHostDetails.filter(host => host !== undefined);
// Write them to the CSV file
if (options.output && validHostDetails.length > 0) {
tempFileName = path.join(options.output, tempFileName);
const csvHeader = [
'Address', 'Domain', 'RAM', 'Storage', 'CPU Model', 'CPU Time',
'CPU Cores', 'CPU Speed', 'Sashimono Version', 'Country Code',
'Total Instance Slots', 'Available Instance Slots', 'Active'
];
const csvData = validHostDetails.map(host => [
host.address,
host.domain,
host.ram,
host.storage,
host.cpu.model,
host.cpu.time,
host.cpu.cores,
host.cpu.speed,
host.sashimonoVersion,
host.countryCode,
host.totalInstanceSlots,
host.availableInstanceSlots,
host.active
]);
// Convert header and data into CSV format
const csvContent = [csvHeader, ...csvData].map(row => row.join(',')).join('\n');
// Write the CSV string to the temporary file
fs.writeFile(tempFileName, csvContent, (err) => {
if (err) {
console.error('Error writing to the CSV file:', err.message);
} else {
console.log('CSV file written successfully.');
}
});
}
log(validHostDetails);
}
catch (e) {
error('Error occurred while getting the host info:', e);
}
finally {
if (evernodeMgr)
await evernodeMgr.terminate();
}
}
async function keygen() {
info(`command: keygen`);
try {
const keys = await generateKeys();
success('New key pair generated', keys);
info('Record these keys and set the private key to the environment variable called EV_USER_PRIVATE_KEY for future operations.');
}
catch (e) {
error('Error occurred while generating key pair:', e);
}
}
async function acquire(host, options) {
info(`command: acquire`);
let evernodeMgr;
try {
evernodeMgr = new EvernodeManager({
tenantSecret: appenv.tenantSecret
});
await evernodeMgr.init();
const userKeys = await generateKeys(appenv.userPrivateKey, 'hex');
const result = await evernodeMgr.acquire(
host,
options.moments || 1,
userKeys.publicKey,
options.contractId,
options.image,
appenv.hpInitCfg || {});
success('Instance created!', result);
}
catch (e) {
error('Error occurred while acquiring the instance:', e);
}
finally {
if (evernodeMgr)
await evernodeMgr.terminate();
}
}
async function extend(instancesFilePath, options) {
info(`command: extend`);
let evernodeMgr;
try {
if (!instancesFilePath || !fs.existsSync(instancesFilePath))
throw 'Instance file path does not exist.';
evernodeMgr = new EvernodeManager({
tenantSecret: appenv.tenantSecret
});
await evernodeMgr.init();
const moments = options?.moments ? options.moments : 1;
// Read contents of the file
const data = fs.readFileSync(instancesFilePath, 'UTF-8');
// Split the contents by new line
const instances = data.split(/\r?\n/).filter(e => e);
await Promise.all(instances.map(async (line, i) => {
await new Promise(resolve => setTimeout(resolve, 1000 * i));
try {
let [hostAddress, instanceName, life] = line.split(":");
hostAddress = hostAddress.trim();
instanceName = instanceName.trim();
if (life)
life = life.trim();
if (!hostAddress || !instanceName)
throw 'Host address and instance name are required.';
if (life && isNaN(life))
throw 'Moment life should be a integer number.';
else if (life) {
life = Number(life);
if (!Number.isInteger(life))
throw 'Moment life should be a integer number.';
}
else {
life = moments;
}
const result = await evernodeMgr.extend(hostAddress, instanceName, life);
info(`Extending the instance for ${life} ${life === 1 ? 'moment' : 'moments'}.`);
success(`Extension txn Ref: ${result.extendRefId}\nExpiry moment: ${result.expiryMoment}`);
}
catch (e) {
error(e.reason || e);
}
}));
}
catch (e) {
error('Error occurred while extending the instance:.', e);
}
finally {
if (evernodeMgr)
await evernodeMgr.terminate();
}
}
async function extendInstance(hostAddress, instanceName, options) {
info(`command: extend`);
let evernodeMgr;
try {
evernodeMgr = new EvernodeManager({
tenantSecret: appenv.tenantSecret
});
await evernodeMgr.init();
const moments = options?.moments ? options.moments : 1;
const result = await evernodeMgr.extend(hostAddress, instanceName, moments);
info(`Extending the instance for ${moments} ${moments === 1 ? 'moment' : 'moments'}.`);
success(`Extension txn Ref: ${result.extendRefId}\nExpiry moment: ${result.expiryMoment}`);
}
catch (e) {
error('Error occurred while extending the instance:.', e);
}
finally {
if (evernodeMgr)
await evernodeMgr.terminate();
}
}
async function audit(options) {
info(`command: audit`);
let hostsToAudit = [];
let auditResults = [];
let totalAmount = 0;
let evernodeMgr, evrBalance;
const errorDescription = {
1: 'Host inactive',
2: 'Host invalid (not registered)',
3: 'No lease offer',
4: 'Timeout during Acquire',
5: 'User Install error during Acquire',
6: 'Transaction failed',
7: 'Insufficient funds during Acquire',
8: 'Connection failed during auditing',
9: 'Transaction failed due to timeout',
100: 'Unknown error',
'N/A': 'Not Applicable'
};
const errorMap = {
'HOST_INACTIVE': 1,
'HOST_INVALID': 2,
'NO_OFFER': 3,
'TIMEOUT': 4,
'user_install_error': 5,
'TRANSACTION_FAILURE': 6,
'TRANSACTION_FAILURE (tecINSUFFICIENT_FUNDS)': 7,
'Connection failed': 8,
'TRANSACTION_FAILURE (TimeoutError)': 9,
'N/A': 'N/A'
};
const expectedOperationalTime = options?.opTime ? options.opTime : DEFAULT_OPERATIONAL_TIME_BOUND;
try {
if (options.filePath) {
if (!fs.existsSync(options.filePath) || !fs.statSync(options.filePath).isFile())
throw `Hosts file ${options.filePath} does not exists.`;
hostsToAudit = options.filePath ? fs.readFileSync(options.filePath, 'UTF-8').split(/\r?\n/).filter(h => h) : [];
}
if (options.hostAddress)
hostsToAudit.push(options.hostAddress);
if (!hostsToAudit || hostsToAudit.length == 0)
throw `No hosts specified to audit.`;
evernodeMgr = new EvernodeManager({
tenantSecret: (options?.aliveness) ? null : appenv.tenantSecret
});
await evernodeMgr.init();
let userKeys;
if (!options?.aliveness)
userKeys = await generateKeys(appenv.userPrivateKey, 'hex');
const defaultValue = 'N/A';
const auditStatus = ['Success', 'Failed', 'Cannot Audit'];
const AuditResult = (hostAddress, status, alivenessData, timeAcquire = defaultValue, timeReadRequestResponse = defaultValue, timeContractResponse = defaultValue, hpVersion = defaultValue, ledgerSeqNo = defaultValue, errorMessage = defaultValue, inactiveStatus = null) => {
return {
"HOST ADDRESS": hostAddress,
"STATUS": inactiveStatus || auditStatus[status],
"CONT_ALIVENESS": alivenessData.aliveness,
"SUSTAINED_UP_TIME (H:m)": alivenessData.uptime,
"ACQUISITION DURATION (s)": timeAcquire,
"READ RES DURATION (s)": timeReadRequestResponse,
"CONTRACT RES DURATION (s)": timeContractResponse,
"HP VER": hpVersion,
"LEDGER SEQ NO": ledgerSeqNo,
"ERROR": errorMap[errorMessage] || '100'
}
}
const AlivenessResult = (hostAddress, alivenessData) => {
return {
"HOST ADDRESS": hostAddress,
"CONT_ALIVENESS": alivenessData.aliveness,
"SUSTAINED_UP_TIME (H:m)": alivenessData.uptime
}
}
if (!options?.aliveness) {
evrBalance = parseFloat(await evernodeMgr.getEVRBalance());
for (let hostIndex in hostsToAudit) {
const hostAddress = hostsToAudit[hostIndex];
const leases = await evernodeMgr.getHostLeases(hostAddress);
if (leases.length === 0) {
totalAmount += 0;
} else {
const amountValue = parseFloat(leases[0].Amount.value);
if (!isNaN(amountValue)) {
totalAmount += amountValue;
} else {
console.error('Invalid amount value in the first lease:', leases[0].Amount.value);
}
}
}
if (evrBalance < totalAmount) {
console.log(`Not enough EVRs to proceed.\nNeed ${totalAmount} EVRs.\nBut you have only ${evrBalance} EVRs.`)
process.exit(1);
}
else {
const answer = await questionSync(`It will cost ${totalAmount} EVRs from your account for the Audit. Do you wish to proceed [Y/n] ? `);
if ((answer.trim().toLowerCase() === 'n')) {
console.log("Exiting...");
process.exit(0);
}
else if (answer.trim().toLowerCase() !== 'y' && answer.trim() !== '') {
console.log('Invalid input. Please enter either "y" or "n".\nExiting...');
process.exit(0);
}
}
}
const currentTimestamp = (Date.now()) / 1000;
const latestXrplLedgerIndex = evernodeMgr.getLatestLedgerIndex();
for (let hostIndex in hostsToAudit) {
const hostAddress = hostsToAudit[hostIndex];
const timeTracker = new TimeTracker();
let timeAcquire, timeContractResponse, timeReadRequestResponse, hpVersion, ledgerSeqNo, status, errorMessage, alivenessData;
let inactiveStatus = null;
let instanceMgr;
try {
info(`Auditing ${hostAddress} ...`);
alivenessData = await evernodeMgr.checkHostRealAliveness(hostAddress, currentTimestamp, latestXrplLedgerIndex, expectedOperationalTime);
if (!options?.aliveness) {
timeTracker.start();
const result = await evernodeMgr.acquire(
hostAddress,
1,
userKeys.publicKey,
uuid.v4(),
options.image || appenv.instanceImage,
appenv.hpInitCfg || {});
success('Instance created!', result);
timeAcquire = timeTracker.end();
const instanceIp = result.domain;
const instanceUserPort = result.user_port;
instanceMgr = new InstanceManager({
ip: instanceIp,
userPort: instanceUserPort,
userPrivateKey: appenv.userPrivateKey
});
await instanceMgr.init();
timeTracker.start();
const readRequestResponse = await instanceMgr.checkReadRequestBootstrapResponse();
if (readRequestResponse == null)
throw 'Read request response check failed'
timeReadRequestResponse = timeTracker.end();
timeTracker.start();
const bootstrapStatusResult = await instanceMgr.checkBootstrapStatus();
if (bootstrapStatusResult == null)
throw 'Bootstrap status response check failed'
timeContractResponse = timeTracker.end();
const statusResult = await instanceMgr.checkStatus();
if (statusResult == null)
throw 'Status check failed'
status = 0;
hpVersion = statusResult.hpVersion;
ledgerSeqNo = statusResult.ledgerSeqNo;
}
}
catch (e) {
status = 1;
errorMessage = e.reason;
if (e.reason == 'HOST_INACTIVE') {
const hostInfo = await evernodeMgr.getHostInfo(hostAddress);
const lastActiveTimestamp = hostInfo?.lastHeartbeatIndex || hostInfo?.registrationTimestamp;
const downtime = Math.floor(Date.now() / 1000) - lastActiveTimestamp;
const days = Math.floor(downtime / (3600 * 24));
const hours = Math.floor((downtime % (3600 * 24)) / 3600);
inactiveStatus = `Inactive(${days}D-${hours}H)`;
}
else if (e.reason == 'NO_OFFER') {
status = 2;
} else if (typeof e === 'string' && e.includes('connection failed')) {
errorMessage = 'Connection failed';
}
error(`Error occurred while auditing ${hostAddress}. Error:`, e);
}
finally {
if (options?.aliveness) {
auditResults.push(AlivenessResult(hostAddress, alivenessData));
} else {
auditResults.push(AuditResult(hostAddress, status, alivenessData, timeAcquire, timeReadRequestResponse, timeContractResponse, hpVersion, ledgerSeqNo, errorMessage, inactiveStatus));
if (instanceMgr)
await instanceMgr.terminate();
}
}
}
}
catch (e) {
error('Error occurred while auditing. Error:', e);
}
finally {
if (auditResults && auditResults.length) {
console.table(auditResults);
info(`NOTE: Considered ${expectedOperationalTime}Hrs. for the Continuous Aliveness check.`)
if (!options?.aliveness) {
info('Error Descriptions:');
for (const key in errorDescription) {
console.log(`${key}: ${errorDescription[key]}`);
}
}
}
else
error("No hosts were audited.");
if (evernodeMgr)
await evernodeMgr.terminate();
}
}
async function bundle(contractDirectoryPath, instancePublicKey, contractBin, options) {
info(`command: bundle`);
try {
contractDirectoryPath = path.normalize(contractDirectoryPath);
const stats = fs.existsSync(contractDirectoryPath) ? fs.statSync(contractDirectoryPath) : null;
if (!stats || !stats.isDirectory())
throw `Contract directory ${contractDirectoryPath} does not exists.`;
const userOverrideCfg = appenv.hpOverrideCfg || {};
const hpOverrideCfg = {
...userOverrideCfg,
contract: {
...(userOverrideCfg.contract || {}),
unl: [
...(userOverrideCfg.contract?.unl || []),
instancePublicKey
],
bin_path: contractBin,
bin_args: options.contractArgs
}
}
const bundlePath = await bundleContract(
contractDirectoryPath,
hpOverrideCfg);
if (bundlePath)
success(`Archive finished. (location: ${bundlePath})`);
} catch (e) {
error('Error occurred while bundling:', e);
}
}
async function deploy(contractBundlePath, instanceIp, instanceUserPort) {
info(`command: deploy`);
let instanceMgr;
try {
instanceMgr = new InstanceManager({
ip: instanceIp,
userPort: instanceUserPort,
userPrivateKey: appenv.userPrivateKey
});
await instanceMgr.init();
await instanceMgr.uploadBundle(contractBundlePath);
success(`Contract bundle uploaded!`);
} catch (e) {
error('Error occurred while uploading the bundle:', e);
}
finally {
if (instanceMgr)
await instanceMgr.terminate();
}
}
async function acquireAndDeploy(contractDirectoryPath, contractBin, host, options) {
info(`command: acquire-and-deploy`);
let evernodeMgr;
let instanceMgr;
try {
evernodeMgr = new EvernodeManager({
tenantSecret: appenv.tenantSecret
});
contractDirectoryPath = path.normalize(contractDirectoryPath);
const stats = fs.existsSync(contractDirectoryPath) ? fs.statSync(contractDirectoryPath) : null;
if (!stats || !stats.isDirectory())
throw `Contract directory ${contractDirectoryPath} does not exists.`;
await evernodeMgr.init();
const hpConfig = appenv.hpInitCfg || {};
const userOverrideCfg = appenv.hpOverrideCfg || {};
const userKeys = await generateKeys(appenv.userPrivateKey, 'hex');
const result = await evernodeMgr.acquire(
host,
options.moments || 1,
userKeys.publicKey,
options.contractId,
options.image,
hpConfig);
const instancePublicKey = result.pubkey;
const instanceIp = result.domain;
const instanceUserPort = result.user_port;
info('Instance created!', result);
const hpOverrideCfg = {
...userOverrideCfg,
contract: {
...(userOverrideCfg.contract || {}),
unl: [
...(userOverrideCfg.contract?.unl || []),
instancePublicKey
],
bin_path: contractBin,
bin_args: options.contractArgs
}
};
const bundlePath = await bundleContract(
contractDirectoryPath,
hpOverrideCfg);
if (!bundlePath)
throw 'Archive failed.';
info(`Archive finished. (location: ${bundlePath})`);
instanceMgr = new InstanceManager({
ip: instanceIp,
userPort: instanceUserPort,
userPrivateKey: appenv.userPrivateKey
});
await instanceMgr.init();
await instanceMgr.uploadBundle(bundlePath);
info(`Contract bundle uploaded!`);
success(`Contract deployed!`);
}
catch (e) {
error('Error occurred while deploying:', e);
}
finally {
if (evernodeMgr)
await evernodeMgr.terminate();
if (instanceMgr)
await instanceMgr.terminate();
}
}
async function clusterCreate(size, contractDirectoryPath, contractBin, hostsFilePath, options) {
info(`command: create-cluster`);
let clusterMgr;
let instanceMgr;
try {
contractDirectoryPath = path.normalize(contractDirectoryPath);
const stats = fs.existsSync(contractDirectoryPath) ? fs.statSync(contractDirectoryPath) : null;
if (!stats || !stats.isDirectory())
throw `Contract directory ${contractDirectoryPath} does not exist.`;
if (!hostsFilePath || !fs.existsSync(hostsFilePath))
throw 'Preferred Host file path does not exist.';
if (options?.signers && !fs.existsSync(options.signers))
throw 'Signer Details file path does not exist.';
if (options?.lifePlan) {
if (!(Object.values(LifePlan).includes(options.lifePlan)))
throw 'Invalid cluster node life plan is provided.';
switch (options.lifePlan) {
case LifePlan.RANDOM: {
info("Randomized node life planning is considered.");
if (options?.signerLife)
throw 'Defining --signer-life is not applicable in Random life plan.';
if (options?.moments)
throw 'Defining --moments is not applicable in Random life plan.';
if (options?.evrLimit)
throw 'Defining --evr-limit is not applicable in Random life plan.';
if (options?.lifeGap)
throw 'Defining --life-gap is not applicable in Random life plan.';
if (!options?.minLife)
throw 'Defining --min-life is not applicable in Random life plan.';
if (!options?.maxLife) {
info(`Default value of --max-life (${MAX_LIFE_UPPER_BO