Include reputationd as a sub module (#390)

This commit is contained in:
Chalith Desaman
2024-06-05 01:35:33 +05:30
committed by GitHub
parent 7fc427e6ff
commit 36ed3c83b6
15 changed files with 4 additions and 4134 deletions

3
.gitmodules vendored
View File

@@ -2,3 +2,6 @@
path = evernode-bootstrap-contract
url = https://github.com/HotPocketDev/evernode-bootstrap-contract.git
branch = release
[submodule "reputationd"]
path = reputationd
url = https://github.com/EvernodeXRPL/reputationd

1
reputationd Submodule

Submodule reputationd added at ddc00f398f

View File

@@ -1,17 +0,0 @@
{
"env": {
"browser": true,
"commonjs": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 13
},
"rules": {
"no-async-promise-executor": "off"
},
"globals": {
"__dirname": true
}
}

View File

@@ -1,5 +0,0 @@
node_modules
dist
log
reputationd.cfg
secret.cfg

View File

@@ -1,114 +0,0 @@
const process = require('process');
// Uncaught Exception Handling.
process.on('uncaughtException', (err) => {
process.removeAllListeners('uncaughtException');
process.removeAllListeners('unhandledRejection');
console.error('Unhandled exception occurred:', err?.message);
console.error('Stack trace:', err?.stack);
console.log("REPUTATIOND_EXITED");
process.exit(1);
});
// Unhandled Rejection Handling.
process.on('unhandledRejection', (reason, promise) => {
process.removeAllListeners('unhandledRejection');
process.removeAllListeners('uncaughtException');
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
console.log("REPUTATIOND_EXITED");
process.exit(1);
});
const logger = require('./lib/logger');
const { appenv } = require('./lib/appenv');
const { Setup } = require('./lib/setup');
const { ReputationD } = require('./lib/reputationd');
async function main() {
if (process.argv[2] === 'version') {
console.log(appenv.REPUTATIOND_VERSION);
}
if (process.argv.length >= 3) {
try {
if (process.argv.length >= 5 && process.argv[2] === 'new') {
const accountAddress = process.argv[3];
const accountSecretPath = process.argv[4];
const setup = new Setup();
setup.newConfig(accountAddress, accountSecretPath);
}
else if (process.argv.length >= 4 && process.argv[2] === 'wait-for-funds') {
await new Setup().waitForFunds(process.argv[3], parseFloat(process.argv[4]));
}
else if (process.argv.length >= 2 && process.argv[2] === 'prepare') {
await new Setup().prepareReputationAccount();
} else if (process.argv.length >= 4 && process.argv[2] === 'update-config') {
// TODO: Remove this in 0.8.4.
}
else if (process.argv.length >= 3 && process.argv[2] === 'upgrade') {
await new Setup().upgrade();
}
else if (process.argv.length === 3 && process.argv[2] === 'repinfo') {
await new Setup().repInfo();
}
else if (process.argv[2] === 'help') {
console.log(`Usage:
node index.js - Run message board.
node index.js version - Print version.
node index.js new [address] [secretPath] - Create new config files.
node index.js wait-for-funds [currencyType] [expectedBalance] - Wait until the funds are received.
node index.js upgrade [governorAddress] - Upgrade message board data.
node index.js help - Print help.`);
}
else {
throw "Invalid args.";
}
}
catch (err) {
process.removeAllListeners('uncaughtException');
process.removeAllListeners('unhandledRejection');
// If error is a RippledError show internal error message, Otherwise show err.
console.log(err?.data?.error_message || err);
console.log("REPUTATIOND_EXITED");
process.exit(1);
}
}
else {
try {
// Logs are formatted with the timestamp and a log file will be created inside log directory.
logger.init(appenv.LOG_PATH, appenv.FILE_LOG_ENABLED);
console.log('Starting the Evernode Xahau reputationd.' + (appenv.IS_DEV_MODE ? ' (in dev mode)' : '') + ' --- patch applied ---');
console.log('Data dir: ' + appenv.DATA_DIR);
console.log('Using message board config: ' + appenv.MB_XRPL_CONFIG_PATH);
const rep = new ReputationD(appenv.CONFIG_PATH, appenv.MB_XRPL_CONFIG_PATH, appenv.INSTANCE_IMAGE);
await rep.init();
}
catch (err) {
process.removeAllListeners('uncaughtException');
process.removeAllListeners('unhandledRejection');
// If error is a RippledError show internal error message, Otherwise show err.
console.log(err?.data?.error_message || err);
console.log("Evernode ReputationD exiting with error.");
console.log("REPUTATIOND_EXITED");
process.exit(1);
}
}
}
main().then(() => {
process.removeAllListeners('uncaughtException');
process.removeAllListeners('unhandledRejection');
console.log("REPUTATIOND_SUCCESS");
}).catch((e) => {
process.removeAllListeners('uncaughtException');
process.removeAllListeners('unhandledRejection');
console.error(e);
console.log("REPUTATIOND_EXITED");
process.exit(1);
});

View File

@@ -1,24 +0,0 @@
const process = require('process');
const path = require('path');
let appenv = {
IS_DEV_MODE: process.env.REPUTATIOND_DEV === "1",
FILE_LOG_ENABLED: process.env.REPUTATIOND_FILE_LOG === "1",
DATA_DIR: process.env.REPUTATIOND_DATA_DIR || __dirname,
INSTANCE_IMAGE: 'evernodedev/reputation:hp.latest-ubt.20.04',
}
appenv = {
...appenv,
CONFIG_PATH: appenv.DATA_DIR + '/reputationd.cfg',
LOG_PATH: appenv.DATA_DIR + '/log/reputationd.log',
REPUTATIOND_VERSION: '0.8.3',
REPUTATIOND_SCHEDULER_INTERVAL_SECONDS: 2,
MB_XRPL_CONFIG_PATH: path.join(appenv.DATA_DIR, '../') + "mb-xrpl/mb-xrpl.cfg",
}
Object.freeze(appenv);
module.exports = {
appenv
}

View File

@@ -1,62 +0,0 @@
const fs = require('fs');
const COMMON_CONFIG_KEYS = ['network', 'governorAddress', 'rippledServer'];
class ConfigHelper {
static readConfig(configPath, mbXrplConfigPath = null, readSecret = false) {
if (!fs.existsSync(configPath))
throw `Config file does not exist at ${configPath}`;
let config = JSON.parse(fs.readFileSync(configPath).toString());
if (readSecret) {
if (!fs.existsSync(config.xrpl.secretPath))
throw `Secret config file does not exist at ${config.xrpl.secretPath}`;
const secretCfg = JSON.parse(fs.readFileSync(config.xrpl.secretPath).toString());
config.xrpl = { ...config.xrpl, ...secretCfg.xrpl };
}
if (mbXrplConfigPath && fs.existsSync(mbXrplConfigPath)) {
let mbXrplConfig = JSON.parse(fs.readFileSync(mbXrplConfigPath).toString());
if (readSecret) {
if (!fs.existsSync(mbXrplConfig.xrpl.secretPath))
throw `Secret config file does not exist at ${mbXrplConfig.xrpl.secretPath}`;
const mbXrplSecretCfg = JSON.parse(fs.readFileSync(mbXrplConfig.xrpl.secretPath).toString());
mbXrplConfig.xrpl = { ...mbXrplConfig.xrpl, ...mbXrplSecretCfg.xrpl }
}
for (const e of Object.entries(mbXrplConfig.xrpl)) {
const key = COMMON_CONFIG_KEYS.includes(e[0]) ? e[0] : `host${e[0].charAt(0).toUpperCase()}${e[0].slice(1)}`;
if (!(key in config.xrpl))
config.xrpl[key] = e[1];
}
}
return config;
}
static writeConfig(config, configPath) {
let publicCfg = JSON.parse(JSON.stringify(config)); // Make a copy. So, referenced object won't get changed.
if ('secret' in publicCfg.xrpl)
delete publicCfg.xrpl.secret;
if ('network' in publicCfg.xrpl)
delete publicCfg.xrpl.network;
if ('governorAddress' in publicCfg.xrpl)
delete publicCfg.xrpl.governorAddress;
if ('rippledServer' in publicCfg.xrpl)
delete publicCfg.xrpl.rippledServer;
// Remove host related props.
for (const e of Object.entries(publicCfg.xrpl)) {
if (e[0].startsWith('host'))
delete publicCfg.xrpl[e[0]];
}
fs.writeFileSync(configPath, JSON.stringify(publicCfg, null, 2), { mode: 0o644 }); // Set file permission so only current user can read/write and others can read.
}
}
module.exports = {
ConfigHelper
}

View File

@@ -1,104 +0,0 @@
const bson = require('bson');
const HotPocket = require('hotpocket-js-client');
const { CommonHelper } = require('./util-helper');
const DEFAULT_TIMEOUT = 120000;
const INPUT_PROTOCOLS = HotPocket.protocols;
class ContractInstanceManager {
#ip;
#userPort;
#userPrivateKey;
#hpClient;
constructor(options = {}) {
this.#ip = options.ip;
this.#userPort = options.userPort;
this.#userPrivateKey = options.userPrivateKey;
}
async init() {
if (!this.#ip)
throw "Instance IP is missing!";
else if (!this.#userPort)
throw "Instance user port is missing!";
else if (!this.#userPrivateKey)
throw "Instance user private key is missing!";
const userKeys = await CommonHelper.generateKeys(this.#userPrivateKey, 'binary');
console.log('My public key is: ' + Buffer.from(userKeys.publicKey).toString('hex'));
const server = `wss://${this.#ip}:${this.#userPort}`;
this.#hpClient = await HotPocket.createClient([server], userKeys, {
protocol: HotPocket.protocols.bson
});
// Establish HotPocket connection.
if (!await this.#hpClient.connect())
throw `${server} connection failed.`;
}
async terminate() {
if (this.#hpClient)
await this.#hpClient.close()
}
async sendContractInput(input, timeoutMs = DEFAULT_TIMEOUT, protocol = HotPocket.protocols.bson) {
return new Promise(async (resolve, reject) => {
const inputTimer = setTimeout(() => {
clearTimeout(inputTimer);
this.#hpClient.clear(HotPocket.events.contractOutput);
reject("Input timeout.");
}, timeoutMs);
const failure = (e) => {
clearTimeout(inputTimer);
this.#hpClient.clear(HotPocket.events.contractOutput);
reject(e);
}
const success = (result) => {
clearTimeout(inputTimer);
resolve(result);
}
// This will get fired when contract sends an output.
this.#hpClient.on(HotPocket.events.contractOutput, (r) => {
r.outputs.forEach(output => {
let result;
try {
result = protocol === INPUT_PROTOCOLS.bson ? bson.deserialize(output) : JSON.parse(output);
}
catch (e) {
failure(e);
}
if (result?.type == `${input.type}Result`) {
if (result.status == "ok")
success(result.message);
else
failure(`Input failed. reason: ${result.message}`);
}
});
});
const res = await this.#hpClient.submitContractInput(protocol === INPUT_PROTOCOLS.bson ? bson.serialize(input) : JSON.stringify(input));
const submission = await res.submissionStatus;
if (submission.status != "accepted")
failure("Input submission failed. reason: " + submission.reason);
});
}
async sendContractReadRequest(input, protocol = HotPocket.protocols.bson) {
const output = await this.#hpClient.submitContractReadRequest(protocol === INPUT_PROTOCOLS.bson ? bson.serialize(input) : JSON.stringify(input));
const result = protocol === INPUT_PROTOCOLS.bson ? bson.deserialize(output) : JSON.parse(output);
return result?.message;
}
}
module.exports = {
ContractInstanceManager,
INPUT_PROTOCOLS
}

View File

@@ -1,118 +0,0 @@
const { CommonHelper } = require('./util-helper');
const WebSocket = require('ws');
const DEFAULT_TIMEOUT = 120000;
class LobbyManager {
#ip;
#userPort;
#userPrivateKey;
#userKeys;
#wsClient;
constructor(options = {}) {
this.#ip = options.ip;
this.#userPort = options.userPort;
this.#userPrivateKey = options.userPrivateKey;
}
async init() {
if (!this.#ip)
throw "Instance IP is missing!";
else if (!this.#userPort)
throw "Instance user port is missing!";
else if (!this.#userPrivateKey)
throw "Instance user private key is missing!";
this.#userKeys = await CommonHelper.generateKeys(this.#userPrivateKey, 'binary');
console.log('My public key is: ' + Buffer.from(this.#userKeys.publicKey).toString('hex'));
const server = `wss://${this.#ip}:${this.#userPort}`;
this.#wsClient = new WebSocket(server, {
rejectUnauthorized: false
});
}
terminate() {
if (this.#wsClient)
this.#wsClient.close()
}
#handleMessage(message) {
var message = JSON.parse(message);
switch (message.type) {
case 'upgrade':
if (message.status === 'SUCCESS')
return true;
else
throw message.data ?? 'UNKNOWN_ERROR';
default:
throw 'UNHANDLED_MESSAGE';
}
}
async upgradeContract(unl, peers, timeoutMs = DEFAULT_TIMEOUT) {
return new Promise(async (resolve, reject) => {
const inputTimer = setTimeout(() => {
clearTimeout(inputTimer);
reject("Input timeout.");
}, timeoutMs);
const failure = (e) => {
clearTimeout(inputTimer);
reject(e);
}
const success = (result) => {
clearTimeout(inputTimer);
resolve(result);
}
if (!this.#wsClient)
failure('Web socket connection is not initiated');
try {
// This will get fired when contract sends an output.
this.#wsClient.on('message', (data) => {
console.log('Received from server:', data.toString());
try {
const res = this.#handleMessage(data);
if (res)
success('CONTRACT_UPGRADED');
else
throw 'UNKNOWN_ERROR'
}
catch (e) {
failure(e);
}
});
this.#wsClient.on('open', () => {
console.log('Connection opened. Sending upgrade request...');
try {
this.#wsClient.send(JSON.stringify({
type: 'upgrade',
user: this.#userKeys.publicKey,
data: {
unl: unl,
peers: peers
}
}));
}
catch (e) {
failure(e);
}
});
}
catch (e) {
failure(e);
}
});
}
}
module.exports = {
LobbyManager
}

View File

@@ -1,40 +0,0 @@
const fs = require('fs');
const path = require('path');
const util = require('util');
const formatText = (text, logType = 'dbg') => {
const date = new Date().toISOString().
replace(/T/, ' '). // Replace T with a space.
replace(/\..+/, ''). // Delete the dot and everything after.
replace(/-/g, ''); // Delete the dashes.
return `${date} [${logType}] ${text}\n`;
}
exports.init = (logPath, fileLogEnabled) => {
let flog = null;
if (fileLogEnabled) {
const dirname = path.dirname(logPath);
if (!fs.existsSync(dirname))
fs.mkdirSync(dirname, { recursive: true });
flog = fs.createWriteStream(logPath, { flags: 'a' });
}
console.log = function () {
const text = formatText(util.format.apply(this, arguments));
process.stdout.write(text);
if (flog)
flog.write(text);
};
console.error = function () {
const text = formatText(util.format.apply(this, arguments), 'err');
process.stderr.write(text);
if (flog)
flog.write(text);
};
}

View File

@@ -1,652 +0,0 @@
const evernode = require('evernode-js-client');
const crypto = require('crypto');
const uuid = require('uuid');
const { appenv } = require('./appenv');
const { ConfigHelper } = require('./config-helper');
const { CommonHelper } = require('./util-helper');
const { ContractInstanceManager, INPUT_PROTOCOLS } = require('./contract-instance-manager');
const { LobbyManager } = require('./lobby-manager');
const ContractStatus = {
Created: 1,
Updated: 2,
Deployed: 3
}
class ReputationD {
#concurrencyQueue = {
processing: false,
queue: []
};
#applyFeeUpliftment = false;
#reputationRetryDelay = 300000; // 5 mins
#reputationRetryCount = 3;
#feeUpliftment = 0;
#preparationTimeQuota = 0.9; // Percentage of moment size.
#reputationRegTimeQuota = 0.2; // Percentage of (1 - preparationTimeQuota) for reputation registration.
#lobbyTimeQuota = 0.8; // Percentage of (1 - reputationRegTimeQuota) for reputation contract deployment.
#universeSize = 64;
#readScoreCmd = 'read_scores';
#configPath;
#mbXrplConfigPath;
#instanceImage;
constructor(configPath, mbXrplConfigPath, instanceImage) {
this.#configPath = configPath;
this.#mbXrplConfigPath = mbXrplConfigPath;
this.#instanceImage = instanceImage;
}
async init() {
this.#readConfig();
if (!this.cfg.version || !this.cfg.xrpl.address || !this.cfg.xrpl.secret)
throw "Required cfg fields cannot be empty.";
await evernode.Defaults.useNetwork(this.cfg.xrpl.network || appenv.NETWORK);
if (this.cfg.xrpl.governorAddress)
evernode.Defaults.set({
governorAddress: this.cfg.xrpl.governorAddress
});
if (this.cfg.xrpl.rippledServer)
evernode.Defaults.set({
rippledServer: this.cfg.xrpl.rippledServer
});
if (this.cfg.xrpl.fallbackRippledServers && this.cfg.xrpl.fallbackRippledServers.length)
evernode.Defaults.set({
fallbackRippledServers: this.cfg.xrpl.fallbackRippledServers
});
this.xrplApi = new evernode.XrplApi();
evernode.Defaults.set({
xrplApi: this.xrplApi
})
await this.xrplApi.connect();
this.hostClient = new evernode.HostClient(this.cfg.xrpl.hostAddress, this.cfg.xrpl.hostSecret);
await this.#connectHost();
console.log("Using,");
console.log("\tGovernor account " + this.cfg.xrpl.governorAddress);
console.log("\tReputation account " + this.hostClient.config.reputationAddress);
console.log("Using xahaud " + this.cfg.xrpl.rippledServer);
// Get last heartbeat moment from the host info.
let hostInfo = await this.hostClient.getRegistration();
if (!hostInfo)
throw "Host is not registered.";
this.reputationClient = await evernode.HookClientFactory.create(evernode.HookTypes.reputation, { config: this.hostClient.config });
await this.#connectReputation({ skipConfigs: true });
const repInfo = await this.hostClient.getReputationInfo();
// Last registered moment n means reputation is sent in n-1 moment.
this.lastReputationMoment = repInfo ? (repInfo.lastRegisteredMoment - 1) : 0;
this.xrplApi.on(evernode.XrplApiEvents.DISCONNECTED, async (e) => {
console.log(`Exiting due to server disconnect (code ${e})...`);
process.exit(1);
});
this.xrplApi.on(evernode.XrplApiEvents.SERVER_DESYNCED, async (e) => {
console.log(`Exiting due to server desync condition...`);
process.exit(1);
});
this.xrplApi.on(evernode.XrplApiEvents.LEDGER, async (e) => {
this.lastValidatedLedgerIndex = e.ledger_index;
this.lastLedgerTime = evernode.UtilHelpers.getCurrentUnixTime('milli');
});
// Start queue processor job.
this.#startReputationClockScheduler();
// Schedule reputation jobs.
this.#startReputationSendScheduler();
// Schedule reputation contract jobs.
this.#startReputationContractScheduler();
}
#prepareHostClientFunctionOptions() {
let options = {}
if (this.#applyFeeUpliftment) {
options.transactionOptions = { feeUplift: this.#feeUpliftment }
}
return options;
}
// Try to acquire the lease update lock.
async #acquireConcurrencyQueue() {
await new Promise(async resolve => {
while (this.#concurrencyQueue.processing) {
await new Promise(resolveSleep => {
setTimeout(resolveSleep, 1000);
})
}
resolve();
});
this.#concurrencyQueue.processing = true;
}
// Release the lease update lock.
async #releaseConcurrencyQueue() {
this.#concurrencyQueue.processing = false;
}
async #queueAction(action, maxAttempts = 5, delay = 0) {
await this.#acquireConcurrencyQueue();
this.#concurrencyQueue.queue.push({
callback: action,
submissionRefs: {},
attempts: 0,
maxAttempts: maxAttempts,
delay: delay
});
await this.#releaseConcurrencyQueue();
}
async #processConcurrencyQueue() {
await this.#acquireConcurrencyQueue();
let toKeep = [];
for (let action of this.#concurrencyQueue.queue) {
try {
await action.callback(action.submissionRefs);
this.#applyFeeUpliftment = false;
this.#feeUpliftment = 0;
}
catch (e) {
console.error(e);
if (action.attempts < action.maxAttempts) {
action.attempts++;
console.log(`Retry attempt ${action.attempts}`);
if (this.cfg.xrpl.affordableExtraFee > 0 && e.status === "TOOK_LONG") {
this.#applyFeeUpliftment = true;
this.#feeUpliftment = Math.floor((this.cfg.xrpl.affordableExtraFee * action.attempts) / action.maxAttempts);
}
if (action.delay > 0) {
new Promise((resolve) => {
const checkFlagInterval = setInterval(() => {
if (!this.#concurrencyQueue.processing) {
this.#concurrencyQueue.queue.push(action);
clearInterval(checkFlagInterval);
resolve();
}
}, action.delay);
});
} else
toKeep.push(action);
}
else {
console.error('Max retry attempts reached. Abandoned.');
}
}
}
this.#concurrencyQueue.queue = toKeep;
await this.#releaseConcurrencyQueue();
}
// Connect the host and trying to reconnect in the event of account not found error.
// Account not found error can be because of a network reset. (Dev and test nets)
async #connect(client, options = null) {
let attempts = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
try {
attempts++;
const ret = options ? await client.connect(options) : await client.connect();
if (ret)
break;
} catch (error) {
if (error?.data?.error === 'actNotFound') {
let delaySec;
// The maximum delay will be 5 minutes.
if (attempts > 150) {
delaySec = 300;
} else {
delaySec = 2 * attempts;
}
console.log(`Network reset detected. Attempt ${attempts} failed. Retrying in ${delaySec}s...`);
await new Promise(resolve => setTimeout(resolve, delaySec * 1000));
} else
throw error;
}
}
}
async #connectHost() {
await this.#connect(this.hostClient, { reputationAddress: this.cfg.xrpl.address, reputationSecret: this.cfg.xrpl.secret });
}
async #connectReputation(options = {}) {
await this.#connect(this.reputationClient, options);
}
async #startReputationClockScheduler() {
const timeout = appenv.REPUTATIOND_SCHEDULER_INTERVAL_SECONDS * 1000; // Seconds to millisecs.
const scheduler = async () => {
await this.#processConcurrencyQueue();
setTimeout(async () => {
await scheduler();
}, timeout);
};
setTimeout(async () => {
await scheduler();
}, timeout);
}
async #startReputationSendScheduler() {
const momentSize = this.hostClient.config.momentSize;
const timeout = momentSize * 1000; // Converting seconds to milliseconds.
const scheduler = async () => {
setTimeout(async () => {
await scheduler();
}, timeout);
await this.#sendReputations();
};
let startTimeout = 0;
const momentStartTimestamp = await this.hostClient.getMomentStartIndex();
const currentTimestamp = evernode.UtilHelpers.getCurrentUnixTime();
const currentMoment = await this.hostClient.getMoment();
// Set time relative to current passed time.
const timeQuota = momentSize * (1 - this.#preparationTimeQuota);
const upperBound = Math.floor(momentStartTimestamp + momentSize - (timeQuota * (1 - this.#reputationRegTimeQuota)));
const lowerBound = Math.floor(momentStartTimestamp + momentSize - timeQuota);
if (currentTimestamp < lowerBound || currentTimestamp >= upperBound)
startTimeout = Math.floor(lowerBound + (Math.random() * ((upperBound - lowerBound) / 2)) - currentTimestamp) * 1000 // Converting seconds to milliseconds.
// If already registered for this moment, Schedule for next moment.
if (startTimeout < 0 || this.lastReputationMoment === currentMoment)
startTimeout += (momentSize * 1000);
console.log(`Reputation sender scheduled to start in ${startTimeout} milliseconds.`);
setTimeout(async () => {
await scheduler();
}, startTimeout);
}
async #startReputationContractScheduler() {
const momentSize = this.hostClient.config.momentSize;
const timeout = momentSize * 1000; // Converting seconds to milliseconds.
const scheduler = async () => {
setTimeout(async () => {
await scheduler();
}, timeout);
await this.#createReputationContract();
};
let startTimeout = 0;
const momentStartTimestamp = await this.hostClient.getMomentStartIndex();
const currentTimestamp = evernode.UtilHelpers.getCurrentUnixTime();
const currentMoment = await this.hostClient.getMoment();
const timeQuota = momentSize * (1 - this.#preparationTimeQuota);
const upperBound = Math.floor(momentStartTimestamp + momentSize);
const lowerBound = Math.floor(momentStartTimestamp + momentSize - (timeQuota * (1 - this.#reputationRegTimeQuota)));
if (currentTimestamp < lowerBound)
startTimeout = Math.floor(lowerBound - currentTimestamp) * 1000;
// If deploy widow has passed or, not registered for next moment, Schedule for next moment.
if (currentTimestamp > upperBound || (startTimeout === 0 && this.lastReputationMoment !== currentMoment))
startTimeout = Math.floor(lowerBound + momentSize - currentTimestamp) * 1000;
// If zero, We are in the deploy window. Try to deploy now and schedule the next in start of next moments window.
if (startTimeout === 0) {
console.log(`Reputation contract creation will be done now since we are in the window.`);
setTimeout(async () => {
await this.#createReputationContract();
}, 0);
startTimeout = Math.floor(lowerBound + momentSize - currentTimestamp) * 1000;
console.log(`Next reputation contract creation scheduled to start in ${startTimeout} milliseconds.`);
}
else {
console.log(`Reputation contract creation scheduled to start in ${startTimeout} milliseconds.`);
}
setTimeout(async () => {
await scheduler();
}, startTimeout);
}
async #getUniverseInfo(moment) {
if (!this.hostClient.reputationAcc)
return null;
const orderInfo = await this.reputationClient.getReputationOrderByAddress(this.hostClient.reputationAcc.address, moment);
if (!orderInfo || !('orderedId' in orderInfo))
return null;
return {
universeIndex: Math.floor(orderInfo.orderedId / this.#universeSize)
};
}
async #getInstancesInUniverse(universeIndex, moment) {
const minOrderedId = universeIndex * this.#universeSize;
return (await Promise.all(Array.from({ length: this.#universeSize }, (_, i) => i + minOrderedId).map(async (orderedId) => {
const repInfo = await this.reputationClient.getReputationContractInfoByOrderedId(orderedId, moment);
if (!repInfo)
return null;
return repInfo.contract;
}))).filter(i => i);
}
// Find the universe id and generate contract id.
#generateContractId(universeIndex) {
const buf = Buffer.alloc(4, 0);
buf.writeUint32LE(universeIndex);
// Generate a hash from the seed
const hash = crypto.createHash('sha1').update(buf.toString('hex')).digest('hex');
// Use a portion of the hash to generate a random UUID
const id = uuid.v4({
random: Buffer.from(hash, 'hex')
});
return id;
}
async #upgradeContract(instanceIp, instanceUserPort, userPrivateKey, unl, peers) {
let lobbyMgr;
try {
lobbyMgr = new LobbyManager({
ip: instanceIp,
userPort: instanceUserPort,
userPrivateKey: userPrivateKey
});
await lobbyMgr.init();
await lobbyMgr.upgradeContract(unl, peers);
if (lobbyMgr)
lobbyMgr.terminate();
console.log(`Contract bundle uploaded!`);
} catch (e) {
if (lobbyMgr)
lobbyMgr.terminate();
throw e;
}
}
// Create and setup reputation contract.
async #createReputationContract() {
const scheduledMoment = await this.hostClient.getMoment();
await this.#queueAction(async (submissionRefs) => {
const curMoment = await this.reputationClient.getMoment();
const universeInfo = await this.#getUniverseInfo(curMoment + 1);
if (!universeInfo) {
console.log(`Skipping reputation contract preparation since there's no universe info for the moment ${curMoment + 1}.`);
return;
}
if (scheduledMoment != curMoment) {
console.log(`Skipping since scheduled moment has passed. Scheduled in ${scheduledMoment}, Current moment ${curMoment}.`);
return;
}
else if (this.lastReputationMoment !== curMoment) {
console.log(`Skipping reputation contract preparation since not registered for the moment ${curMoment + 1}.`);
return;
}
console.log(`Preparing reputation contract for the Moment ${curMoment + 1}...`);
let createdMoment = this.cfg.contractInstance?.created_moment ?? -1;
let acquireSentMoment = this.cfg.contractInstance?.transaction ? (this.cfg.contractInstance?.acquire_sent_moment ?? -1) : -1;
if (curMoment > createdMoment) {
const tenantClient = new evernode.TenantClient(this.hostClient.reputationAcc.address, this.hostClient.reputationAcc.secret);
await tenantClient.connect();
submissionRefs.refs ??= [{}, {}];
// Check again wether the transaction is validated before retry.
const txHash1 = submissionRefs?.refs[0]?.submissionResult?.result?.tx_json?.hash;
let retry = true;
if (txHash1) {
const txResponse = await tenantClient.xrplApi.getTransactionValidatedResults(txHash1);
if (txResponse && txResponse.code === "tesSUCCESS") {
console.log('Transaction is validated and success, Retry skipped!');
retry = false;
}
}
if (retry) {
await tenantClient.prepareAccount({ submissionRef: submissionRefs?.refs[0], ...this.#prepareHostClientFunctionOptions() });
}
// Check again wether the transaction is validated before retry.
const txHash2 = submissionRefs?.refs[1]?.submissionResult?.result?.tx_json?.hash;
retry = true;
if (txHash2) {
const txResponse = await tenantClient.xrplApi.getTransactionValidatedResults(txHash2);
if (txResponse && txResponse.code === "tesSUCCESS") {
console.log('Transaction is validated and success, Retry skipped!')
retry = false;
}
}
if (retry) {
if (curMoment > createdMoment ||
curMoment > acquireSentMoment) {
console.log(`Acquiring the reputation contract instance...`);
const ownerKeys = await CommonHelper.generateKeys();
const contractId = this.#generateContractId(universeInfo.universeIndex);
let requirement = {
owner_pubkey: ownerKeys.publicKey,
contract_id: contractId,
image: this.#instanceImage,
config: {}
};
// Update the registry with the active instance count.
const transaction = await tenantClient.acquireLeaseSubmit(this.hostClient.xrplAcc.address, requirement, { submissionRef: submissionRefs?.refs[1], ...this.#prepareHostClientFunctionOptions() });
if (!transaction)
throw 'Error on acquire submit';
acquireSentMoment = await this.reputationClient.getMoment();
this.cfg.contractInstance = {
transaction: transaction,
acquire_sent_moment: acquireSentMoment,
owner_privatekey: ownerKeys.privateKey,
status: ContractStatus.AcquireSent
};
this.#persistConfig();
}
const result = await tenantClient.watchAcquireResponse(this.cfg.contractInstance.transaction);
createdMoment = await this.reputationClient.getMoment();
// Assign ip to domain and outbound_ip for instance created from old sashimono version.
if ('ip' in result.instance) {
result.instance.domain = result.instance.ip;
delete result.instance.ip;
}
console.log('Reputation contract created in instance', result.instance);
this.cfg.contractInstance = {
...result.instance,
created_moment: createdMoment,
owner_privatekey: this.cfg.contractInstance.owner_privatekey,
status: ContractStatus.Created
};
this.#persistConfig();
}
await tenantClient.disconnect();
}
else {
console.log(`Skipping acquire since there is already created instance for the moment ${curMoment + 1}.`);
}
if (curMoment === createdMoment) {
if (this.cfg.contractInstance.status === ContractStatus.Created) {
// Set reputation contract info in domain.
console.log(`Updating host reputation domain info...`);
await this.hostClient.setReputationContractInfo(this.cfg.contractInstance.peer_port, this.cfg.contractInstance.pubkey, curMoment + 1);
console.log(`Updated host reputation domain info.`);
// Mark as updated.
this.cfg.contractInstance.status = ContractStatus.Updated;
this.#persistConfig();
}
if (this.cfg.contractInstance.status === ContractStatus.Updated) {
// Wait for some time to let others to prepare.
const momentSize = this.hostClient.config.momentSize;
const momentStartTimestamp = await this.hostClient.getMomentStartIndex();
const currentTimestamp = evernode.UtilHelpers.getCurrentUnixTime();
const timeQuota = momentSize * (1 - this.#preparationTimeQuota);
const upperBound = Math.floor(momentStartTimestamp + momentSize);
const lowerBound = Math.floor(momentStartTimestamp + momentSize - (timeQuota * (1 - this.#reputationRegTimeQuota)));
const startTimestamp = Math.floor(lowerBound + ((upperBound - lowerBound) * this.#lobbyTimeQuota));
let startTimeout = 0;
if (startTimestamp > currentTimestamp)
startTimeout = (startTimestamp - currentTimestamp) * 1000;
console.log(`Waiting ${startTimeout} milliseconds until other hosts are ready.`);
await new Promise((resolve) => setTimeout(resolve, startTimeout));
const instances = await this.#getInstancesInUniverse(universeInfo.universeIndex, curMoment + 1);
const unl = instances.map(p => `${p.pubkey}`);
const peers = instances.map(p => `${p.domain}:${p.peerPort}`);
console.log(`Upgrading the reputation contract instance.`);
await this.#upgradeContract(this.cfg.contractInstance.domain, this.cfg.contractInstance.user_port, this.cfg.contractInstance.owner_privatekey, unl, peers);
console.log(`Reputation contract instance upgraded.`);
// Mark as deployed.
this.cfg.contractInstance.status = ContractStatus.Deployed;
this.#persistConfig();
}
}
else {
console.log(`Skipping deploy since instance is not created in the moment ${curMoment}.`)
}
});
}
async #getScores() {
if (!this.cfg.contractInstance?.domain || !this.cfg.contractInstance?.user_port)
return null;
let instanceMgr;
try {
instanceMgr = new ContractInstanceManager({
ip: this.cfg.contractInstance.domain,
userPort: this.cfg.contractInstance.user_port,
userPrivateKey: this.cfg.contractInstance.owner_privatekey
});
await instanceMgr.init();
const res = await instanceMgr.sendContractReadRequest({ command: this.#readScoreCmd }, INPUT_PROTOCOLS.json);
return res;
} catch (e) {
console.error('Error occurred while reading the scores:', e);
return null;
}
finally {
if (instanceMgr)
await instanceMgr.terminate();
}
}
// Reputation sender.
async #sendReputations() {
const scheduledMoment = await this.hostClient.getMoment();
await this.#queueAction(async (submissionRefs) => {
// Skip if host is not registered.
const hostInfo = await this.hostClient.getRegistration();
if (!hostInfo.active) {
console.log(`Skipping reputation sender since host is not active.`);
return;
}
const currentMoment = await this.hostClient.getMoment();
if (scheduledMoment == currentMoment) {
// Sending reputations every moment.
if (this.lastReputationMoment === 0 || currentMoment !== this.lastReputationMoment) {
submissionRefs.refs ??= [{}];
// Check again wether the transaction is validated before retry.
const txHash = submissionRefs?.refs[0]?.submissionResult?.result?.tx_json?.hash;
if (txHash) {
const txResponse = await this.hostClient.xrplApi.getTransactionValidatedResults(txHash);
if (txResponse && txResponse.code === "tesSUCCESS") {
console.log('Transaction is validated and success, Retry skipped!')
return;
}
}
let scores = null;
const createdMoment = this.cfg.contractInstance?.created_moment ?? -2;
if (currentMoment === (createdMoment + 1))
scores = await this.#getScores();
console.log(`Reporting reputations at Moment ${currentMoment} ${scores ? 'With scores' : 'Without scores'}...`);
try {
await this.hostClient.sendReputations(scores, { submissionRef: submissionRefs?.refs[0], ...this.#prepareHostClientFunctionOptions() });
this.lastReputationMoment = await this.hostClient.getMoment();
}
catch (err) {
if (err.code === 'tecHOOK_REJECTED') {
console.log("Reputation rejected by the hook.");
}
else {
console.log("Reputation tx error", err);
throw err;
}
}
}
}
else {
console.log(`Skipping reputation sender since scheduled moment has passed. Scheduled in ${scheduledMoment}, Current moment ${curMoment}.`);
}
}, this.#reputationRetryCount, this.#reputationRetryDelay);
}
#readConfig() {
this.cfg = ConfigHelper.readConfig(this.#configPath, this.#mbXrplConfigPath, true);
}
#persistConfig() {
ConfigHelper.writeConfig(this.cfg, this.#configPath);
}
}
module.exports = {
ReputationD
}

View File

@@ -1,195 +0,0 @@
const { appenv } = require('./appenv');
const evernode = require('evernode-js-client');
const { ConfigHelper } = require('./config-helper');
async function setEvernodeDefaults(network, governorAddress, rippledServer, fallbackRippledServers) {
await evernode.Defaults.useNetwork(network || appenv.NETWORK);
if (governorAddress)
evernode.Defaults.set({
governorAddress: governorAddress
});
if (rippledServer)
evernode.Defaults.set({
rippledServer: rippledServer
});
if (fallbackRippledServers && fallbackRippledServers.length)
evernode.Defaults.set({
fallbackRippledServers: fallbackRippledServers
});
}
const MAX_TX_RETRY_ATTEMPTS = 10;
class Setup {
#getConfig(readSecret = true, includeMbConfig = true) {
return ConfigHelper.readConfig(appenv.CONFIG_PATH, includeMbConfig ? appenv.MB_XRPL_CONFIG_PATH : null, readSecret);
}
#saveConfig(cfg) {
ConfigHelper.writeConfig(cfg, appenv.CONFIG_PATH);
}
newConfig(address = "", secretPath = "") {
const baseConfig = {
version: appenv.REPUTATIOND_VERSION,
xrpl: {
address: address,
secretPath: secretPath
},
contractInstance: {}
};
this.#saveConfig(baseConfig);
}
async prepareReputationAccount() {
const config = this.#getConfig();
const acc = config.xrpl;
await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer, acc.fallbackRippledServers);
// Prepare host account.
const hostClient = new evernode.HostClient(acc.hostAddress, acc.hostSecret);
await hostClient.connect();
// Update the Defaults with "xrplApi" of the client.
evernode.Defaults.set({
xrplApi: hostClient.xrplApi
});
try {
console.log(`Preparing reputation account:${acc.address} | Reputation Hook:${hostClient.config.reputationAddress}`);
await hostClient.prepareReputationAccount(acc.address, acc.secret, { retryOptions: { maxRetryAttempts: MAX_TX_RETRY_ATTEMPTS, feeUplift: Math.floor(acc.affordableExtraFee / MAX_TX_RETRY_ATTEMPTS) } });
await hostClient.disconnect();
}
catch (e) {
await hostClient.disconnect();
throw e;
}
}
async waitForFunds(currencyType, expectedBalance, waitPeriod = 120) {
const config = this.#getConfig(false);
const acc = config.xrpl;
await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer, acc.fallbackRippledServers);
// Prepare host account.
const hostClient = new evernode.HostClient(acc.hostAddress);
// Update the Defaults with "xrplApi" of the client.
evernode.Defaults.set({
xrplApi: hostClient.xrplApi
});
try {
let attempts = 0;
let balance = 0;
while (attempts >= 0) {
try {
// In order to handle the account not found issue via catch block.
await hostClient.connect();
// Prepare reputation account.
const reputationAcc = new evernode.XrplAccount(acc.address);
await new Promise(resolve => setTimeout(resolve, 1000));
if (currencyType === 'NATIVE')
balance = Number((await reputationAcc.getInfo()).Balance) / 1000000;
else {
const lines = await reputationAcc.getTrustLines(evernode.EvernodeConstants.EVR, hostClient.config.evrIssuerAddress);
balance = lines.length > 0 ? Number(lines[0].balance) : 0;
}
if (balance < expectedBalance) {
if (++attempts <= waitPeriod)
continue;
await hostClient.disconnect();
throw "NOT_ENOUGH_FUNDS";
}
break;
} catch (err) {
if (err.data?.error === 'actNotFound' && ++attempts <= waitPeriod) {
await new Promise(resolve => setTimeout(resolve, 1000));
continue;
}
await hostClient.disconnect();
throw (err.data?.error === 'actNotFound' || err === 'NOT_ENOUGH_FUNDS') ? "Funds not received within timeout." : "Error occurred in account balance check.";
}
}
console.log(`${balance} ${currencyType == 'NATIVE' ? 'XAH' : 'EVR'} balance is there in your host account.`);
await hostClient.disconnect();
}
catch (e) {
await hostClient.disconnect();
throw e;
}
}
// Upgrades existing message board data to the new version.
async upgrade() {
// Do a simple version change in the config.
const cfg = this.#getConfig(false, false);
cfg.version = appenv.REPUTATIOND_VERSION;
this.#saveConfig(cfg);
await Promise.resolve(); // async placeholder.
}
async repInfo() {
const acc = this.#getConfig(false).xrpl;
await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer, acc.fallbackRippledServers);
const hostClient = new evernode.HostClient(acc.hostAddress);
await hostClient.connect();
// Update the Defaults with "xrplApi" of the client.
evernode.Defaults.set({
xrplApi: hostClient.xrplApi
});
try {
const repInfo = await hostClient.getReputationInfo();
const config = hostClient.config;
await hostClient.disconnect();
const moment = await hostClient.getMoment();
if (!repInfo) {
console.log('You don\'t have reputation info yet.\n Make sure you have opted-in. If opted-in, wait until your host reporting for first reputation.');
return;
}
else if (!repInfo.moment) {
repInfo.moment = moment;
}
const repClient = await evernode.HookClientFactory.create(evernode.HookTypes.reputation, { config: config });
await repClient.connect({ skipConfigs: true });
const globalInfo = await repClient.getReputationInfo();
await repClient.disconnect();
console.log(JSON.stringify({ ...repInfo, reportedHostCount: globalInfo?.count ?? 0 }, null, 2));
}
catch (e) {
await hostClient.disconnect();
throw e;
}
finally {
await evernode.Defaults.values.xrplApi.disconnect();
}
}
}
module.exports = {
Setup
}

View File

@@ -1,15 +0,0 @@
const HotPocket = require('hotpocket-js-client');
class CommonHelper {
static async generateKeys(privateKey = null, format = 'hex') {
const keys = await HotPocket.generateKeys(privateKey);
return format === 'hex' ? {
privateKey: Buffer.from(keys.privateKey).toString('hex'),
publicKey: Buffer.from(keys.publicKey).toString('hex')
} : keys;
}
}
module.exports = {
CommonHelper
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +0,0 @@
{
"name": "reputationd",
"scripts": {
"lint": "./node_modules/.bin/eslint ./app.js",
"build": "npm run lint && ncc build app.js --minify -o dist"
},
"dependencies": {
"archiver": "5.3.1",
"evernode-js-client": "0.6.52",
"hotpocket-js-client": "0.5.6",
"uuid": "9.0.1",
"ws": "8.17.0"
},
"devDependencies": {
"eslint": "8.3.0"
}
}