From c7cf33580a4422f84dec387fed3a9228ffe37350 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Fri, 27 Oct 2023 11:13:31 +0530 Subject: [PATCH 01/26] Temporary setup helpers moved to sashimono bin directory. (#290) --- installer/sashimono-install.sh | 4 ++++ installer/setup.sh | 31 ++++++++++++++++++------------- mb-xrpl/lib/appenv.js | 2 +- src/version.hpp | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/installer/sashimono-install.sh b/installer/sashimono-install.sh index 12edd24..3906741 100755 --- a/installer/sashimono-install.sh +++ b/installer/sashimono-install.sh @@ -27,6 +27,7 @@ ipv6_net_interface=${20} script_dir=$(dirname "$(realpath "$0")") desired_slirp4netns_version="1.2.1" +setup_helper_dir="/tmp/evernode-setup-helpers" function stage() { echo "STAGE $1" # This is picked up by the setup console output filter. @@ -237,6 +238,9 @@ rm -r "$tmp" cp "$script_dir"/{sagent,hpfs,user-cgcreate.sh,user-install.sh,user-uninstall.sh,docker-registry-uninstall.sh} $SASHIMONO_BIN chmod -R +x $SASHIMONO_BIN +# Copy the temporary setup-helper directory content to SASHIMONO_BIN directory. +cp -Rdp $setup_helper_dir $SASHIMONO_BIN/evernode-setup-helpers + # Copy Blake3 and update linker library cache. [ ! -f /usr/local/lib/libblake3.so ] && cp "$script_dir"/libblake3.so /usr/local/lib/ && ldconfig diff --git a/installer/setup.sh b/installer/setup.sh index bf9dcd2..5e5aa33 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -29,8 +29,8 @@ installer_version_timestamp_file="installer.version.timestamp" setup_version_timestamp_file="setup.version.timestamp" default_rippled_server="wss://hooks-testnet-v3.xrpl-labs.com" setup_helper_dir="/tmp/evernode-setup-helpers" -nodejs_temp_bin="$setup_helper_dir/node" -jshelper_temp_bin="$setup_helper_dir/jshelper/index.js" +nodejs_util_bin="$setup_helper_dir/node" +jshelper_bin="$setup_helper_dir/jshelper/index.js" # export vars used by Sashimono installer. export USER_BIN=/usr/bin @@ -146,6 +146,13 @@ if [ "$mode" == "install" ] || [ "$mode" == "uninstall" ] || [ "$mode" == "updat (! $transfer || $installed) && [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 fi +# Change the relevant setup helper path based on Evernode installation condition and the command mode. +if $installed && [ "$mode" != "update" ] ; then + setup_helper_dir="$SASHIMONO_BIN/evernode-setup-helpers" + nodejs_util_bin="$setup_helper_dir/node" + jshelper_bin="$setup_helper_dir/jshelper/index.js" +fi + # Format the given KB number into GB units. function GB() { echo "$(bc <<<"scale=2; $1 / 1000000") GB" @@ -214,22 +221,22 @@ function init_setup_helpers() { echo "Downloading setup support files..." - local jshelper_dir=$(dirname $jshelper_temp_bin) + local jshelper_dir=$(dirname $jshelper_bin) rm -r $jshelper_dir >/dev/null 2>&1 sudo -u $noroot_user mkdir -p $jshelper_dir - [ ! -f "$nodejs_temp_bin" ] && sudo -u $noroot_user curl $nodejs_url --output $nodejs_temp_bin - [ ! -f "$nodejs_temp_bin" ] && echo "Could not download nodejs for setup checks." && exit 1 - chmod +x $nodejs_temp_bin + [ ! -f "$nodejs_util_bin" ] && sudo -u $noroot_user curl $nodejs_url --output $nodejs_util_bin + [ ! -f "$nodejs_util_bin" ] && echo "Could not download nodejs for setup checks." && exit 1 + chmod +x $nodejs_util_bin - if [ ! -f "$jshelper_temp_bin" ]; then + if [ ! -f "$jshelper_bin" ]; then pushd $jshelper_dir >/dev/null 2>&1 sudo -u $noroot_user curl $jshelper_url --output jshelper.tar.gz sudo -u $noroot_user tar zxf jshelper.tar.gz --strip-components=1 rm jshelper.tar.gz popd >/dev/null 2>&1 fi - [ ! -f "$jshelper_temp_bin" ] && echo "Could not download helper tool for setup checks." && exit 1 + [ ! -f "$jshelper_bin" ] && echo "Could not download helper tool for setup checks." && exit 1 echo -e "Done.\n" } @@ -240,7 +247,7 @@ function exec_jshelper() { [ -p $resp_file ] || sudo -u $noroot_user mkfifo $resp_file # Execute js helper asynchronously while collecting response to fifo file. - sudo -u $noroot_user RESPFILE=$resp_file $nodejs_temp_bin $jshelper_temp_bin "$@" >/dev/null 2>&1 & + sudo -u $noroot_user RESPFILE=$resp_file $nodejs_util_bin $jshelper_bin "$@" >/dev/null 2>&1 & local pid=$! local result=$(cat $resp_file) && [ "$result" != "-" ] && echo $result @@ -826,6 +833,8 @@ function update_evernode() { echo $latest_setup_script_version > $SASHIMONO_DATA/$setup_version_timestamp_file fi + rm -r $setup_helper_dir >/dev/null 2>&1 + echo "Upgrade complete." } @@ -1082,8 +1091,6 @@ function config() { local server=${2} # Rippled server URL [ -z $server ] && echomult "Your current rippled server is: $cfg_rippled_server\n" && exit 0 - init_setup_helpers - ! validate_rippled_url $server && echomult "\nUsage: evernode config rippled | evernode config rippled \n" && exit 1 @@ -1170,8 +1177,6 @@ function config() { echomult "Could not proceed the reconfiguration as there are occupied instances." && exit 1 fi - init_setup_helpers - set_ipv6_subnet if [[ "$ipv6_subnet" == "-" || "$ipv6_net_interface" == "-" ]]; then echo -e "Could not proceed with provided details." && exit 1 diff --git a/mb-xrpl/lib/appenv.js b/mb-xrpl/lib/appenv.js index 4496093..81bfbc9 100644 --- a/mb-xrpl/lib/appenv.js +++ b/mb-xrpl/lib/appenv.js @@ -28,7 +28,7 @@ appenv = { ORPHAN_PRUNE_SCHEDULER_INTERVAL_HOURS: 4, SASHIMONO_SCHEDULER_INTERVAL_SECONDS: 2, SASHI_CLI_PATH: appenv.IS_DEV_MODE ? "../build/sashi" : "/usr/bin/sashi", - MB_VERSION: '0.7.1', + MB_VERSION: '0.7.2', TOS_HASH: '757A0237B44D8B2BBB04AE2BAD5813858E0AECD2F0B217075E27E0630BA74314' // This is the sha256 hash of TOS text. } Object.freeze(appenv); diff --git a/src/version.hpp b/src/version.hpp index 192347d..45f7209 100644 --- a/src/version.hpp +++ b/src/version.hpp @@ -6,7 +6,7 @@ namespace version { // Sashimono agent version. Written to new configs. - constexpr const char *AGENT_VERSION = "0.7.1"; + constexpr const char *AGENT_VERSION = "0.7.2"; // Minimum compatible config version (this will be used to validate configs). constexpr const char *MIN_CONFIG_VERSION = "0.5.0"; From 9e28388843123ab5ebe489aa15e98e10b2a8aeac Mon Sep 17 00:00:00 2001 From: Chalith Desaman Date: Thu, 2 Nov 2023 18:28:10 +0530 Subject: [PATCH 02/26] Retry killing user processes on instance delete (#291) --- dependencies/user-uninstall.sh | 19 +++++++++---------- test/docker/Dockerfile.ubt.20.04 | 2 +- test/docker/Dockerfile.ubt.20.04-njs | 2 +- test/docker/build.sh | 4 ++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/dependencies/user-uninstall.sh b/dependencies/user-uninstall.sh index 40a7600..31c94d3 100755 --- a/dependencies/user-uninstall.sh +++ b/dependencies/user-uninstall.sh @@ -7,6 +7,7 @@ peer_port=$2 user_port=$3 instance_name=$4 prefix="sashi" +max_kill_attempts=5 # Check whether this is a valid sashimono username. [ ${#user} -lt 24 ] || [ ${#user} -gt 32 ] || [[ ! "$user" =~ ^$prefix[0-9]+$ ]] && echo "ARGS,UNINST_ERR" && exit 1 @@ -55,18 +56,16 @@ for mnt in "${mntarr[@]}"; do done # Force kill user processes. -procs=$(ps -U $user 2>/dev/null | wc -l) -if [ "$procs" != "0" ]; then - - # Wait for some time and check again. +i=0 +while true; do sleep 1 procs=$(ps -U $user 2>/dev/null | wc -l) - if [ "$procs" != "0" ]; then - echo "Force killing user processes." - pkill -SIGKILL -u "$user" - fi - -fi + [ "$procs" == "1" ] && echo "All user processes terminated." && break + [[ $i -ge $max_kill_attempts ]] && echo "Max force user process kill attempts $max_kill_attempts reached. Abondaning." && break + ((i++)) + echo "Force killing user processes. Retrying $i..." + pkill -SIGKILL -u "$user" +done echo "Removing cgroups" # Delete config values. diff --git a/test/docker/Dockerfile.ubt.20.04 b/test/docker/Dockerfile.ubt.20.04 index 9c6f875..32deac1 100644 --- a/test/docker/Dockerfile.ubt.20.04 +++ b/test/docker/Dockerfile.ubt.20.04 @@ -1,4 +1,4 @@ -FROM evernodedev/hotpocket:0.6.3-ubt.20.04 +FROM evernodedev/hotpocket:0.6.4-ubt.20.04 RUN apt-get update \ && apt-get install --no-install-recommends -y unzip jq \ diff --git a/test/docker/Dockerfile.ubt.20.04-njs b/test/docker/Dockerfile.ubt.20.04-njs index 8ec3adf..d45e7d7 100644 --- a/test/docker/Dockerfile.ubt.20.04-njs +++ b/test/docker/Dockerfile.ubt.20.04-njs @@ -1,4 +1,4 @@ -FROM evernodedev/hotpocket:0.6.3-ubt.20.04-njs.20 +FROM evernodedev/hotpocket:0.6.4-ubt.20.04-njs.20 RUN apt-get update \ && apt-get install --no-install-recommends -y unzip jq \ diff --git a/test/docker/build.sh b/test/docker/build.sh index fe58359..fa3afaf 100755 --- a/test/docker/build.sh +++ b/test/docker/build.sh @@ -2,5 +2,5 @@ img=evernodedev/sashimono -docker build -t $img:hp.latest-ubt.20.04 -t $img:hp.0.6.3-ubt.20.04 -f ./Dockerfile.ubt.20.04 . -docker build -t $img:hp.latest-ubt.20.04-njs.20 -t $img:hp.0.6.3-ubt.20.04-njs.20 -f ./Dockerfile.ubt.20.04-njs . +docker build -t $img:hp.latest-ubt.20.04 -t $img:hp.0.6.4-ubt.20.04 -f ./Dockerfile.ubt.20.04 . +docker build -t $img:hp.latest-ubt.20.04-njs.20 -t $img:hp.0.6.4-ubt.20.04-njs.20 -f ./Dockerfile.ubt.20.04-njs . From 53409ac57157b305fac976d6f9a9ffb3a7d41ddc Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Wed, 8 Nov 2023 11:59:31 +0530 Subject: [PATCH 03/26] Heartbeat mechanism adjustment (#293) --- mb-xrpl/lib/message-board.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/mb-xrpl/lib/message-board.js b/mb-xrpl/lib/message-board.js index cb29b1f..8c9ce02 100644 --- a/mb-xrpl/lib/message-board.js +++ b/mb-xrpl/lib/message-board.js @@ -448,9 +448,6 @@ class MessageBoard { } async #startHeartBeatScheduler() { - // Sending a heartbeat at startup - await this.#sendHeartbeat(); - const momentSize = this.hostClient.config.momentSize; const halfMomentSize = momentSize / 2; // Getting half of moment size const timeout = momentSize * 1000; // Converting seconds to milliseconds. @@ -462,10 +459,24 @@ class MessageBoard { await this.#sendHeartbeat(); }; + const currentTimestamp = evernode.UtilHelpers.getCurrentUnixTime(); const currentMomentStartIdx = await this.hostClient.getMomentStartIndex(); + const currentMoment = await this.hostClient.getMoment(); + const currentMomentDuration = currentTimestamp - currentMomentStartIdx; + const hostInfo = await this.hostClient.getRegistration(); - // If the start index is in the begining of the moment, delay the heartbeat scheduler 1 minute to make sure the hook timestamp is not in previous moment when accepting the heartbeat. - const startTimeout = (evernode.UtilHelpers.getCurrentUnixTime() - currentMomentStartIdx) < halfMomentSize ? ((momentSize + 60) * 1000) : ((momentSize) * 1000); + // Schedule the next heartbeat based on last heartbeat occurrence. + // NOTE : Initially checks whether host has sent a heartbeat in the current moment or not. + // If schedule the next heartbeat based on its last heartbeat. + // If it is not further checks whether it is about to send the heartbeat at the second half of a moment or not. + // If the current timestamp lies in the second half of the moment, schedule the next heartbeat withing the next moment (in the its first half). + // If it is not schedule it right now. + const schedule = (this.lastHeartbeatMoment === currentMoment) + ? momentSize - (currentTimestamp - hostInfo.lastHeartbeatIndex) + : (currentMomentDuration > halfMomentSize && currentMomentDuration < momentSize) ? halfMomentSize : 0; + + // If the start index is in the beginning of the moment, delay the heartbeat scheduler 1 minute to make sure the hook timestamp is not in previous moment when accepting the heartbeat. + const startTimeout = (currentMomentDuration) < halfMomentSize ? ((schedule + 60) * 1000) : ((schedule) * 1000); setTimeout(async () => { await scheduler(); From 5b9d2da01648e60658fc14361c0eb29118248e70 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Wed, 8 Nov 2023 12:00:12 +0530 Subject: [PATCH 04/26] Modification related to HotPocket changes. (#292) --- dependencies/hp.cfg | 7 +++++-- evernode-bootstrap-contract | 2 +- src/hp_manager.cpp | 3 +++ src/msg/json/msg_json.cpp | 7 +++++++ src/msg/msg_common.hpp | 7 +++++++ test/docker/Dockerfile.ubt.20.04 | 2 +- test/docker/Dockerfile.ubt.20.04-njs | 2 +- test/docker/build.sh | 4 ++-- 8 files changed, 27 insertions(+), 7 deletions(-) diff --git a/dependencies/hp.cfg b/dependencies/hp.cfg index a711a7f..5d7616a 100644 --- a/dependencies/hp.cfg +++ b/dependencies/hp.cfg @@ -1,5 +1,5 @@ { - "hp_version": "0.6.3", + "hp_version": "0.6.5", "node": { "public_key": "", "private_key": "", @@ -29,7 +29,10 @@ "mode": "public", "roundtime": 2000, "stage_slice": 25, - "threshold": 80 + "threshold": 80, + "fallback": { + "execute": false + } }, "npl": { "mode": "public" diff --git a/evernode-bootstrap-contract b/evernode-bootstrap-contract index e40c3ae..0b1ea69 160000 --- a/evernode-bootstrap-contract +++ b/evernode-bootstrap-contract @@ -1 +1 @@ -Subproject commit e40c3ae7053458ff3c3b72cc66359bf02b811dab +Subproject commit 0b1ea69d2f1b3965d1e1ad7bde1438385781d1de diff --git a/src/hp_manager.cpp b/src/hp_manager.cpp index 394bf2c..486b544 100644 --- a/src/hp_manager.cpp +++ b/src/hp_manager.cpp @@ -719,6 +719,9 @@ namespace hp if (config.contract.consensus.threshold.has_value()) d["contract"]["consensus"]["threshold"] = config.contract.consensus.threshold.value(); + if (config.contract.consensus.fallback.execute.has_value()) + d["contract"]["consensus"]["fallback"]["execute"] = config.contract.consensus.fallback.execute.value(); + if (config.contract.npl.mode.has_value()) d["contract"]["npl"]["mode"] = config.contract.npl.mode.value(); diff --git a/src/msg/json/msg_json.cpp b/src/msg/json/msg_json.cpp index 4b74e9f..ba3ef72 100644 --- a/src/msg/json/msg_json.cpp +++ b/src/msg/json/msg_json.cpp @@ -338,6 +338,13 @@ namespace msg::json if (consensus.contains(msg::FLD_THRESHOLD)) msg.config.contract.consensus.threshold = consensus[msg::FLD_THRESHOLD].as(); + + if (consensus.contains(msg::FLD_FALLBACK)) + { + const jsoncons::json &fallback = consensus[msg::FLD_FALLBACK]; + if (fallback.contains(msg::FLD_EXECUTE)) + msg.config.contract.consensus.fallback.execute = fallback[msg::FLD_EXECUTE].as(); + } } if (contract.contains(msg::FLD_NPL)) diff --git a/src/msg/msg_common.hpp b/src/msg/msg_common.hpp index e43539b..8df4c29 100644 --- a/src/msg/msg_common.hpp +++ b/src/msg/msg_common.hpp @@ -37,12 +37,18 @@ namespace msg std::optional max_file_count; }; + struct fallback_config + { + std::optional execute; + }; + struct consensus_config { std::optional mode; std::optional roundtime; std::optional stage_slice; std::optional threshold; + fallback_config fallback; }; struct npl_config @@ -188,6 +194,7 @@ namespace msg constexpr const char *FLD_ROUNDTIME = "roundtime"; constexpr const char *FLD_STAGE_SLICE = "stage_slice"; constexpr const char *FLD_THRESHOLD = "threshold"; + constexpr const char *FLD_FALLBACK = "fallback"; constexpr const char *FLD_ROUND_LIMITS = "round_limits"; constexpr const char *FLD_USER_INP_BYTES = "user_input_bytes"; constexpr const char *FLD_USER_OUTP_BYTES = "user_output_bytes"; diff --git a/test/docker/Dockerfile.ubt.20.04 b/test/docker/Dockerfile.ubt.20.04 index 32deac1..d64a639 100644 --- a/test/docker/Dockerfile.ubt.20.04 +++ b/test/docker/Dockerfile.ubt.20.04 @@ -1,4 +1,4 @@ -FROM evernodedev/hotpocket:0.6.4-ubt.20.04 +FROM evernodedev/hotpocket:0.6.5-ubt.20.04 RUN apt-get update \ && apt-get install --no-install-recommends -y unzip jq \ diff --git a/test/docker/Dockerfile.ubt.20.04-njs b/test/docker/Dockerfile.ubt.20.04-njs index d45e7d7..977ebf2 100644 --- a/test/docker/Dockerfile.ubt.20.04-njs +++ b/test/docker/Dockerfile.ubt.20.04-njs @@ -1,4 +1,4 @@ -FROM evernodedev/hotpocket:0.6.4-ubt.20.04-njs.20 +FROM evernodedev/hotpocket:0.6.5-ubt.20.04-njs.20 RUN apt-get update \ && apt-get install --no-install-recommends -y unzip jq \ diff --git a/test/docker/build.sh b/test/docker/build.sh index fa3afaf..15500d1 100755 --- a/test/docker/build.sh +++ b/test/docker/build.sh @@ -2,5 +2,5 @@ img=evernodedev/sashimono -docker build -t $img:hp.latest-ubt.20.04 -t $img:hp.0.6.4-ubt.20.04 -f ./Dockerfile.ubt.20.04 . -docker build -t $img:hp.latest-ubt.20.04-njs.20 -t $img:hp.0.6.4-ubt.20.04-njs.20 -f ./Dockerfile.ubt.20.04-njs . +docker build -t $img:hp.latest-ubt.20.04 -t $img:hp.0.6.5-ubt.20.04 -f ./Dockerfile.ubt.20.04 . +docker build -t $img:hp.latest-ubt.20.04-njs.20 -t $img:hp.0.6.5-ubt.20.04-njs.20 -f ./Dockerfile.ubt.20.04-njs . From e2c931682d16d80a3fac221fc629b03c5d8d8783 Mon Sep 17 00:00:00 2001 From: Dulana Peiris Date: Tue, 21 Nov 2023 10:47:11 +0530 Subject: [PATCH 05/26] Pointed bootstrap-contract submodule to point to release branch --- .gitmodules | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitmodules b/.gitmodules index ea19230..8b1a17a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "evernode-bootstrap-contract"] path = evernode-bootstrap-contract url = https://github.com/HotPocketDev/evernode-bootstrap-contract.git + branch = release From c1d1a986fd91490ec5ab428fb42fffc88b303cc4 Mon Sep 17 00:00:00 2001 From: Dulana Peiris Date: Tue, 21 Nov 2023 10:48:42 +0530 Subject: [PATCH 06/26] Revert "Modification related to HotPocket changes. (#292)" This reverts commit 5b9d2da01648e60658fc14361c0eb29118248e70. --- dependencies/hp.cfg | 7 ++----- evernode-bootstrap-contract | 2 +- src/hp_manager.cpp | 3 --- src/msg/json/msg_json.cpp | 7 ------- src/msg/msg_common.hpp | 7 ------- test/docker/Dockerfile.ubt.20.04 | 2 +- test/docker/Dockerfile.ubt.20.04-njs | 2 +- test/docker/build.sh | 4 ++-- 8 files changed, 7 insertions(+), 27 deletions(-) diff --git a/dependencies/hp.cfg b/dependencies/hp.cfg index 5d7616a..a711a7f 100644 --- a/dependencies/hp.cfg +++ b/dependencies/hp.cfg @@ -1,5 +1,5 @@ { - "hp_version": "0.6.5", + "hp_version": "0.6.3", "node": { "public_key": "", "private_key": "", @@ -29,10 +29,7 @@ "mode": "public", "roundtime": 2000, "stage_slice": 25, - "threshold": 80, - "fallback": { - "execute": false - } + "threshold": 80 }, "npl": { "mode": "public" diff --git a/evernode-bootstrap-contract b/evernode-bootstrap-contract index 0b1ea69..e40c3ae 160000 --- a/evernode-bootstrap-contract +++ b/evernode-bootstrap-contract @@ -1 +1 @@ -Subproject commit 0b1ea69d2f1b3965d1e1ad7bde1438385781d1de +Subproject commit e40c3ae7053458ff3c3b72cc66359bf02b811dab diff --git a/src/hp_manager.cpp b/src/hp_manager.cpp index 486b544..394bf2c 100644 --- a/src/hp_manager.cpp +++ b/src/hp_manager.cpp @@ -719,9 +719,6 @@ namespace hp if (config.contract.consensus.threshold.has_value()) d["contract"]["consensus"]["threshold"] = config.contract.consensus.threshold.value(); - if (config.contract.consensus.fallback.execute.has_value()) - d["contract"]["consensus"]["fallback"]["execute"] = config.contract.consensus.fallback.execute.value(); - if (config.contract.npl.mode.has_value()) d["contract"]["npl"]["mode"] = config.contract.npl.mode.value(); diff --git a/src/msg/json/msg_json.cpp b/src/msg/json/msg_json.cpp index ba3ef72..4b74e9f 100644 --- a/src/msg/json/msg_json.cpp +++ b/src/msg/json/msg_json.cpp @@ -338,13 +338,6 @@ namespace msg::json if (consensus.contains(msg::FLD_THRESHOLD)) msg.config.contract.consensus.threshold = consensus[msg::FLD_THRESHOLD].as(); - - if (consensus.contains(msg::FLD_FALLBACK)) - { - const jsoncons::json &fallback = consensus[msg::FLD_FALLBACK]; - if (fallback.contains(msg::FLD_EXECUTE)) - msg.config.contract.consensus.fallback.execute = fallback[msg::FLD_EXECUTE].as(); - } } if (contract.contains(msg::FLD_NPL)) diff --git a/src/msg/msg_common.hpp b/src/msg/msg_common.hpp index 8df4c29..e43539b 100644 --- a/src/msg/msg_common.hpp +++ b/src/msg/msg_common.hpp @@ -37,18 +37,12 @@ namespace msg std::optional max_file_count; }; - struct fallback_config - { - std::optional execute; - }; - struct consensus_config { std::optional mode; std::optional roundtime; std::optional stage_slice; std::optional threshold; - fallback_config fallback; }; struct npl_config @@ -194,7 +188,6 @@ namespace msg constexpr const char *FLD_ROUNDTIME = "roundtime"; constexpr const char *FLD_STAGE_SLICE = "stage_slice"; constexpr const char *FLD_THRESHOLD = "threshold"; - constexpr const char *FLD_FALLBACK = "fallback"; constexpr const char *FLD_ROUND_LIMITS = "round_limits"; constexpr const char *FLD_USER_INP_BYTES = "user_input_bytes"; constexpr const char *FLD_USER_OUTP_BYTES = "user_output_bytes"; diff --git a/test/docker/Dockerfile.ubt.20.04 b/test/docker/Dockerfile.ubt.20.04 index d64a639..32deac1 100644 --- a/test/docker/Dockerfile.ubt.20.04 +++ b/test/docker/Dockerfile.ubt.20.04 @@ -1,4 +1,4 @@ -FROM evernodedev/hotpocket:0.6.5-ubt.20.04 +FROM evernodedev/hotpocket:0.6.4-ubt.20.04 RUN apt-get update \ && apt-get install --no-install-recommends -y unzip jq \ diff --git a/test/docker/Dockerfile.ubt.20.04-njs b/test/docker/Dockerfile.ubt.20.04-njs index 977ebf2..d45e7d7 100644 --- a/test/docker/Dockerfile.ubt.20.04-njs +++ b/test/docker/Dockerfile.ubt.20.04-njs @@ -1,4 +1,4 @@ -FROM evernodedev/hotpocket:0.6.5-ubt.20.04-njs.20 +FROM evernodedev/hotpocket:0.6.4-ubt.20.04-njs.20 RUN apt-get update \ && apt-get install --no-install-recommends -y unzip jq \ diff --git a/test/docker/build.sh b/test/docker/build.sh index 15500d1..fa3afaf 100755 --- a/test/docker/build.sh +++ b/test/docker/build.sh @@ -2,5 +2,5 @@ img=evernodedev/sashimono -docker build -t $img:hp.latest-ubt.20.04 -t $img:hp.0.6.5-ubt.20.04 -f ./Dockerfile.ubt.20.04 . -docker build -t $img:hp.latest-ubt.20.04-njs.20 -t $img:hp.0.6.5-ubt.20.04-njs.20 -f ./Dockerfile.ubt.20.04-njs . +docker build -t $img:hp.latest-ubt.20.04 -t $img:hp.0.6.4-ubt.20.04 -f ./Dockerfile.ubt.20.04 . +docker build -t $img:hp.latest-ubt.20.04-njs.20 -t $img:hp.0.6.4-ubt.20.04-njs.20 -f ./Dockerfile.ubt.20.04-njs . From c38ce5d31ff9ad65bbd4e21de763411338604154 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Fri, 24 Nov 2023 14:22:58 +0530 Subject: [PATCH 07/26] Redesign installation process (#299) --- evernode-bootstrap-contract | 2 +- installer/jshelper/index.js | 301 +++++- installer/jshelper/package-lock.json | 258 ++--- installer/jshelper/package.json | 5 +- installer/prereq.sh | 10 +- installer/sashimono-install.sh | 115 +- installer/sashimono-uninstall.sh | 44 +- installer/setup-old.sh | 1447 ++++++++++++++++++++++++++ installer/setup.sh | 923 +++++++++++----- mb-xrpl/app.js | 17 +- mb-xrpl/lib/appenv.js | 15 +- mb-xrpl/lib/config-helper.js | 11 +- mb-xrpl/lib/governance-manager.js | 20 +- mb-xrpl/lib/message-board.js | 17 +- mb-xrpl/lib/setup.js | 125 +-- mb-xrpl/package-lock.json | 14 +- mb-xrpl/package.json | 2 +- src/version.hpp | 2 +- 18 files changed, 2657 insertions(+), 671 deletions(-) create mode 100644 installer/setup-old.sh diff --git a/evernode-bootstrap-contract b/evernode-bootstrap-contract index e40c3ae..1e0e816 160000 --- a/evernode-bootstrap-contract +++ b/evernode-bootstrap-contract @@ -1 +1 @@ -Subproject commit e40c3ae7053458ff3c3b72cc66359bf02b811dab +Subproject commit 1e0e816abbb12f317f0a447c059e0081b78c266a diff --git a/installer/jshelper/index.js b/installer/jshelper/index.js index a4a477a..897764f 100644 --- a/installer/jshelper/index.js +++ b/installer/jshelper/index.js @@ -4,6 +4,10 @@ const evernode = require("evernode-js-client"); const process = require("process"); const fs = require("fs"); const ip6addr = require('ip6addr'); +const keypairs = require('ripple-keypairs'); +const http = require('http'); +const crypto = require('crypto'); +const { appenv } = require("../../mb-xrpl/lib/appenv"); function checkParams(args, count) { for (let i = 0; i < count; i++) { @@ -18,11 +22,16 @@ const funcs = { 'validate-server': async (args) => { checkParams(args, 1); const rippledUrl = args[0]; - const xrplApi = new evernode.XrplApi(rippledUrl, { autoReconnect: false }); + await evernode.Defaults.useNetwork(appenv.NETWORK); + evernode.Defaults.set({ + rippledServer: rippledUrl + }); + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); await xrplApi.connect(); await xrplApi.disconnect(); return { success: true }; }, + 'validate-account': async (args) => { checkParams(args, 3); const rippledUrl = args[0]; @@ -30,15 +39,22 @@ const funcs = { const accountAddress = args[2]; const validateFor = args[3] || "register"; - const xrplApi = new evernode.XrplApi(rippledUrl, { autoReconnect: false }); + await evernode.Defaults.useNetwork(appenv.NETWORK); + + evernode.Defaults.set({ + rippledServer: rippledUrl, + governorAddress: governorAddress + }); + + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); await xrplApi.connect(); - const hostClient = new evernode.HostClient(accountAddress, null, { - rippledServer: rippledUrl, - governorAddress: governorAddress, + evernode.Defaults.set({ xrplApi: xrplApi }); + const hostClient = new evernode.HostClient(accountAddress, null); + if (!await hostClient.xrplAcc.exists()) return { success: false, result: "Account not found." }; @@ -71,15 +87,26 @@ const funcs = { await xrplApi.disconnect(); return { success: true }; }, + 'validate-keys': async (args) => { checkParams(args, 3); const rippledUrl = args[0]; const accountAddress = args[1]; const accountSecret = args[2]; - const xrplApi = new evernode.XrplApi(rippledUrl, { autoReconnect: false }); + await evernode.Defaults.useNetwork(appenv.NETWORK); + + evernode.Defaults.set({ + rippledServer: rippledUrl + }); + + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); await xrplApi.connect(); + evernode.Defaults.set({ + xrplApi: xrplApi + }); + const xrplAcc = new evernode.XrplAccount(accountAddress, accountSecret, { xrplApi: xrplApi }); @@ -91,31 +118,33 @@ const funcs = { }, 'access-evernode-cfg': async (args) => { - checkParams(args, 4); + checkParams(args, 3); const rippledUrl = args[0]; const governorAddress = args[1]; - const accountAddress = args[2]; - const configName = args[3]; + const configName = args[2]; - const xrplApi = new evernode.XrplApi(rippledUrl, { autoReconnect: false }); + await evernode.Defaults.useNetwork(appenv.NETWORK); + + evernode.Defaults.set({ + rippledServer: rippledUrl, + governorAddress: governorAddress + }); + + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); await xrplApi.connect(); - const hostClient = new evernode.HostClient(accountAddress, null, { - rippledServer: rippledUrl, - governorAddress: governorAddress, + evernode.Defaults.set({ xrplApi: xrplApi }); - if (!await hostClient.xrplAcc.exists()) - return { success: false, result: "Account not found." }; + const governorClient = await evernode.HookClientFactory.create(evernode.HookTypes.governor); + await governorClient.connect(); + const config = await governorClient.config; - await hostClient.connect(); - const config = hostClient.config; - - await hostClient.disconnect(); + await governorClient.disconnect(); await xrplApi.disconnect(); - return { success: true, result: config[configName] }; + return { success: true, result: typeof config[configName] === 'object' ? JSON.stringify(config[configName]) : `${config[configName]}` }; }, 'transfer': async (args) => { @@ -126,15 +155,22 @@ const funcs = { const accountSecret = args[3]; const transfereeAddress = args[4]; - const xrplApi = new evernode.XrplApi(rippledUrl, { autoReconnect: false }); + await evernode.Defaults.useNetwork(appenv.NETWORK); + + evernode.Defaults.set({ + rippledServer: rippledUrl, + governorAddress: governorAddress + }); + + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); await xrplApi.connect(); - const hostClient = new evernode.HostClient(accountAddress, accountSecret, { - rippledServer: rippledUrl, - governorAddress: governorAddress, + evernode.Defaults.set({ xrplApi: xrplApi }); + const hostClient = new evernode.HostClient(accountAddress, accountSecret); + if (!await hostClient.xrplAcc.exists()) return { success: false, result: "Account not found." }; @@ -203,6 +239,223 @@ const funcs = { } return { success: false }; + }, + + 'check-balance': async (args) => { + checkParams(args, 5); + const rippledUrl = args[0]; + const governorAddress = args[1]; + const accountAddress = args[2]; + const tokenType = args[3]; + const expectedBalance = args[4]; + + const WAIT_PERIOD = 120; // seconds + + await evernode.Defaults.useNetwork(appenv.NETWORK); + + evernode.Defaults.set({ + rippledServer: rippledUrl, + governorAddress: governorAddress + }); + + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); + await xrplApi.connect(); + + evernode.Defaults.set({ + xrplApi: xrplApi + }); + + const hostClient = new evernode.HostClient(accountAddress, null); + const terminateConnections = async () => { + await hostClient.disconnect(); + await xrplApi.disconnect(); + } + + 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(); + + await new Promise(resolve => setTimeout(resolve, 1000)); + if (tokenType === 'NATIVE') + balance = Number((await hostClient.xrplAcc.getInfo()).Balance) / 1000000; + else + balance = Number(await hostClient.getEVRBalance()); + + if (balance < expectedBalance) { + if (++attempts <= WAIT_PERIOD) + continue; + + await terminateConnections(); + return { success: false, result: "Funds not received within timeout." }; + } + + break; + } catch (err) { + if (err.data?.error === 'actNotFound' && ++attempts <= WAIT_PERIOD) { + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + await terminateConnections(); + return { success: false, result: (err.data?.error === 'actNotFound') ? "Funds not received within timeout." : "Error occurred in account balance check." }; + } + } + + await terminateConnections(); + return { success: true, result: `${balance}` }; + }, + + 'generate-account': async (args) => { + let seed = null; + if (args[0]) + seed = args[0]; + else + seed = keypairs.generateSeed({ algorithm: "ecdsa-secp256k1" }); + + const keypair = keypairs.deriveKeypair(seed); + const createdKeypair = { + address: keypairs.deriveAddress(keypair.publicKey), + secret: seed + } + return { success: true, result: typeof createdKeypair === 'object' ? JSON.stringify(createdKeypair) : `${createdKeypair}` }; + }, + + 'prepare-host': async (args) => { + checkParams(args, 4); + const rippledUrl = args[0]; + const governorAddress = args[1]; + const accountAddress = args[2]; + const accountSecret = args[3]; + // Optional + const domain = args[4] ? args[4] : ""; + + const WAIT_PERIOD = 120; // seconds + + await evernode.Defaults.useNetwork(appenv.NETWORK); + + evernode.Defaults.set({ + rippledServer: rippledUrl, + governorAddress: governorAddress + }); + + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); + await xrplApi.connect(); + + evernode.Defaults.set({ + xrplApi: xrplApi + }); + + const hostClient = new evernode.HostClient(accountAddress, accountSecret); + await hostClient.connect(); + + const terminateConnections = async () => { + await hostClient.disconnect(); + await xrplApi.disconnect(); + } + + { + let attempts = 0; + while (attempts >= 0) { + try { + await hostClient.prepareAccount(domain); + break; + } + catch (err) { + if (err.data?.error === 'actNotFound' && ++attempts <= WAIT_PERIOD) { + // Wait and retry. + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + + await terminateConnections(); + return { success: false, result: "Error occurred in account preparation." }; + } + } + } + + await terminateConnections(); + return { success: true }; + + }, + + // Starts an HTTP server on port 80 and check whether that's reachable via + // the provided domain. + 'validate-domain': async (args) => { + checkParams(args, 2); + const domain = args[0]; + const port = parseInt(args[1]); + const urlPath = "/" + crypto.randomBytes(16).toString('hex'); + const responseString = crypto.randomBytes(16).toString('hex'); + + const server = http.createServer((req, res) => { + if (req.url === urlPath) { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end(responseString + '\n'); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found\n'); + } + }); + + try { + await new Promise((resolve, reject) => { + server.on('error', function (e) { + // We assume this is an error when starting to listen. + reject("listen_error"); + }); + + server.listen(port, () => { + // Server started. Now send a request via public domain. + + const reqOptions = { + hostname: domain, + port: port, + path: urlPath, + method: "GET" + }; + + const req = http.request(reqOptions, (res) => { + let data = ""; + + res.on("data", (chunk) => { + data += chunk; + }); + + // request completion event. + res.on("end", () => { + server.close(); + if (data.startsWith(responseString)) { + resolve(); + } else { + // Return string does not match our responseString. Most probably response was + // sent by some other server. Not by us. + reject("domain_error") + } + }); + }); + + req.on("error", (e) => { + server.close(); + reject("domain_error") + }); + + req.setTimeout(3000, () => { // 3 second request timeout + req.destroy(); + server.close(); + reject("domain_error"); + }); + + req.end(); + }); + }); + + return { success: true, result: "ok" }; + + } catch (errorCode) { + return { success: false, result: errorCode }; + } } } diff --git a/installer/jshelper/package-lock.json b/installer/jshelper/package-lock.json index ba831f4..95e80b7 100644 --- a/installer/jshelper/package-lock.json +++ b/installer/jshelper/package-lock.json @@ -6,8 +6,9 @@ "": { "name": "evernode-setup-helper", "dependencies": { - "evernode-js-client": "0.6.20", - "ip6addr": "0.2.5" + "evernode-js-client": "0.6.21", + "ip6addr": "0.2.5", + "ripple-keypairs": "1.3.1" } }, "node_modules/@noble/hashes": { @@ -363,9 +364,9 @@ } }, "node_modules/evernode-js-client": { - "version": "0.6.20", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.20.tgz", - "integrity": "sha512-OC6VNAhwqnNvUc0NhffxwNI9bTDH+BkD/KBTC5Xuwoiq8BhRfYhmfHBnD6M9K5AvLqv+Jxdufc3l1AlzHgILWg==", + "version": "0.6.21", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.21.tgz", + "integrity": "sha512-Q5P6caMTzx3xaUNKhnP1vJTw3wTP/d2J2xSQEMn4m1+t/t67d8+eii3/FeQapRBSZEbNRHm9EbRry9PJhb9xcg==", "dependencies": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", @@ -376,6 +377,27 @@ "xrpl-binary-codec": "1.4.2" } }, + "node_modules/evernode-js-client/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/evernode-js-client/node_modules/ripple-keypairs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.1.0.tgz", + "integrity": "sha512-Zlmbtn2YUpW4uKlLm2/tpkY5RC/EXQlkJwIIKp0AoF9D23pJ43/EuipNW2F6qURdbkUezDwB0bMV7uRXip3x2w==", + "dependencies": { + "bn.js": "^5.1.1", + "brorand": "^1.0.5", + "elliptic": "^6.5.4", + "hash.js": "^1.0.3", + "ripple-address-codec": "^4.2.0" + }, + "engines": { + "node": ">= 10", + "npm": ">=7.0.0" + } + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -818,19 +840,18 @@ } }, "node_modules/ripple-keypairs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.1.0.tgz", - "integrity": "sha512-Zlmbtn2YUpW4uKlLm2/tpkY5RC/EXQlkJwIIKp0AoF9D23pJ43/EuipNW2F6qURdbkUezDwB0bMV7uRXip3x2w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.1.tgz", + "integrity": "sha512-dmPlraWKJciFJxHcoubDahGnoIalG5e/BtV6HNDUs7wLXmtnLMHt6w4ed9R8MTL2zNrVPiIdI/HCtMMo0Tm7JQ==", "dependencies": { "bn.js": "^5.1.1", "brorand": "^1.0.5", "elliptic": "^6.5.4", "hash.js": "^1.0.3", - "ripple-address-codec": "^4.2.0" + "ripple-address-codec": "^4.3.1" }, "engines": { - "node": ">= 10", - "npm": ">=7.0.0" + "node": ">= 10" } }, "node_modules/ripple-keypairs/node_modules/bn.js": { @@ -838,6 +859,18 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, + "node_modules/ripple-keypairs/node_modules/ripple-address-codec": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.3.1.tgz", + "integrity": "sha512-Qa3+9wKVvpL/xYtT6+wANsn0A1QcC5CT6IMZbRJZ/1lGt7gmwIfsrCuz1X0+LCEO7zgb+3UT1I1dc0k/5dwKQQ==", + "dependencies": { + "base-x": "^3.0.9", + "create-hash": "^1.1.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/ripple-secret-codec": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ripple-secret-codec/-/ripple-secret-codec-1.0.3.tgz", @@ -1119,21 +1152,6 @@ "node": ">= 10" } }, - "node_modules/xrpl-accountlib/node_modules/ripple-keypairs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.0.tgz", - "integrity": "sha512-LzM3Up9Pwz3dYqnczzNptimN3AxtjeGbDGeiOzREzbkslKiZcJ615b/ghBN4H23SC6W1GAL95juEzzimDi4THw==", - "dependencies": { - "bn.js": "^5.1.1", - "brorand": "^1.0.5", - "elliptic": "^6.5.4", - "hash.js": "^1.0.3", - "ripple-address-codec": "^4.3.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/xrpl-binary-codec": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/xrpl-binary-codec/-/xrpl-binary-codec-1.4.2.tgz", @@ -1190,38 +1208,6 @@ "ripple-keypairs": "^1.1.5" } }, - "node_modules/xrpl-secret-numbers/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, - "node_modules/xrpl-secret-numbers/node_modules/ripple-address-codec": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.3.0.tgz", - "integrity": "sha512-Tvd81i7hpDmNqHvkj6iYlj8Tv3I1Romw5gfjni9eacewJvGV2xe+p2y0FAw39z72qfciRMhQyHvpnviBcWVBNw==", - "dependencies": { - "base-x": "^3.0.9", - "create-hash": "^1.1.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/xrpl-secret-numbers/node_modules/ripple-keypairs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.0.tgz", - "integrity": "sha512-LzM3Up9Pwz3dYqnczzNptimN3AxtjeGbDGeiOzREzbkslKiZcJ615b/ghBN4H23SC6W1GAL95juEzzimDi4THw==", - "dependencies": { - "bn.js": "^5.1.1", - "brorand": "^1.0.5", - "elliptic": "^6.5.4", - "hash.js": "^1.0.3", - "ripple-address-codec": "^4.3.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/xrpl-sign-keypairs": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xrpl-sign-keypairs/-/xrpl-sign-keypairs-2.2.0.tgz", @@ -1233,11 +1219,6 @@ "ripple-keypairs": "^1.1.4" } }, - "node_modules/xrpl-sign-keypairs/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, "node_modules/xrpl-sign-keypairs/node_modules/ripple-address-codec": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.3.0.tgz", @@ -1267,26 +1248,6 @@ "node": ">= 10" } }, - "node_modules/xrpl-sign-keypairs/node_modules/ripple-keypairs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.0.tgz", - "integrity": "sha512-LzM3Up9Pwz3dYqnczzNptimN3AxtjeGbDGeiOzREzbkslKiZcJ615b/ghBN4H23SC6W1GAL95juEzzimDi4THw==", - "dependencies": { - "bn.js": "^5.1.1", - "brorand": "^1.0.5", - "elliptic": "^6.5.4", - "hash.js": "^1.0.3", - "ripple-address-codec": "^4.3.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/xrpl/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, "node_modules/xrpl/node_modules/ripple-address-codec": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.3.0.tgz", @@ -1299,21 +1260,6 @@ "node": ">= 10" } }, - "node_modules/xrpl/node_modules/ripple-keypairs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.0.tgz", - "integrity": "sha512-LzM3Up9Pwz3dYqnczzNptimN3AxtjeGbDGeiOzREzbkslKiZcJ615b/ghBN4H23SC6W1GAL95juEzzimDi4THw==", - "dependencies": { - "bn.js": "^5.1.1", - "brorand": "^1.0.5", - "elliptic": "^6.5.4", - "hash.js": "^1.0.3", - "ripple-address-codec": "^4.3.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/yaeti": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", @@ -1597,9 +1543,9 @@ } }, "evernode-js-client": { - "version": "0.6.20", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.20.tgz", - "integrity": "sha512-OC6VNAhwqnNvUc0NhffxwNI9bTDH+BkD/KBTC5Xuwoiq8BhRfYhmfHBnD6M9K5AvLqv+Jxdufc3l1AlzHgILWg==", + "version": "0.6.21", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.21.tgz", + "integrity": "sha512-Q5P6caMTzx3xaUNKhnP1vJTw3wTP/d2J2xSQEMn4m1+t/t67d8+eii3/FeQapRBSZEbNRHm9EbRry9PJhb9xcg==", "requires": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", @@ -1608,6 +1554,25 @@ "xrpl": "2.2.1", "xrpl-accountlib": "2.2.0", "xrpl-binary-codec": "1.4.2" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "ripple-keypairs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.1.0.tgz", + "integrity": "sha512-Zlmbtn2YUpW4uKlLm2/tpkY5RC/EXQlkJwIIKp0AoF9D23pJ43/EuipNW2F6qURdbkUezDwB0bMV7uRXip3x2w==", + "requires": { + "bn.js": "^5.1.1", + "brorand": "^1.0.5", + "elliptic": "^6.5.4", + "hash.js": "^1.0.3", + "ripple-address-codec": "^4.2.0" + } + } } }, "ext": { @@ -1943,21 +1908,30 @@ } }, "ripple-keypairs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.1.0.tgz", - "integrity": "sha512-Zlmbtn2YUpW4uKlLm2/tpkY5RC/EXQlkJwIIKp0AoF9D23pJ43/EuipNW2F6qURdbkUezDwB0bMV7uRXip3x2w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.1.tgz", + "integrity": "sha512-dmPlraWKJciFJxHcoubDahGnoIalG5e/BtV6HNDUs7wLXmtnLMHt6w4ed9R8MTL2zNrVPiIdI/HCtMMo0Tm7JQ==", "requires": { "bn.js": "^5.1.1", "brorand": "^1.0.5", "elliptic": "^6.5.4", "hash.js": "^1.0.3", - "ripple-address-codec": "^4.2.0" + "ripple-address-codec": "^4.3.1" }, "dependencies": { "bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "ripple-address-codec": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.3.1.tgz", + "integrity": "sha512-Qa3+9wKVvpL/xYtT6+wANsn0A1QcC5CT6IMZbRJZ/1lGt7gmwIfsrCuz1X0+LCEO7zgb+3UT1I1dc0k/5dwKQQ==", + "requires": { + "base-x": "^3.0.9", + "create-hash": "^1.1.2" + } } } }, @@ -2134,11 +2108,6 @@ "ws": "^8.2.2" }, "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, "ripple-address-codec": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.3.0.tgz", @@ -2147,18 +2116,6 @@ "base-x": "^3.0.9", "create-hash": "^1.1.2" } - }, - "ripple-keypairs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.0.tgz", - "integrity": "sha512-LzM3Up9Pwz3dYqnczzNptimN3AxtjeGbDGeiOzREzbkslKiZcJ615b/ghBN4H23SC6W1GAL95juEzzimDi4THw==", - "requires": { - "bn.js": "^5.1.1", - "brorand": "^1.0.5", - "elliptic": "^6.5.4", - "hash.js": "^1.0.3", - "ripple-address-codec": "^4.3.0" - } } } }, @@ -2209,18 +2166,6 @@ "decimal.js": "^10.2.0", "ripple-address-codec": "^4.3.0" } - }, - "ripple-keypairs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.0.tgz", - "integrity": "sha512-LzM3Up9Pwz3dYqnczzNptimN3AxtjeGbDGeiOzREzbkslKiZcJ615b/ghBN4H23SC6W1GAL95juEzzimDi4THw==", - "requires": { - "bn.js": "^5.1.1", - "brorand": "^1.0.5", - "elliptic": "^6.5.4", - "hash.js": "^1.0.3", - "ripple-address-codec": "^4.3.0" - } } } }, @@ -2274,34 +2219,6 @@ "@types/brorand": "^1.0.30", "brorand": "^1.1.0", "ripple-keypairs": "^1.1.5" - }, - "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, - "ripple-address-codec": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.3.0.tgz", - "integrity": "sha512-Tvd81i7hpDmNqHvkj6iYlj8Tv3I1Romw5gfjni9eacewJvGV2xe+p2y0FAw39z72qfciRMhQyHvpnviBcWVBNw==", - "requires": { - "base-x": "^3.0.9", - "create-hash": "^1.1.2" - } - }, - "ripple-keypairs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.0.tgz", - "integrity": "sha512-LzM3Up9Pwz3dYqnczzNptimN3AxtjeGbDGeiOzREzbkslKiZcJ615b/ghBN4H23SC6W1GAL95juEzzimDi4THw==", - "requires": { - "bn.js": "^5.1.1", - "brorand": "^1.0.5", - "elliptic": "^6.5.4", - "hash.js": "^1.0.3", - "ripple-address-codec": "^4.3.0" - } - } } }, "xrpl-sign-keypairs": { @@ -2315,11 +2232,6 @@ "ripple-keypairs": "^1.1.4" }, "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, "ripple-address-codec": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.3.0.tgz", @@ -2341,18 +2253,6 @@ "decimal.js": "^10.2.0", "ripple-address-codec": "^4.3.0" } - }, - "ripple-keypairs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.3.0.tgz", - "integrity": "sha512-LzM3Up9Pwz3dYqnczzNptimN3AxtjeGbDGeiOzREzbkslKiZcJ615b/ghBN4H23SC6W1GAL95juEzzimDi4THw==", - "requires": { - "bn.js": "^5.1.1", - "brorand": "^1.0.5", - "elliptic": "^6.5.4", - "hash.js": "^1.0.3", - "ripple-address-codec": "^4.3.0" - } } } }, diff --git a/installer/jshelper/package.json b/installer/jshelper/package.json index 4aad456..6cd21ba 100644 --- a/installer/jshelper/package.json +++ b/installer/jshelper/package.json @@ -4,7 +4,8 @@ "build": "ncc build index.js --minify -o dist" }, "dependencies": { - "evernode-js-client": "0.6.20", - "ip6addr": "0.2.5" + "evernode-js-client": "0.6.21", + "ip6addr": "0.2.5", + "ripple-keypairs": "1.3.1" } } diff --git a/installer/prereq.sh b/installer/prereq.sh index 6c5f53f..588d3f4 100755 --- a/installer/prereq.sh +++ b/installer/prereq.sh @@ -28,6 +28,7 @@ stage "Installing dependencies" # To fix - Repository 'https://apprepo.vultr.com/ubuntu universal InRelease' changed its 'Codename' value from 'buster' to 'universal' apt-get update --allow-releaseinfo-change apt-get install -y uidmap fuse3 cgroup-tools quota curl openssl jq + # uidmap # Required for rootless docker. # slirp4netns # Required for high performance rootless networking. # fuse3 # Required for hpfs. @@ -40,8 +41,13 @@ apt-get install -y uidmap fuse3 cgroup-tools quota curl openssl jq # Install nodejs if not exists. if ! command -v node &>/dev/null; then stage "Installing nodejs" - apt-get -y install ca-certificates # In case nodejs package certitficates are renewed. - curl -sL https://deb.nodesource.com/setup_16.x | bash - + apt-get install -y ca-certificates curl gnupg + mkdir -p /etc/apt/keyrings + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg + + NODE_MAJOR=16 + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list + apt-get update apt-get -y install nodejs else version=$(node -v | cut -d '.' -f1) diff --git a/installer/sashimono-install.sh b/installer/sashimono-install.sh index 3906741..9042fd4 100755 --- a/installer/sashimono-install.sh +++ b/installer/sashimono-install.sh @@ -16,7 +16,7 @@ diskKB=${9} lease_amount=${10} rippled_server=${11} xrpl_account_address=${12} -xrpl_account_secret=${13} +xrpl_account_secret_path=${13} email_address=${14} tls_key_file=${15} tls_cert_file=${16} @@ -28,16 +28,49 @@ ipv6_net_interface=${20} script_dir=$(dirname "$(realpath "$0")") desired_slirp4netns_version="1.2.1" setup_helper_dir="/tmp/evernode-setup-helpers" +secret_backup_location="/root/.evernode/.host-account-secret.key" +previous_secret_path_note=/root/.evernode/previous_secret_path.txt +default_key_filepath="/home/$MB_XRPL_USER/.evernode-host/.host-account-secret.key" + +secret_stored_path="-" function stage() { echo "STAGE $1" # This is picked up by the setup console output filter. } +function confirm() { + echo -en $1" [Y/n] " + local yn="" + read yn /etc/systemd/system/$EVERNODE_AUTO_UPDATE_SERVICE.service - - # Create a timer for the service (every two hours). - echo "[Unit] -Description=Timer for the Evernode auto-update. -# Allow manual starts -RefuseManualStart=no -# Allow manual stops -RefuseManualStop=no -[Timer] -Unit=$EVERNODE_AUTO_UPDATE_SERVICE.service -OnCalendar=0/12:00:00 -# Execute job if it missed a run due to machine being off -Persistent=true -# To prevent rush time, adding 2 hours delay -RandomizedDelaySec=7200 -[Install] -WantedBy=timers.target" >/etc/systemd/system/$EVERNODE_AUTO_UPDATE_SERVICE.timer - - # Reload the systemd daemon. - systemctl daemon-reload - - echo "Enabling Evernode auto update service..." - systemctl enable $EVERNODE_AUTO_UPDATE_SERVICE.service - - echo "Enabling Evernode auto update timer..." - systemctl enable $EVERNODE_AUTO_UPDATE_SERVICE.timer - echo "Starting Evernode auto update timer..." - systemctl start $EVERNODE_AUTO_UPDATE_SERVICE.timer -} - function setup_certbot() { stage "Setting up letsencrypt certbot" @@ -231,13 +222,13 @@ rm -r "$tmp" openssl req -newkey rsa:2048 -new -nodes -x509 -days 365 -keyout $SASHIMONO_DATA/contract_template/cfg/tlskey.pem \ -out $SASHIMONO_DATA/contract_template/cfg/tlscert.pem -subj "/C=HP/CN=$(jq -r '.hp.host_address' $SASHIMONO_DATA/sa.cfg)" -# Setup tls certs used for contract instance websockets. -[ "$UPGRADE" == "0" ] && setup_tls_certs - # Install Sashimono agent binaries into sashimono bin dir. cp "$script_dir"/{sagent,hpfs,user-cgcreate.sh,user-install.sh,user-uninstall.sh,docker-registry-uninstall.sh} $SASHIMONO_BIN chmod -R +x $SASHIMONO_BIN +# Setup tls certs used for contract instance websockets. +[ "$UPGRADE" == "0" ] && setup_tls_certs + # Copy the temporary setup-helper directory content to SASHIMONO_BIN directory. cp -Rdp $setup_helper_dir $SASHIMONO_BIN/evernode-setup-helpers @@ -279,14 +270,37 @@ if [ "$NO_MB" == "" ]; then cp -r "$script_dir"/mb-xrpl $SASHIMONO_BIN - # Creating message board user (if not exists). + # Create MB_XRPL_USER if does not exists. if ! grep -q "^$MB_XRPL_USER:" /etc/passwd; then useradd --shell /usr/sbin/nologin -m $MB_XRPL_USER + fi + + # Assign message board user priviledges. + if ! id -nG "$MB_XRPL_USER" | grep -qw "$SASHIADMIN_GROUP"; then usermod --lock $MB_XRPL_USER usermod -a -G $SASHIADMIN_GROUP $MB_XRPL_USER loginctl enable-linger $MB_XRPL_USER # Enable lingering to support service installation. fi + if [ "$UPGRADE" != "0" ]; then + # Restore keyfile. + if [ -f $secret_backup_location ]; then + echo "Restoring secret file via $secret_backup_location." + secret_stored_path=$(cat $previous_secret_path_note) + + if [ "$secret_stored_path" == "$default_key_filepath" ]; then + key_directory=$(dirname "$secret_stored_path") + if [ ! -d "$key_directory" ]; then + mkdir -p "$key_directory" + fi + fi + + [ -d $(dirname "$secret_stored_path") ] && mv $secret_backup_location $secret_stored_path && \ + chown $MB_XRPL_USER: $secret_stored_path && \ + chmod 600 $secret_stored_path && rm -f $previous_secret_path_note + fi + fi + # First create the folder from root and then transfer ownership to the user # since the folder is created in /etc/sashimono directory. ! mkdir -p $MB_XRPL_DATA && echo "Could not create '$MB_XRPL_DATA'. Make sure you are running as sudo." && exit 1 @@ -304,7 +318,7 @@ if [ "$NO_MB" == "" ]; then # ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN betagen $EVERNODE_GOVERNOR_ADDRESS $inetaddr $lease_amount $rippled_server $xrpl_account_secret && echo "XRPLACC_FAILURE" && rollback # doreg=1 - ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN new $xrpl_account_address $xrpl_account_secret $EVERNODE_GOVERNOR_ADDRESS $inetaddr $lease_amount $rippled_server $ipv6_subnet $ipv6_net_interface && echo "XRPLACC_FAILURE" && rollback + ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN new $xrpl_account_address $xrpl_account_secret_path $EVERNODE_GOVERNOR_ADDRESS $inetaddr $lease_amount $rippled_server $ipv6_subnet $ipv6_net_interface && echo "XRPLACC_FAILURE" && rollback doreg=1 fi @@ -452,11 +466,6 @@ if [ ! -f /run/reboot-required.pkgs ] || [ ! -n "$(grep sashimono /run/reboot-re fi fi -stage "Configuring auto updater service" - -# Enable the Evernode Auto Updater Service. -enable_evernode_auto_updater - echo "Sashimono installed successfully." exit 0 diff --git a/installer/sashimono-uninstall.sh b/installer/sashimono-uninstall.sh index c14181c..23de3d1 100755 --- a/installer/sashimono-uninstall.sh +++ b/installer/sashimono-uninstall.sh @@ -2,9 +2,13 @@ # Sashimono agent uninstall script. # This must be executed with root privileges. +export TRANSFER=${TRANSFER:-0} + [ "$UPGRADE" == "0" ] && echo "---Sashimono uninstaller---" || echo "---Sashimono uninstaller (for upgrade)---" force=$1 +secret_backup_location="/root/.evernode/.host-account-secret.key" +previous_secret_path_note=/root/.evernode/previous_secret_path.txt function confirm() { echo -en $1" [Y/n] " @@ -30,24 +34,6 @@ function cgrulesengd_servicename() { fi } -function remove_evernode_auto_updater() { - - echo "Removing Evernode auto update timer..." - systemctl stop $EVERNODE_AUTO_UPDATE_SERVICE.timer - systemctl disable $EVERNODE_AUTO_UPDATE_SERVICE.timer - service_path="/etc/systemd/system/$EVERNODE_AUTO_UPDATE_SERVICE.timer" - rm $service_path - - echo "Removing Evernode auto update service..." - systemctl stop $EVERNODE_AUTO_UPDATE_SERVICE.service - systemctl disable $EVERNODE_AUTO_UPDATE_SERVICE.service - service_path="/etc/systemd/system/$EVERNODE_AUTO_UPDATE_SERVICE.service" - rm $service_path - - # Reload the systemd daemon. - systemctl daemon-reload -} - function cleanup_certbot_ssl() { # revoke/delete certs if certbot is used. if command -v certbot &>/dev/null && [ -f "$SASHIMONO_DATA/sa.cfg" ]; then @@ -86,6 +72,16 @@ if grep -q "^$MB_XRPL_USER:" /etc/passwd; then sudo -u "$MB_XRPL_USER" XDG_RUNTIME_DIR="$mb_user_runtime_dir" systemctl --user disable $MB_XRPL_SERVICE fi + # Backup the secret in upgrade process. + if [[ "$UPGRADE" != "0" ]]; then + + mb_xrpl_config_data=$(cat $MB_XRPL_DATA/mb-xrpl.cfg) + secret_stored_path=$(echo $mb_xrpl_config_data | jq -r '.xrpl.secretPath') + backup_dir=$(dirname $secret_backup_location) + echo "Backing up account secret at $secret_backup_location." && mkdir -p $backup_dir && cp -p --no-preserve=ownership $secret_stored_path $secret_backup_location + echo $secret_stored_path >$previous_secret_path_note + fi + fi # Uninstall all contract instance users--------------------------- @@ -186,6 +182,15 @@ if grep -q "^$MB_XRPL_USER:" /etc/passwd; then ! confirm "Evernode host deregistration failed. Still do you want to continue uninstallation?" && echo "Aborting uninstallation. Try again later." && exit 1 echo "Continuing uninstallation..." fi + + mb_xrpl_config_data=$(cat $MB_XRPL_DATA/mb-xrpl.cfg) + current_secret_path=$(echo $mb_xrpl_config_data | jq -r '.xrpl.secretPath') + + # Remove secret from the saved location.(This is applied for secrets not specified in default path) + rm -f $current_secret_path + + # Remove Evernode util directory + [ -d "/root/.evernode" ] && rm -rf "/root/.evernode" fi echo "Deleting message board user..." @@ -215,7 +220,4 @@ groupdel $SASHIADMIN_GROUP [ "$UPGRADE" == "0" ] && echo "Sashimono uninstalled successfully." || echo "Sashimono uninstalled successfully. Your data has been preserved." -# Remove the Evernode Auto Updater Service. -[ "$UPGRADE" == "0" ] && remove_evernode_auto_updater - exit 0 diff --git a/installer/setup-old.sh b/installer/setup-old.sh new file mode 100644 index 0000000..5e5aa33 --- /dev/null +++ b/installer/setup-old.sh @@ -0,0 +1,1447 @@ +#!/bin/bash +# Evernode host setup tool to manage Sashimono installation and host registration. +# This script is also used as the 'evernode' cli alias after the installation. +# usage: ./setup.sh install + +# surrounding braces are needed make the whole script to be buffered on client before execution. +{ + +# set the LANG environment variable to a universal encoding +export LANG=C.UTF-8 + +evernode="Evernode" +maxmind_creds="687058:FtcQjM0emHFMEfgI" +cgrulesengd_default="cgrulesengd" +alloc_ratio=80 +ramKB_per_instance=524288 +instances_per_core=3 +max_non_ipv6_instances=5 +max_ipv6_prefix_len=112 +evernode_alias=/usr/bin/evernode +log_dir=/tmp/evernode-beta +cloud_storage="https://stevernode.blob.core.windows.net/evernode-dev-v3-a86733dc-c0fc-4b1f-97cf-2071ae9c5bee" +setup_script_url="$cloud_storage/setup.sh" +installer_url="$cloud_storage/installer.tar.gz" +licence_url="$cloud_storage/licence.txt" +nodejs_url="$cloud_storage/node" +jshelper_url="$cloud_storage/setup-jshelper.tar.gz" +installer_version_timestamp_file="installer.version.timestamp" +setup_version_timestamp_file="setup.version.timestamp" +default_rippled_server="wss://hooks-testnet-v3.xrpl-labs.com" +setup_helper_dir="/tmp/evernode-setup-helpers" +nodejs_util_bin="$setup_helper_dir/node" +jshelper_bin="$setup_helper_dir/jshelper/index.js" + +# export vars used by Sashimono installer. +export USER_BIN=/usr/bin +export SASHIMONO_BIN=/usr/bin/sashimono +export MB_XRPL_BIN=$SASHIMONO_BIN/mb-xrpl +export DOCKER_BIN=$SASHIMONO_BIN/dockerbin +export SASHIMONO_DATA=/etc/sashimono +export MB_XRPL_DATA=$SASHIMONO_DATA/mb-xrpl +export SASHIMONO_SERVICE="sashimono-agent" +export CGCREATE_SERVICE="sashimono-cgcreate" +export MB_XRPL_SERVICE="sashimono-mb-xrpl" +export SASHIADMIN_GROUP="sashiadmin" +export SASHIUSER_GROUP="sashiuser" +export SASHIUSER_PREFIX="sashi" +export MB_XRPL_USER="sashimbxrpl" +export CG_SUFFIX="-cg" +export EVERNODE_AUTO_UPDATE_SERVICE="evernode-auto-update" + +# TODO: Verify if the correct Governor address is present in the DEV/BETA envs. +export EVERNODE_GOVERNOR_ADDRESS="raVhw4Q8FQr296jdaDLDfZ4JDhh7tFG7SF" +export MIN_EVR_BALANCE=5120 + +# Private docker registry (not used for now) +export DOCKER_REGISTRY_USER="sashidockerreg" +export DOCKER_REGISTRY_PORT=0 + +# We execute some commands as unprivileged user for better security. +# (we execute as the user who launched this script as sudo) +noroot_user=${SUDO_USER:-$(whoami)} + +# Helper to print multi line text. +# (When passed as a parameter, bash auto strips spaces and indentation which is what we want) +function echomult() { + echo -e $1 +} + +function confirm() { + echo -en $1" [Y/n] " + local yn="" + read yn /dev/null; then + version=$(node -v | cut -d '.' -f1) + version=${version:1} + if [[ $version -lt 16 ]]; then + echo "$evernode requires NodeJs 16.x or later. You system has NodeJs $version installed. Either remove the NodeJs installation or upgrade to NodeJs 16.x." + exit 1 + fi + fi + + # Check bc command is installed. + if ! command -v bc &>/dev/null; then + echo "bc command not found. Installing.." + apt-get -y install bc >/dev/null + fi + + # Check host command is installed. + if ! command -v host &> /dev/null; then + echo "host command not found. Installing.." + apt-get -y install bind9-host >/dev/null + fi +} + +function check_sys_req() { + + # Assign sys resource info to global vars since these will also be used for instance allocation later. + ramKB=$(free | grep Mem | awk '{print $2}') + swapKB=$(free | grep -i Swap | awk '{print $2}') + diskKB=$(df | grep -w /home | head -1 | awk '{print $4}') + [ -z "$diskKB" ] && diskKB=$(df | grep -w / | head -1 | awk '{print $4}') + + [ "$SKIP_SYSREQ" == "1" ] && echo "System requirements check skipped." && return 0 + + local proc1=$(ps --no-headers -o comm 1) + if [ "$proc1" != "systemd" ]; then + echo "$evernode host installation requires systemd. Your system does not have systemd running. Aborting." + exit 1 + fi + + local os=$(grep -ioP '^ID=\K.+' /etc/os-release) + local osversion=$(grep -ioP '^VERSION_ID=\K.+' /etc/os-release) + + local errors="" + ([ "$os" != "ubuntu" ] || [ "$osversion" != '"20.04"' ]) && errors=" OS: $os $osversion (required: Ubuntu 20.04)\n" + [ $ramKB -lt 2000000 ] && errors="$errors RAM: $(GB $ramKB) (required: 2 GB RAM)\n" + [ $swapKB -lt 2000000 ] && errors="$errors Swap: $(GB $swapKB) (required: 2 GB Swap)\n" + [ $diskKB -lt 4000000 ] && errors="$errors Disk space (/home): $(GB $diskKB) (required: 4 GB)\n" + + if [ -z "$errors" ]; then + echo "System check complete. Your system is capable of becoming an $evernode host." + else + echomult "Your system does not meet following $evernode system requirements:\n $errors" + echomult "$evernode host registration requires Ubuntu 20.04 with minimum 2 GB RAM, + 2 GB Swap and 4 GB free disk space for /home. Aborting setup." + exit 1 + fi +} + +function init_setup_helpers() { + + echo "Downloading setup support files..." + + local jshelper_dir=$(dirname $jshelper_bin) + rm -r $jshelper_dir >/dev/null 2>&1 + sudo -u $noroot_user mkdir -p $jshelper_dir + + [ ! -f "$nodejs_util_bin" ] && sudo -u $noroot_user curl $nodejs_url --output $nodejs_util_bin + [ ! -f "$nodejs_util_bin" ] && echo "Could not download nodejs for setup checks." && exit 1 + chmod +x $nodejs_util_bin + + if [ ! -f "$jshelper_bin" ]; then + pushd $jshelper_dir >/dev/null 2>&1 + sudo -u $noroot_user curl $jshelper_url --output jshelper.tar.gz + sudo -u $noroot_user tar zxf jshelper.tar.gz --strip-components=1 + rm jshelper.tar.gz + popd >/dev/null 2>&1 + fi + [ ! -f "$jshelper_bin" ] && echo "Could not download helper tool for setup checks." && exit 1 + echo -e "Done.\n" +} + +function exec_jshelper() { + + # Create fifo file to read response data from the helper script. + local resp_file=$setup_helper_dir/helper_fifo + [ -p $resp_file ] || sudo -u $noroot_user mkfifo $resp_file + + # Execute js helper asynchronously while collecting response to fifo file. + sudo -u $noroot_user RESPFILE=$resp_file $nodejs_util_bin $jshelper_bin "$@" >/dev/null 2>&1 & + local pid=$! + local result=$(cat $resp_file) && [ "$result" != "-" ] && echo $result + + # Wait for js helper to exit and reflect the error exit code in this function return. + wait $pid && [ $? -eq 0 ] && rm $resp_file && return 0 + rm $resp_file && return 1 +} + +function resolve_filepath() { + # name reference the variable name provided as first argument. + local -n filepath=$1 + local option=$2 + local prompt="${*:3} " + + while [ -z "$filepath" ]; do + read -p "$prompt" filepath /dev/null 2>&1 && return 0 + inetaddr="" && return 1 +} + +function validate_inet_addr() { + # inert address cannot be empty and cannot contain spaces. + [ -z "$inetaddr" ] || [[ $inetaddr = *" "* ]] && inetaddr="" && return 1 + + # Attempt to resolve ip (in case inetaddr is a DNS address) + # This will resolve correctly if inetaddr is a valid ip or dns address. + + local resolved_ips=$(getent hosts $inetaddr | wc -l) + + # Check if there is more than one IP address + if [ $resolved_ips -eq 1 ]; then + return 0 + elif [ $resolved_ips -gt 1 ]; then + echo "Your domain ($inetaddr) must point to a single IP address." + fi + + # If invalid, reset inetaddr and return with non-zero code. + inetaddr="" && return 1 + +} + +function validate_positive_decimal() { + ! [[ $1 =~ ^(0*[1-9][0-9]*(\.[0-9]+)?|0+\.[0-9]*[1-9][0-9]*)$ ]] && return 1 + return 0 +} + +function validate_rippled_url() { + ! [[ $1 =~ ^(wss?:\/\/)([^\/|^:|^ ]{3,})(:([0-9]{1,5}))?$ ]] && echo "Rippled URL must be a valid URL that starts with 'wss://'" && return 1 + + echo "Checking server $1..." + ! exec_jshelper validate-server $1 && echo "Could not communicate with the rippled server." && return 1 + return 0 +} + +function validate_email_address() { + local emailAddress=$1 + email_address_length=${#emailAddress} + ( ( ! [[ "$email_address_length" -le 40 ]] && echo "Email address length should not exceed 40 characters." ) || + ( ! [[ $emailAddress =~ .+@.+ ]] && echo "Email address is invalid." ) ) || return 0 + return 1 +} + +function set_inet_addr() { + + if $interactive && [ "$NO_DOMAIN" == "" ] ; then + echo "" + while [ -z "$inetaddr" ]; do + read -p "Please specify the domain name that this host is reachable at: " inetaddr &1 \ + | tee -a $logfile | stdbuf --output=L grep "STAGE" | cut -d ' ' -f 2- && install_failure + fi + + # Create evernode cli alias at the begining. + # So, if the installation attempt failed user can uninstall the failed installation using evernode commands. + ! create_evernode_alias && install_failure + + # Currently the domain address saved only in account_info and an empty value in Hook states. + # Set description to empty value ('_' will be treated as empty) + description="_" + + echo "Installing Sashimono..." + + init_setup_helpers + registry_address=$(exec_jshelper access-evernode-cfg $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_account_address registryAddress) + + # Filter logs with STAGE prefix and ommit the prefix when echoing. + # If STAGE log contains -p arg, move the cursor to previous log line and overwrite the log. + ! UPGRADE=$upgrade EVERNODE_REGISTRY_ADDRESS=$registry_address ./sashimono-install.sh $inetaddr $init_peer_port $init_user_port $countrycode $alloc_instcount \ + $alloc_cpu $alloc_ramKB $alloc_swapKB $alloc_diskKB $lease_amount $rippled_server $xrpl_account_address $xrpl_account_secret $email_address \ + $tls_key_file $tls_cert_file $tls_cabundle_file $description $ipv6_subnet $ipv6_net_interface 2>&1 \ + | tee -a $logfile | stdbuf --output=L grep "STAGE\|ERROR" \ + | while read line ; do [[ $line =~ ^STAGE[[:space:]]-p(.*)$ ]] && echo -e \\e[1A\\e[K"${line:9}" || echo ${line:6} ; done \ + && remove_evernode_alias && install_failure + set +o pipefail + + rm -r $tmp + + # Write the verison timestamp to a file for later updated version comparison. + echo $installer_version_timestamp > $SASHIMONO_DATA/$installer_version_timestamp_file + echo $setup_version_timestamp > $SASHIMONO_DATA/$setup_version_timestamp_file +} + +function check_exisiting_contracts() { + + local upgrade=$1 + + # Check the condition of existing contract instances. + local users=$(cut -d: -f1 /etc/passwd | grep "^$SASHIUSER_PREFIX" | sort) + readarray -t userarr <<<"$users" + local sashiusers=() + for user in "${userarr[@]}"; do + [ ${#user} -lt 24 ] || [ ${#user} -gt 32 ] || [[ ! "$user" =~ ^$SASHIUSER_PREFIX[0-9]+$ ]] && continue + sashiusers+=("$user") + done + local ucount=${#sashiusers[@]} + + if [ "$upgrade" == "0" ] ; then + $interactive && [ $ucount -gt 0 ] && ! confirm "This will delete $ucount contract instances. \n\nDo you still want to continue?" && exit 1 + ! $interactive && echo "$ucount contract instances will be deleted." + fi +} + +function uninstall_evernode() { + + local upgrade=$1 + + if ! $transfer ; then + [ "$upgrade" == "0" ] && echo "Uninstalling..." || echo "Uninstalling for upgrade..." + ! UPGRADE=$upgrade TRANSFER=0 $SASHIMONO_BIN/sashimono-uninstall.sh $2 && uninstall_failure + else + echo "Intiating Transfer..." + echo "Uninstalling for transfer..." + ! UPGRADE=$upgrade TRANSFER=1 $SASHIMONO_BIN/sashimono-uninstall.sh $2 && uninstall_failure + fi + # Remove the evernode alias at the end. + # So, if the uninstallation failed user can try uninstall again with evernode commands. + remove_evernode_alias +} + +function update_evernode() { + echo "Checking for updates..." + local latest_installer_script_version=$(online_version_timestamp $installer_url) + local latest_setup_script_version=$(online_version_timestamp $setup_script_url) + [ -z "$latest_installer_script_version" ] && echo "Could not check for updates. Online installer not found." && exit 1 + + local current_installer_script_version=$(cat $SASHIMONO_DATA/$installer_version_timestamp_file) + local current_setup_script_version=$(cat $SASHIMONO_DATA/$setup_version_timestamp_file) + [ "$latest_installer_script_version" == "$current_installer_script_version" ] && [ "$latest_setup_script_version" == "$current_setup_script_version" ] && echo "Your $evernode installation is up to date." && exit 0 + + echo "New $evernode update available. Setup will re-install $evernode with updated software. Your account and contract instances will be preserved." + $interactive && ! confirm "\nDo you want to install the update?" && exit 1 + + echo "Starting upgrade..." + # Alias for setup.sh is created during 'install_evernode' too. + # If only the setup.sh is updated but not the installer, then the alias should be created again. + if [ "$latest_installer_script_version" != "$current_installer_script_version" ] ; then + uninstall_evernode 1 + install_evernode 1 + elif [ "$latest_setup_script_version" != "$current_setup_script_version" ] ; then + [ -d $log_dir ] || mkdir -p $log_dir + logfile="$log_dir/installer-$(date +%s).log" + remove_evernode_alias + ! create_evernode_alias && echo "Alias creation failed." + echo $latest_setup_script_version > $SASHIMONO_DATA/$setup_version_timestamp_file + fi + + rm -r $setup_helper_dir >/dev/null 2>&1 + + echo "Upgrade complete." +} + +function init_evernode_transfer() { + + if ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN transfer $transferee_address && + [ "$force" != "-f" ] && [ -f $mb_service_path ]; then + ! confirm "Evernode transfer initiation was failed. Still do you want to continue the unistallation?" && echo "Aborting unistallation. Try again later." && exit 1 + echo "Continuing uninstallation..." + fi + +} + +function create_log() { + tempfile=$(mktemp /tmp/evernode.XXXXXXXXX.log) + { + echo "System:" + uname -r + lsb_release -a + echo "" + echo "sa.cfg:" + cat "$SASHIMONO_DATA/sa.cfg" + echo "" + echo "mb-xrpl.cfg:" + cat "$MB_XRPL_DATA/mb-xrpl.cfg" + echo "" + echo "Sashimono log:" + journalctl -u sashimono-agent.service | tail -n 200 + echo "" + echo "Message board log:" + sudo -u sashimbxrpl bash -c journalctl --user -u sashimono-mb-xrpl | tail -n 200 + echo "" + echo "Auto updater service log:" + journalctl -u evernode-auto-update | tail -n 200 + } > "$tempfile" 2>&1 + echo "Evernode log saved to $tempfile" +} + +# Create a copy of this same script as a command. +function create_evernode_alias() { + ! curl -fsSL $setup_script_url --output $evernode_alias >> $logfile 2>&1 && echo "Error in creating alias." && return 1 + ! chmod +x $evernode_alias >> $logfile 2>&1 && echo "Error in changing permission for the alias." && return 1 + return 0 +} + +function remove_evernode_alias() { + rm $evernode_alias +} + +function check_installer_pending_finish() { + if [ -f /run/reboot-required.pkgs ] && [ -n "$(grep sashimono /run/reboot-required.pkgs)" ]; then + echo "Your system needs to be rebooted in order to complete Sashimono installation." + $interactive && confirm "Reboot now?" && reboot + ! $interactive && echo "Rebooting..." && reboot + return 0 + else + # If reboot not required, check whether re-login is required in case the setup was run with sudo. + # This is because the user account gets added to sashiadmin group and re-login is needed for group permission to apply. + # without this, user cannot run "sashi" cli commands without sudo. + if [ "$mode" == "install" ] && [ -n "$SUDO_USER" ] ; then + echo "You need to logout and log back in, to complete Sashimono installation." + return 0 + else + return 1 + fi + fi +} + +function reg_info() { + echo "" + if MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN reginfo ; then + local sashimono_agent_status=$(systemctl is-active sashimono-agent.service) + local mb_user_id=$(id -u "$MB_XRPL_USER") + local mb_user_runtime_dir="/run/user/$mb_user_id" + local sashimono_mb_xrpl_status=$(sudo -u "$MB_XRPL_USER" XDG_RUNTIME_DIR="$mb_user_runtime_dir" systemctl --user is-active $MB_XRPL_SERVICE) + echo "Sashimono agent status: $sashimono_agent_status" + echo "Sashimono mb xrpl status: $sashimono_mb_xrpl_status" + echo -e "\nYour account details are stored in $MB_XRPL_DATA/mb-xrpl.cfg and $MB_XRPL_DATA/secret.cfg." + fi +} + +function apply_ssl() { + [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 + + local tls_key_file=$1 + local tls_cert_file=$2 + local tls_cabundle_file=$3 + + ([ ! -f "$tls_key_file" ] || [ ! -f "$tls_cert_file" ] || \ + ([ "$tls_cabundle_file" != "" ] && [ ! -f "$tls_cabundle_file" ])) && + echo -e "One or more invalid files provided.\nusage: applyssl " && exit 1 + + echo "Applying new SSL certificates for $evernode" + echo "Key: $tls_key_file" && cp $tls_key_file $SASHIMONO_DATA/contract_template/cfg/tlskey.pem || exit 1 + echo "Cert: $tls_cert_file" && cp $tls_cert_file $SASHIMONO_DATA/contract_template/cfg/tlscert.pem || exit 1 + # ca bundle is optional. + [ "$tls_cabundle_file" != "" ] && echo "CA bundle: $tls_cabundle_file" && (cat $tls_cabundle_file >> $SASHIMONO_DATA/contract_template/cfg/tlscert.pem || exit 1) + + sashi list | jq -rc '.[]' | while read -r inst; do \ + local instuser=$(echo $inst | jq -r '.user'); \ + local instname=$(echo $inst | jq -r '.name'); \ + echo -e "\nStopping contract instance $instname" && sashi stop -n $instname && \ + echo "Updating SSL certificates" && \ + cp $SASHIMONO_DATA/contract_template/cfg/tlskey.pem $SASHIMONO_DATA/contract_template/cfg/tlscert.pem /home/$instuser/$instname/cfg/ && \ + chmod 644 /home/$instuser/$instname/cfg/tlscert.pem && chmod 600 /home/$instuser/$instname/cfg/tlskey.pem && \ + chown -R $instuser:$instuser /home/$instuser/$instname/cfg/*.pem && \ + echo -e "Starting contract instance $instname" && sashi start -n $instname; \ + done + + echo "Done." +} + +function reconfig_sashi() { + echomult "Configuaring sashimono...\n" + + ! $SASHIMONO_BIN/sagent reconfig $SASHIMONO_DATA $alloc_instcount $alloc_cpu $alloc_ramKB $alloc_swapKB $alloc_diskKB && + echomult "There was an error in updating sashimono configuration." && return 1 + + # Update cgroup allocations. + ( [[ $alloc_ramKB -gt 0 ]] || [[ $alloc_swapKB -gt 0 ]] || [[ $alloc_instcount -gt 0 ]] ) && + echomult "Updating the cgroup configuration..." && + ! $SASHIMONO_BIN/user-cgcreate.sh $SASHIMONO_DATA && echomult "Error occured while upgrading cgroup allocations" && return 1 + + # Update disk quotas. + if ( [[ $alloc_diskKB -gt 0 ]] || [[ $alloc_instcount -gt 0 ]] ) ; then + echomult "Updating the disk quotas..." + + users=$(cut -d: -f1 /etc/passwd | grep "^$SASHIUSER_PREFIX" | sort) + readarray -t userarr <<<"$users" + sashiusers=() + for user in "${userarr[@]}"; do + [ ${#user} -lt 24 ] || [ ${#user} -gt 32 ] || [[ ! "$user" =~ ^$SASHIUSER_PREFIX[0-9]+$ ]] && continue + sashiusers+=("$user") + done + + max_storage_kbytes=$(jq '.system.max_storage_kbytes' $saconfig) + max_instance_count=$(jq '.system.max_instance_count' $saconfig) + disk=$(expr $max_storage_kbytes / $max_instance_count) + ucount=${#sashiusers[@]} + if [ $ucount -gt 0 ]; then + for user in "${sashiusers[@]}"; do + setquota -g -F vfsv0 "$user" "$disk" "$disk" 0 0 / + done + fi + fi + + return 0 +} + +function reconfig_mb() { + echomult "Configuaring message board...\n" + + ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN reconfig $lease_amount $alloc_instcount $rippled_server $ipv6_subnet $ipv6_net_interface && + echo "There was an error in updating message board configuration." && return 1 + return 0 +} + +function config() { + [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 + + alloc_instcount=0 + alloc_cpu=0 + alloc_ramKB=0 + alloc_swapKB=0 + alloc_diskKB=0 + lease_amount=0 + rippled_server='-' + ipv6_subnet='-' + ipv6_net_interface='-' + + local saconfig="$SASHIMONO_DATA/sa.cfg" + local max_instance_count=$(jq '.system.max_instance_count' $saconfig) + local max_mem_kbytes=$(jq '.system.max_mem_kbytes' $saconfig) + local max_swap_kbytes=$(jq '.system.max_swap_kbytes' $saconfig) + local max_storage_kbytes=$(jq '.system.max_storage_kbytes' $saconfig) + + local mbconfig="$MB_XRPL_DATA/mb-xrpl.cfg" + local cfg_lease_amount=$(jq '.xrpl.leaseAmount' $mbconfig) + local cfg_rippled_server=$(jq -r '.xrpl.rippledServer' $mbconfig) + + local cfg_ipv6_subnet=$(jq -r '.networking.ipv6.subnet' $mbconfig) + local cfg_ipv6_net_interface=$(jq -r '.networking.ipv6.interface' $mbconfig) + + local update_sashi=0 + local update_mb=0 + + local sub_mode=${1} + local occupied_instance_count=$(sashi list | jq length) + + if [ "$sub_mode" == "resources" ] ; then + + local ramMB=${2} # memory to allocate for contract instances. + local swapMB=${3} # Swap to allocate for contract instances. + local diskMB=${4} # Disk space to allocate for contract instances. + local instcount=${5} # Total contract instance count. + + [ -z $ramMB ] && [ -z $swapMB ] && [ -z $diskMB ] && [ -z $instcount ] && + echomult "Your current resource allocation is: + \n Memory: $(GB $max_mem_kbytes) + \n Swap: $(GB $max_swap_kbytes) + \n Disk space: $(GB $max_storage_kbytes) + \n Instance count: $max_instance_count\n" && exit 0 + + + local help_text="Usage: evernode config resources | evernode config resources \n" + [ ! -z $ramMB ] && [[ $ramMB != 0 ]] && ! validate_positive_decimal $ramMB && + echomult "Invalid memory size.\n $help_text" && exit 1 + [ ! -z $swapMB ] && [[ $swapMB != 0 ]] && ! validate_positive_decimal $swapMB && + echomult "Invalid swap size.\n $help_text" && exit 1 + [ ! -z $diskMB ] && [[ $diskMB != 0 ]] && ! validate_positive_decimal $diskMB && + echomult "Invalid disk size.\n $help_text" && exit 1 + [ ! -z $instcount ] && [[ $instcount != 0 ]] && ! validate_positive_decimal $instcount && + echomult "Invalid instance count.\n $help_text" && exit 1 + + [ -z $instcount ] && instcount=0 + alloc_instcount=$instcount + alloc_ramKB=$(( ramMB * 1000 )) + alloc_swapKB=$(( swapMB * 1000 )) + alloc_diskKB=$(( diskMB * 1000 )) + + ( ( [[ $alloc_instcount -eq 0 ]] || [[ $max_instance_count == $alloc_instcount ]] ) && + ( [[ $alloc_ramKB -eq 0 ]] || [[ $max_mem_kbytes == $alloc_ramKB ]] ) && + ( [[ $alloc_swapKB -eq 0 ]] || [[ $max_swap_kbytes == $alloc_swapKB ]] ) && + ( [[ $alloc_diskKB -eq 0 ]] || [[ $max_storage_kbytes == $alloc_diskKB ]] ) ) && + echomult "Resource configuration values are already configured!\n" && exit 0 + + echomult "Using allocation" + [[ $alloc_ramKB -gt 0 ]] && echomult "$(GB $alloc_ramKB) memory" + [[ $alloc_swapKB -gt 0 ]] && echomult "$(GB $alloc_swapKB) Swap" + [[ $alloc_diskKB -gt 0 ]] && echomult "$(GB $alloc_diskKB) disk space" + [[ $alloc_instcount -gt 0 ]] && echomult "Distributed among $alloc_instcount contract instances" + + update_sashi=1 + [[ $alloc_instcount -gt 0 ]] && update_mb=1 + + elif [ "$sub_mode" == "leaseamt" ] ; then + + local amount=${2} # Contract instance lease amount in EVRs. + [ -z $amount ] && echomult "Your current lease amount is: $cfg_lease_amount EVRs.\n" && exit 0 + + + ! validate_positive_decimal $amount && + echomult "Invalid lease amount.\n Usage: evernode config leaseamt | evernode config leaseamt \n" && + exit 1 + lease_amount=$amount + [[ $cfg_lease_amount == $lease_amount ]] && echomult "Lease amount is already configured!\n" && exit 0 + + echomult "Using lease amount $lease_amount EVRs." + + update_mb=1 + + elif [ "$sub_mode" == "rippled" ] ; then + + local server=${2} # Rippled server URL + [ -z $server ] && echomult "Your current rippled server is: $cfg_rippled_server\n" && exit 0 + + ! validate_rippled_url $server && + echomult "\nUsage: evernode config rippled | evernode config rippled \n" && + exit 1 + rippled_server=$server + [[ $cfg_rippled_server == $rippled_server ]] && echomult "Rippled server is already configured!\n" && exit 0 + + echomult "Using the rippled address '$rippled_server'." + + update_mb=1 + + elif [ "$sub_mode" == "email" ] ; then + + local email_address=${2} # Email address + + local cfg_host_address=$(jq -r '.xrpl.address' $mbconfig) + + local mbsecretconfig="$MB_XRPL_DATA/secret.cfg" + local cfg_host_secret=$(jq -r '.xrpl.secret' $mbsecretconfig) + + [ ! -z $email_address ] && ! validate_email_address $email_address && + echomult "\nUsage: evernode config email | evernode config email \n" && + exit 1 + + # Get info of the host. + local host_info=$(sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN hostinfo) || exit 1 + local cur_email_address=$(echo $host_info | jq -r '.email') + + [ -z $email_address ] && echomult "Your current email address is: $cur_email_address\n" && exit 0 + + [[ $cur_email_address == $email_address ]] && echomult "Email address is already configured!\n" && exit 0 + + echomult "Using the email address '$email_address'." + + # If certbot installed, Sashimono might have been setup with letsencrypt certificates. + if command -v certbot &>/dev/null ; then + local inet_addr=$(jq -r '.hp.host_address' $saconfig) + + local key_file="/etc/letsencrypt/live/$inet_addr/privkey.pem" + local cert_file="/etc/letsencrypt/live/$inet_addr/fullchain.pem" + local renewed_key_file="$RENEWED_LINEAGE/privkey.pem" + local sashimono_key_file="$SASHIMONO_DATA/contract_template/cfg/tlskey.pem" + + # If sashimono containes the letsencrypt certificates, Update them with new email. + if ( [ -f $key_file ] && cmp -s $key_file $sashimono_key_file ) || ( [ -f $renewed_key_file ] && cmp -s $renewed_key_file $sashimono_key_file ) ; then + + # Get the current registration email if there's any. + local lenc_acc_email=$(certbot show_account 2>/dev/null | grep "Email contact:" | cut -d ':' -f2 | sed 's/ *//g') + + # If the emails are different, we need to update the letsencrypt email. + if [[ $lenc_acc_email != $email_address ]]; then + # If there are other certificates from this letsencrypt account, + # Complain that sashimono can't update the email since this account is used by other certificates. + local count=$(certbot certificates 2>/dev/null | grep "Certificate Name" | grep -v -c "$inet_addr") + [ $count -gt 0 ] && + echomult "Existing letsencrypt account with $lenc_acc_email has other certificates which are related to sashimono.\n + So letsencrypt email cannot be changed, Please use the same email or update the letsencrypt email with certbot." && + return 1 + + ! certbot -n update_account -m $email_address && + echo "Could not update the letsencrypt email." && return 1 + fi + + fi + fi + + # Send update meassage to the registry. + ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN update $email_address && + echo "Could not update host info." && return 1 + + # We do not need to restart services for email update. + echomult "\nSuccessfully changed the email address!\n" && exit 0 + + elif [ "$sub_mode" == "instance" ] ; then + local attribute=${2} + + if [ "$attribute" == "ipv6" ] ; then + ([ "$cfg_ipv6_subnet" != null ] && [ "$cfg_ipv6_net_interface" != null ]) && + echomult "You have already enabled IPv6 for instance outbound communication. + \n Network Interface: $cfg_ipv6_net_interface + \n Subnet: $cfg_ipv6_subnet" && + ! confirm "\nDo you want to go for a reconfiguration?" && return 0 + + if ( [[ $occupied_instance_count -gt 0 ]] ); then + echomult "Could not proceed the reconfiguration as there are occupied instances." && exit 1 + fi + + set_ipv6_subnet + if [[ "$ipv6_subnet" == "-" || "$ipv6_net_interface" == "-" ]]; then + echo -e "Could not proceed with provided details." && exit 1 + fi + + echo -e "Using $ipv6_subnet IPv6 subnet on $ipv6_net_interface for contract instances.\n" + update_mb=1 + + else + echomult "Invalid arguments.\n Usage: evernode config instance [ipv6]\n" && exit 1 + fi + + else + echomult "Invalid arguments.\n Usage: evernode config [resources|leaseamt|rippled|email|instance] [arguments]\n" && exit 1 + fi + + local mb_user_id=$(id -u "$MB_XRPL_USER") + local mb_user_runtime_dir="/run/user/$mb_user_id" + local has_error=0 + + echomult "\nStarting the reconfiguration...\n" + + # Stop the message board service. + echomult "Stopping the message board..." + sudo -u "$MB_XRPL_USER" XDG_RUNTIME_DIR="$mb_user_runtime_dir" systemctl --user stop $MB_XRPL_SERVICE + + # Stop the sashimono service. + if [ $update_sashi == 1 ] ; then + echomult "Stopping the sashimono..." + systemctl stop $SASHIMONO_SERVICE + + ! reconfig_sashi && has_error=1 + + echomult "Starting the sashimono..." + systemctl start $SASHIMONO_SERVICE + fi + + if [ $has_error == 0 ] && [ $update_mb == 1 ] ; then + ! reconfig_mb && has_error=1 + fi + + echomult "Starting the message board..." + sudo -u "$MB_XRPL_USER" XDG_RUNTIME_DIR="$mb_user_runtime_dir" systemctl --user start $MB_XRPL_SERVICE + + [ $has_error == 1 ] && echomult "\nChanging the configuration exited with an error.\n" && exit 1 + + echomult "\nSuccessfully changed the configuration!\n" +} + +function delete_instance() +{ + [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 + + instance_name=$1 + echo "Deleting instance $instance_name" + ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN delete $instance_name && + echo "There was an error in deleting the instance." && exit 1 + + # Restart the message board to update the instance count + local mb_user_id=$(id -u "$MB_XRPL_USER") + local mb_user_runtime_dir="/run/user/$mb_user_id" + + sudo -u "$MB_XRPL_USER" XDG_RUNTIME_DIR="$mb_user_runtime_dir" systemctl --user restart $MB_XRPL_SERVICE + + echo "Instance deletion completed." +} + +# Begin setup execution flow -------------------- + +if [ "$mode" == "install" ]; then + + if ! $interactive ; then + inetaddr=${3} # IP or DNS address. + init_peer_port=${4} # Starting peer port for instances. + init_user_port=${5} # Starting user port for instances. + countrycode=${6} # 2-letter country code. + alloc_cpu=${7} # CPU microsec to allocate for contract instances (max 1000000). + alloc_ramKB=${8} # Memory to allocate for contract instances. + alloc_swapKB=${9} # Swap to allocate for contract instances. + alloc_diskKB=${10} # Disk space to allocate for contract instances. + alloc_instcount=${11} # Total contract instance count. + lease_amount=${12} # Contract instance lease amount in EVRs. + rippled_server=${13} # Rippled server URL + xrpl_account_address=${14} # XRPL account address. + xrpl_account_secret=${15} # XRPL account secret. + email_address=${16} # User email address + tls_key_file=${17} # File path to the tls private key. + tls_cert_file=${18} # File path to the tls certificate. + tls_cabundle_file=${19} # File path to the tls ca bundle. + ipv6_subnet=${20} # ipv6 subnet to be used for ipv6 instance address assignment. + ipv6_net_interface=${21} # ipv6 bound network interface to be used for outbound communication. + fi + + $interactive && ! confirm "This will install Sashimono, Evernode's contract instance management software, + and register your system as an $evernode host. + \nMake sure your system does not currently contain any other workloads important + to you since we will be making modifications to your system configuration. + \n\nContinue?" && exit 1 + + check_sys_req + check_prereq + + + # Display licence file and ask for concent. + printf "\n*****************************************************************************************************\n\n" + curl --silent $licence_url | cat + printf "\n\n*****************************************************************************************************\n" + $interactive && ! confirm "\nDo you accept the terms of the licence agreement?" && exit 1 + + init_setup_helpers + + if [ "$NO_MB" == "" ]; then + set_rippled_server + echo -e "Using Rippled server '$rippled_server'.\n" + set_host_xrpl_account + echo -e "Using xrpl account $xrpl_account_address with the specified secret.\n" + fi + + set_email_address + echo -e "Using the contact email address '$email_address'.\n" + + set_inet_addr + echo -e "Using '$inetaddr' as host internet address.\n" + + set_country_code + echo -e "Using '$countrycode' as country code.\n" + + set_ipv6_subnet + [ "$ipv6_subnet" != "-" ] && [ "$ipv6_net_interface" != "-" ] && echo -e "Using $ipv6_subnet IPv6 subnet on $ipv6_net_interface for contract instances.\n" + + set_cgrules_svc + echo -e "Using '$cgrulesengd_service' as cgroups rules engine service.\n" + + set_instance_alloc + echo -e "Using allocation $(GB $alloc_ramKB) memory, $(GB $alloc_swapKB) Swap, $(GB $alloc_diskKB) disk space, distributed among $alloc_instcount contract instances.\n" + + set_init_ports + echo -e "Using peer port range $init_peer_port-$((init_peer_port + alloc_instcount)) and user port range $init_user_port-$((init_user_port + alloc_instcount))).\n" + + if [ "$NO_MB" == "" ]; then + set_lease_amount + echo -e "Lease amount set as $lease_amount EVRs per Moment.\n" + fi + + $interactive && ! confirm "\n\nSetup will now begin the installation. Continue?" && exit 1 + + echo "Starting installation..." + install_evernode 0 + + rm -r $setup_helper_dir >/dev/null 2>&1 + + echomult "Installation successful! Installation log can be found at $logfile + \n\nYour system is now registered on $evernode. You can check your system status with 'evernode status' command." + +elif [ "$mode" == "uninstall" ]; then + + # echomult "\nWARNING! Uninstalling will deregister your host from $evernode and you will LOSE YOUR XRPL ACCOUNT credentials + # stored in '$MB_XRPL_DATA/mb-xrpl.cfg' and '$MB_XRPL_DATA/secret.cfg'. This is irreversible. Make sure you have your account address and + # secret elsewhere before proceeding.\n" + + # $interactive && ! confirm "\nHave you read above warning and backed up your account credentials?" && exit 1 + $interactive && ! confirm "\nAre you sure you want to uninstall $evernode?" && exit 1 + + # Check contract condtion. + check_exisiting_contracts 0 + + # Force uninstall on quiet mode. + $interactive && uninstall_evernode 0 || uninstall_evernode 0 -f + echo "Uninstallation complete!" + +elif [ "$mode" == "transfer" ]; then + # If evernode is not installed download setup helpers and call for transfer. + if $installed ; then + $interactive && ! confirm "\nThis will uninstall and deregister this host from $evernode + while allowing you to transfer the registration to a preferred transferee. + \n\nAre you sure you want to transfer $evernode registration from this host?" && exit 1 + + if ! $interactive ; then + transferee_address=${3} # Address of the transferee. + fi + + # Set transferee based on the user input. + set_transferee_address + + # Check contract condtion. + check_exisiting_contracts 0 + + # Initiate transferring. + init_evernode_transfer + + # Execute oftware uninstallation (Force uninstall on quiet mode). + $interactive && uninstall_evernode 0 || uninstall_evernode 0 -f + + else + if ! $interactive ; then + xrpl_account_address=${3} # XRPL account address. + xrpl_account_secret=${4} # XRPL account secret. + transferee_address=${5} # Address of the transferee. + rippled_server=${6} # Rippled server URL + fi + + init_setup_helpers + + # Set rippled server based on the user input. + set_rippled_server + echo -e "Using Rippled server '$rippled_server'.\n" + + # Set host account based on the user input. + set_host_xrpl_account "transfer" + + # Set transferee based on the user input. + set_transferee_address + + $interactive && ! confirm "\nThis will deregister $xrpl_account_address from $evernode + while allowing you to transfer the registration to $([ -z $transferee_address ] && echo "same account" || echo "$transferee_address"). + \n\nAre you sure you want to transfer $evernode registration?" && exit 1 + + # Execute transfer from js helper. + exec_jshelper transfer $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_account_address $xrpl_account_secret $transferee_address + + rm -r $setup_helper_dir >/dev/null 2>&1 + fi + + echo "Transfer process was sucessfully initiated. You can now install and register $evernode using the account $transferee_address." + +elif [ "$mode" == "status" ]; then + reg_info + +elif [ "$mode" == "list" ]; then + sashi list + +elif [ "$mode" == "update" ]; then + update_evernode + +elif [ "$mode" == "log" ]; then + create_log + +elif [ "$mode" == "applyssl" ]; then + apply_ssl $2 $3 $4 + +elif [ "$mode" == "config" ]; then + config $2 $3 $4 $5 $6 + +elif [ "$mode" == "delete" ]; then + [ -z "$2" ] && echomult "A contract instance name must be specified (see 'evernode list').\n Usage: evernode delete " && exit 1 + + delete_instance "$2" + +elif [ "$mode" == "governance" ]; then + [[ "$2" == "" || "$2" == "help" ]] && echomult "Governance management tool + \nSupported commands: + \npropose [hashFile] [shortName] - Propose new governance candidate. + \nwithdraw [candidateId] - Withdraw proposed governance candidate. + \nvote [candidateId] - Vote for a governance candidate. + \nunvote [candidateId] - Remove vote from voted governance candidate. + \nstatus - Get governance info of this host. + \nreport [dudHostAddress] - Report a dud host. + \nhelp - Print help." && exit 0 + ! MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN ${*:1} && exit 1 + +fi + +[ "$mode" != "uninstall" ] && check_installer_pending_finish + +exit 0 + +# surrounding braces are needed make the whole script to be buffered on client before execution. +} diff --git a/installer/setup.sh b/installer/setup.sh index 5e5aa33..e1afa40 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -19,18 +19,38 @@ max_non_ipv6_instances=5 max_ipv6_prefix_len=112 evernode_alias=/usr/bin/evernode log_dir=/tmp/evernode-beta -cloud_storage="https://stevernode.blob.core.windows.net/evernode-dev-v3-a86733dc-c0fc-4b1f-97cf-2071ae9c5bee" -setup_script_url="$cloud_storage/setup.sh" -installer_url="$cloud_storage/installer.tar.gz" -licence_url="$cloud_storage/licence.txt" -nodejs_url="$cloud_storage/node" -jshelper_url="$cloud_storage/setup-jshelper.tar.gz" + +repo_owner="EvernodeXRPL" +repo_name="evernode-resources" +desired_branch="main" + +latest_version_endpoint="https://api.github.com/repos/$repo_owner/$repo_name/releases/latest" +latest_version_data=$(curl -s "$latest_version_endpoint") +latest_version=$(echo "$latest_version_data" | jq -r '.tag_name') +if [ -z "$latest_version" ]|| [ "$latest_version" = "null" ]; then + echo "Failed to retrieve latest version data." + exit 1 +fi + +# Prepare resources URLs +resource_storage="https://github.com/$repo_owner/$repo_name/releases/download/$latest_version" +licence_url="https://raw.githubusercontent.com/$repo_owner/$repo_name/$desired_branch/installer/licence.txt" +config_url="https://raw.githubusercontent.com/$repo_owner/$repo_name/$desired_branch/definitions/definitions.json" +setup_script_url="$resource_storage/setup.sh" +installer_url="$resource_storage/installer.tar.gz" +jshelper_url="$resource_storage/setup-jshelper.tar.gz" + installer_version_timestamp_file="installer.version.timestamp" -setup_version_timestamp_file="setup.version.timestamp" default_rippled_server="wss://hooks-testnet-v3.xrpl-labs.com" setup_helper_dir="/tmp/evernode-setup-helpers" -nodejs_util_bin="$setup_helper_dir/node" +nodejs_util_bin="/usr/bin/node" jshelper_bin="$setup_helper_dir/jshelper/index.js" +config_json_path="$setup_helper_dir/configuration.json" +operation="register" +min_xrp_amount_per_month=25 +spinner=( '|' '/' '-' '\'); +xrpl_address="-" +xrpl_secret="-" # export vars used by Sashimono installer. export USER_BIN=/usr/bin @@ -49,9 +69,7 @@ export MB_XRPL_USER="sashimbxrpl" export CG_SUFFIX="-cg" export EVERNODE_AUTO_UPDATE_SERVICE="evernode-auto-update" -# TODO: Verify if the correct Governor address is present in the DEV/BETA envs. -export EVERNODE_GOVERNOR_ADDRESS="raVhw4Q8FQr296jdaDLDfZ4JDhh7tFG7SF" -export MIN_EVR_BALANCE=5120 +export NETWORK="${NETWORK:-devnet}" # Private docker registry (not used for now) export DOCKER_REGISTRY_USER="sashidockerreg" @@ -61,6 +79,14 @@ export DOCKER_REGISTRY_PORT=0 # (we execute as the user who launched this script as sudo) noroot_user=${SUDO_USER:-$(whoami)} +# Default key path is set to a path in MB_XRPL_USER home +default_key_filepath="/home/$MB_XRPL_USER/.evernode-host/.host-account-secret.key" + +# Backed up secret location. +# Used to restore secret related to a previous installation attempt +secret_backup_location="/root/.evernode/.host-account-secret.key" + + # Helper to print multi line text. # (When passed as a parameter, bash auto strips spaces and indentation which is what we want) function echomult() { @@ -68,26 +94,64 @@ function echomult() { } function confirm() { - echo -en $1" [Y/n] " + local prompt=$1 + local defaultChoice=${2:-y} #Default choice is set to 'y' if $2 parameter is not provided. + + local choiceDisplay="[Y/n]" + if [ "$defaultChoice" == "n" ]; then + choiceDisplay="[y/N]" + fi + + echo -en $prompt $choiceDisplay local yn="" read yn /dev/null; then + echomult "\nChecking initial level pre-requisites..." + + if ! command -v node &>/dev/null; then + echo "Installing nodejs..." + install_nodejs_utility >/dev/null + else version=$(node -v | cut -d '.' -f1) version=${version:1} if [[ $version -lt 16 ]]; then @@ -180,6 +260,12 @@ function check_prereq() { echo "host command not found. Installing.." apt-get -y install bind9-host >/dev/null fi + + # Check qrencode command is installed. + if ! command -v qrencode &>/dev/null; then + stage "qrencode command not found. Installing.." + apt-get install -y qrencode >/dev/null + fi } function check_sys_req() { @@ -190,7 +276,9 @@ function check_sys_req() { diskKB=$(df | grep -w /home | head -1 | awk '{print $4}') [ -z "$diskKB" ] && diskKB=$(df | grep -w / | head -1 | awk '{print $4}') - [ "$SKIP_SYSREQ" == "1" ] && echo "System requirements check skipped." && return 0 + # Skip system requirement check in non-production environments if SKIP_SYSREQ=1. + ([ "$NETWORK" != "mainnet" ] && [ "$SKIP_SYSREQ" == "1" ]) && echo "System requirements check skipped." && return 0 + local proc1=$(ps --no-headers -o comm 1) if [ "$proc1" != "systemd" ]; then @@ -217,6 +305,25 @@ function check_sys_req() { fi } +function set_environment_configs() { + + sudo -u $noroot_user mkdir -p $setup_helper_dir + echomult "\nDownloading Environment configuration...\n" + sudo -u $noroot_user curl $config_url --output $config_json_path + + # Network config selection. + + echomult "\nChecking Evernode $NETWORK environment details..." + + if ! jq -e ".${NETWORK}" "$config_json_path" >/dev/null 2>&1; then + echomult "Sorry the specified environment has not been configured yet..\n" && exit 1 + fi + + export EVERNODE_GOVERNOR_ADDRESS=${OVERRIDE_EVERNODE_GOVERNOR_ADDRESS:-$(jq -r ".$NETWORK.governorAddress" $config_json_path)} + default_rippled_server=$(jq -r ".$NETWORK.rippledServer" $config_json_path) + +} + function init_setup_helpers() { echo "Downloading setup support files..." @@ -225,13 +332,10 @@ function init_setup_helpers() { rm -r $jshelper_dir >/dev/null 2>&1 sudo -u $noroot_user mkdir -p $jshelper_dir - [ ! -f "$nodejs_util_bin" ] && sudo -u $noroot_user curl $nodejs_url --output $nodejs_util_bin - [ ! -f "$nodejs_util_bin" ] && echo "Could not download nodejs for setup checks." && exit 1 - chmod +x $nodejs_util_bin if [ ! -f "$jshelper_bin" ]; then pushd $jshelper_dir >/dev/null 2>&1 - sudo -u $noroot_user curl $jshelper_url --output jshelper.tar.gz + sudo -u $noroot_user curl -L $jshelper_url --output jshelper.tar.gz sudo -u $noroot_user tar zxf jshelper.tar.gz --strip-components=1 rm jshelper.tar.gz popd >/dev/null 2>&1 @@ -256,6 +360,22 @@ function exec_jshelper() { rm $resp_file && return 1 } +function exec_jshelper_root() { + + # Create fifo file to read response data from the helper script. + local resp_file=$setup_helper_dir/helper_fifo + [ -p $resp_file ] || mkfifo $resp_file + + # Execute js helper asynchronously while collecting response to fifo file. + RESPFILE=$resp_file $nodejs_util_bin $jshelper_bin "$@" >/dev/null 2>&1 & + local pid=$! + local result=$(cat $resp_file) && [ "$result" != "-" ] && echo $result + + # Wait for js helper to exit and reflect the error exit code in this function return. + wait $pid && [ $? -eq 0 ] && rm $resp_file && return 0 + rm $resp_file && return 1 +} + function resolve_filepath() { # name reference the variable name provided as first argument. local -n filepath=$1 @@ -297,7 +417,25 @@ function set_domain_certs() { } function validate_inet_addr_domain() { - host $inetaddr >/dev/null 2>&1 && return 0 + if host $inetaddr >/dev/null 2>&1 ; then + local port="80" + echo "Verifying domain $inetaddr on port $port..." + local domain_result=$(exec_jshelper_root validate-domain $inetaddr $port) + [[ "$domain_result" == "ok" ]] && echo "Domain verification successful." && return 0 + + if [ "$domain_result" == "listen_error" ]; then + echomult "Could not initiate domain verification. It's likely that port $port is already in use by another application.\n + It's recommended that you abandon the setup and correct this. You should consider continuing only if you are an advanced user + who knows what they are doing, and is going to provide your own SSL certificates." + confirm "Do you want to abandon the setup (recommended)?" && echo "Setup abandoned." && exit 1 + echo "Continuing with unverified domain $inetaddr" && return 0 + fi + + [[ "$domain_result" == "domain_error" ]] && + echo "Domain verification for $inetaddr failed. Please make sure that this host is reachable via $inetaddr" + fi + + # Reaching this point means some error has occured. So we clear the inetaddress to allow to try again. inetaddr="" && return 1 } @@ -345,38 +483,36 @@ function validate_email_address() { function set_inet_addr() { - if $interactive && [ "$NO_DOMAIN" == "" ] ; then + # TODO : Remove NO_DOMAIN usage (Kept for local testing) + if [ "$NO_DOMAIN" == "" ] ; then echo "" while [ -z "$inetaddr" ]; do - read -p "Please specify the domain name that this host is reachable at: " inetaddr Usage: generate_qrcode " + return 1 + fi + local input_string="$1" + qrencode -s 1 -l L -t UTF8 "$input_string" +} + +function generate_and_save_keyfile() { + + local account_json=$(exec_jshelper generate-account) + xrpl_address=$(jq -r '.address' <<< "$account_json") + xrpl_secret=$(jq -r '.secret' <<< "$account_json") + + if [ "$#" -ne 1 ]; then + echomult "Error: Please provide the full path of the secret file." + return 1 + fi + + key_file_path="$1" + + key_dir=$(dirname "$key_file_path") + if [ ! -d "$key_dir" ]; then + mkdir -p "$key_dir" + fi + + if [ -e "$key_file_path" ]; then + if ! confirm "The file '$key_file_path' already exists. Do you want to override it?"; then + existing_secret=$(jq -r '.xrpl.secret' "$key_file_path" 2>/dev/null) + if [ "$existing_secret" != "null" ] && [ "$existing_secret" != "-" ]; then + account_json=$(exec_jshelper generate-account $existing_secret) + xrpl_address=$(jq -r '.address' <<< "$account_json") + xrpl_secret=$(jq -r '.secret' <<< "$account_json") + echomult "Retrived account details via secret." + return 0 + else + echomult "Error: Existing secret file does not have the expected format." + return 1 + fi + fi + fi + + if [ "$key_file_path" == "$default_key_filepath" ]; then + parent_directory=$(dirname "$key_file_path") + chown -R $MB_XRPL_USER: "$parent_directory" + chmod -R 700 "$parent_directory" + fi + + echo "{ \"xrpl\": { \"secret\": \"$xrpl_secret\" } }" > "$key_file_path" + chmod 600 "$key_file_path" + echomult "Key file saved successfully at $key_file_path" + + chown $MB_XRPL_USER: $key_file_path + + return 0 +} function set_host_xrpl_account() { local account_validate_criteria="register" + local required_balance=0 [ ! -z $1 ] && account_validate_criteria=$1 - if $interactive; then - [ "$account_validate_criteria" == "register" ] && - echomult "In order to register in Evernode you need to have an XRPL account with sufficient Ever (EVR) balance.\n" - local xrpl_address="" - local xrpl_secret="" - while true ; do + local reg_fee=$(exec_jshelper access-evernode-cfg $rippled_server $EVERNODE_GOVERNOR_ADDRESS hostRegFee) - read -p "Specify the XRPL account address: " xrpl_address /dev/null) + if [ "$existing_secret" != "null" ] && [ "$existing_secret" != "-" ]; then + account_json=$(exec_jshelper generate-account $existing_secret) + xrpl_address=$(jq -r '.address' <<< "$account_json") + xrpl_secret=$(jq -r '.secret' <<< "$account_json") + + key_file_dir=$(dirname "$key_file_path") + if [ ! -d "$key_file_dir" ]; then + mkdir -p "$key_file_dir" + fi + + # Modify the permissions accordingly + chown $MB_XRPL_USER: $key_file_path && \ + chmod 600 $key_file_path && \ + echomult "Retrived account details via the backed-up secret." || (echomult "Error occurred in secret restoring." && exit 1) + else + echomult "Error: Backup secret file format does not support." && exit 1 + fi + else + + echomult "Generating new keypair for the host...\n" + generate_and_save_keyfile "$key_file_path" + fi + + echomult "Your host account with the address $xrpl_address has been generated on Xahau $NETWORK. + \nThe secret key of the account is located at $key_file_path. + \n\nThis is the account that will represent this host on the Evernode host registry. You need to load up the account with following funds in order to continue with the installation. + \n1. At least $min_xrp_amount_per_month XAH (Xahau XRP) to cover regular transaction fees for first month. + \n2. At least $reg_fee EVR to cover Evernode registration fee. + \n\nYou can scan the following QR code in your wallet app to send funds:\n" + + generate_qrcode "$xrpl_address" + + required_balance=$min_xrp_amount_per_month + while true ; do + wait_call "exec_jshelper check-balance $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address NATIVE $required_balance" "Thank you. [OUTPUT] XAH balance is there in your host account." \ + && break + confirm "\nDo you want to re-check the balance?\nPressing 'n' would terminate the installation." || exit 1 + done + + echomult "\nPreparing account with EVR trust-line..." + while true ; do + wait_call "exec_jshelper prepare-host $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address $xrpl_secret $inetaddr" "Account preparation is successfull." && break + confirm "\nDo you want to re-try account preparation?\nPressing 'n' would terminate the installation." || exit 1 + done + + echomult "\n\nIn order to register in Evernode you need to have $reg_fee EVR balance in your host account. Please deposit the required registration fee in EVRs. + \nYou can scan the following QR code in your wallet app to send funds:" + + required_balance=$reg_fee + while true ; do + wait_call "exec_jshelper check-balance $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address ISSUED $required_balance" "Thank you. [OUTPUT] EVR balance is there in your host account." \ + && break + confirm "\nDo you want to re-check the balance?\nPressing 'n' would terminate the installation." || exit 1 + done + + + elif [ "$account_validate_criteria" == "transfer" ] || [ "$account_validate_criteria" == "re-register" ]; then + + if [ "$account_validate_criteria" == "re-register" ]; then + account_validate_criteria="register" + fi + + while true ; do + read -ep "Specify the XRPL account address: " xrpl_address /etc/systemd/system/$EVERNODE_AUTO_UPDATE_SERVICE.service + + # Create a timer for the service (every two hours). + echo "[Unit] +Description=Timer for the Evernode auto-update. +# Allow manual starts +RefuseManualStart=no +# Allow manual stops +RefuseManualStop=no +[Timer] +Unit=$EVERNODE_AUTO_UPDATE_SERVICE.service +OnCalendar=0/12:00:00 +# Execute job if it missed a run due to machine being off +Persistent=true +# To prevent rush time, adding 2 hours delay +RandomizedDelaySec=7200 +[Install] +WantedBy=timers.target" >/etc/systemd/system/$EVERNODE_AUTO_UPDATE_SERVICE.timer + + # Reload the systemd daemon. + systemctl daemon-reload + + echo "Enabling Evernode auto update service..." + systemctl enable $EVERNODE_AUTO_UPDATE_SERVICE.service + + echo "Enabling Evernode auto update timer..." + systemctl enable $EVERNODE_AUTO_UPDATE_SERVICE.timer + echo "Starting Evernode auto update timer..." + systemctl start $EVERNODE_AUTO_UPDATE_SERVICE.timer +} + +function remove_evernode_auto_updater() { + [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 + enable_auto_update=false + + echo "Removing Evernode auto update timer..." + systemctl stop $EVERNODE_AUTO_UPDATE_SERVICE.timer + systemctl disable $EVERNODE_AUTO_UPDATE_SERVICE.timer + service_path="/etc/systemd/system/$EVERNODE_AUTO_UPDATE_SERVICE.timer" + rm -f $service_path + + echo "Removing Evernode auto update service..." + systemctl stop $EVERNODE_AUTO_UPDATE_SERVICE.service + systemctl disable $EVERNODE_AUTO_UPDATE_SERVICE.service + service_path="/etc/systemd/system/$EVERNODE_AUTO_UPDATE_SERVICE.service" + rm -f $service_path + + # Reload the systemd daemon. + systemctl daemon-reload } function install_evernode() { local upgrade=$1 # Get installer version (timestamp). We use this later to check for Evernode software updates. - local installer_version_timestamp=$(online_version_timestamp $installer_url) + local installer_version_timestamp=$(online_version_timestamp) [ -z "$installer_version_timestamp" ] && echo "Online installer not found." && exit 1 - # Get setup version (timestamp). - local setup_version_timestamp=$(online_version_timestamp $setup_script_url) local tmp=$(mktemp -d) cd $tmp - curl --silent $installer_url --output installer.tgz + curl --silent -L $installer_url --output installer.tgz tar zxf $tmp/installer.tgz --strip-components=1 rm installer.tgz @@ -734,7 +1080,7 @@ function install_evernode() { logfile="$log_dir/installer-$(date +%s).log" if [ "$upgrade" == "0" ] ; then - echo "Installing prerequisites..." + echo "Installing other prerequisites..." ! ./prereq.sh $cgrulesengd_service 2>&1 \ | tee -a $logfile | stdbuf --output=L grep "STAGE" | cut -d ' ' -f 2- && install_failure fi @@ -750,23 +1096,29 @@ function install_evernode() { echo "Installing Sashimono..." init_setup_helpers - registry_address=$(exec_jshelper access-evernode-cfg $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_account_address registryAddress) + registry_address=$(exec_jshelper access-evernode-cfg $rippled_server $EVERNODE_GOVERNOR_ADDRESS registryAddress) # Filter logs with STAGE prefix and ommit the prefix when echoing. # If STAGE log contains -p arg, move the cursor to previous log line and overwrite the log. - ! UPGRADE=$upgrade EVERNODE_REGISTRY_ADDRESS=$registry_address ./sashimono-install.sh $inetaddr $init_peer_port $init_user_port $countrycode $alloc_instcount \ - $alloc_cpu $alloc_ramKB $alloc_swapKB $alloc_diskKB $lease_amount $rippled_server $xrpl_account_address $xrpl_account_secret $email_address \ + ! UPGRADE=$upgrade EVERNODE_REGISTRY_ADDRESS=$registry_address OPERATION=$operation ./sashimono-install.sh $inetaddr $init_peer_port $init_user_port $countrycode $alloc_instcount \ + $alloc_cpu $alloc_ramKB $alloc_swapKB $alloc_diskKB $lease_amount $rippled_server $xrpl_account_address $xrpl_account_secret_path $email_address \ $tls_key_file $tls_cert_file $tls_cabundle_file $description $ipv6_subnet $ipv6_net_interface 2>&1 \ | tee -a $logfile | stdbuf --output=L grep "STAGE\|ERROR" \ | while read line ; do [[ $line =~ ^STAGE[[:space:]]-p(.*)$ ]] && echo -e \\e[1A\\e[K"${line:9}" || echo ${line:6} ; done \ && remove_evernode_alias && install_failure + + # Enable the Evernode Auto Updater Service. + if [ "$enable_auto_update" = true ]; then + stage "Configuring auto updater service" + enable_evernode_auto_updater + fi + set +o pipefail rm -r $tmp # Write the verison timestamp to a file for later updated version comparison. echo $installer_version_timestamp > $SASHIMONO_DATA/$installer_version_timestamp_file - echo $setup_version_timestamp > $SASHIMONO_DATA/$setup_version_timestamp_file } function check_exisiting_contracts() { @@ -796,6 +1148,9 @@ function uninstall_evernode() { if ! $transfer ; then [ "$upgrade" == "0" ] && echo "Uninstalling..." || echo "Uninstalling for upgrade..." ! UPGRADE=$upgrade TRANSFER=0 $SASHIMONO_BIN/sashimono-uninstall.sh $2 && uninstall_failure + + # Remove the Evernode Auto Updater Service. + [ "$upgrade" == "0" ] && systemctl list-unit-files | grep -q $EVERNODE_AUTO_UPDATE_SERVICE.service && remove_evernode_auto_updater else echo "Intiating Transfer..." echo "Uninstalling for transfer..." @@ -808,13 +1163,11 @@ function uninstall_evernode() { function update_evernode() { echo "Checking for updates..." - local latest_installer_script_version=$(online_version_timestamp $installer_url) - local latest_setup_script_version=$(online_version_timestamp $setup_script_url) + local latest_installer_script_version=$(online_version_timestamp) [ -z "$latest_installer_script_version" ] && echo "Could not check for updates. Online installer not found." && exit 1 local current_installer_script_version=$(cat $SASHIMONO_DATA/$installer_version_timestamp_file) - local current_setup_script_version=$(cat $SASHIMONO_DATA/$setup_version_timestamp_file) - [ "$latest_installer_script_version" == "$current_installer_script_version" ] && [ "$latest_setup_script_version" == "$current_setup_script_version" ] && echo "Your $evernode installation is up to date." && exit 0 + [ "$latest_installer_script_version" == "$current_installer_script_version" ] && echo "Your $evernode installation is up to date." && exit 0 echo "New $evernode update available. Setup will re-install $evernode with updated software. Your account and contract instances will be preserved." $interactive && ! confirm "\nDo you want to install the update?" && exit 1 @@ -825,12 +1178,6 @@ function update_evernode() { if [ "$latest_installer_script_version" != "$current_installer_script_version" ] ; then uninstall_evernode 1 install_evernode 1 - elif [ "$latest_setup_script_version" != "$current_setup_script_version" ] ; then - [ -d $log_dir ] || mkdir -p $log_dir - logfile="$log_dir/installer-$(date +%s).log" - remove_evernode_alias - ! create_evernode_alias && echo "Alias creation failed." - echo $latest_setup_script_version > $SASHIMONO_DATA/$setup_version_timestamp_file fi rm -r $setup_helper_dir >/dev/null 2>&1 @@ -887,8 +1234,7 @@ function remove_evernode_alias() { function check_installer_pending_finish() { if [ -f /run/reboot-required.pkgs ] && [ -n "$(grep sashimono /run/reboot-required.pkgs)" ]; then echo "Your system needs to be rebooted in order to complete Sashimono installation." - $interactive && confirm "Reboot now?" && reboot - ! $interactive && echo "Rebooting..." && reboot + confirm "Reboot now?" && reboot return 0 else # If reboot not required, check whether re-login is required in case the setup was run with sudo. @@ -912,7 +1258,7 @@ function reg_info() { local sashimono_mb_xrpl_status=$(sudo -u "$MB_XRPL_USER" XDG_RUNTIME_DIR="$mb_user_runtime_dir" systemctl --user is-active $MB_XRPL_SERVICE) echo "Sashimono agent status: $sashimono_agent_status" echo "Sashimono mb xrpl status: $sashimono_mb_xrpl_status" - echo -e "\nYour account details are stored in $MB_XRPL_DATA/mb-xrpl.cfg and $MB_XRPL_DATA/secret.cfg." + echo -e "\nYour account details are stored in $MB_XRPL_DATA/mb-xrpl.cfg" fi } @@ -1107,9 +1453,6 @@ function config() { local cfg_host_address=$(jq -r '.xrpl.address' $mbconfig) - local mbsecretconfig="$MB_XRPL_DATA/secret.cfg" - local cfg_host_secret=$(jq -r '.xrpl.secret' $mbsecretconfig) - [ ! -z $email_address ] && ! validate_email_address $email_address && echomult "\nUsage: evernode config email | evernode config email \n" && exit 1 @@ -1248,29 +1591,7 @@ function delete_instance() if [ "$mode" == "install" ]; then - if ! $interactive ; then - inetaddr=${3} # IP or DNS address. - init_peer_port=${4} # Starting peer port for instances. - init_user_port=${5} # Starting user port for instances. - countrycode=${6} # 2-letter country code. - alloc_cpu=${7} # CPU microsec to allocate for contract instances (max 1000000). - alloc_ramKB=${8} # Memory to allocate for contract instances. - alloc_swapKB=${9} # Swap to allocate for contract instances. - alloc_diskKB=${10} # Disk space to allocate for contract instances. - alloc_instcount=${11} # Total contract instance count. - lease_amount=${12} # Contract instance lease amount in EVRs. - rippled_server=${13} # Rippled server URL - xrpl_account_address=${14} # XRPL account address. - xrpl_account_secret=${15} # XRPL account secret. - email_address=${16} # User email address - tls_key_file=${17} # File path to the tls private key. - tls_cert_file=${18} # File path to the tls certificate. - tls_cabundle_file=${19} # File path to the tls ca bundle. - ipv6_subnet=${20} # ipv6 subnet to be used for ipv6 instance address assignment. - ipv6_net_interface=${21} # ipv6 bound network interface to be used for outbound communication. - fi - - $interactive && ! confirm "This will install Sashimono, Evernode's contract instance management software, + ! confirm "This will install Sashimono, Evernode's contract instance management software, and register your system as an $evernode host. \nMake sure your system does not currently contain any other workloads important to you since we will be making modifications to your system configuration. @@ -1282,22 +1603,26 @@ if [ "$mode" == "install" ]; then # Display licence file and ask for concent. printf "\n*****************************************************************************************************\n\n" - curl --silent $licence_url | cat + curl -s -L $licence_url | cat printf "\n\n*****************************************************************************************************\n" - $interactive && ! confirm "\nDo you accept the terms of the licence agreement?" && exit 1 + ! confirm "\nDo you accept the terms of the licence agreement?" && exit 1 + + ! confirm "\nAre you performing a fresh Evernode installation? + \nNOTE: Pressing 'n' implies that you are in the process of transferring from a previous installation in $NETWORK." && operation="re-register" + + set_environment_configs init_setup_helpers - if [ "$NO_MB" == "" ]; then + if [ "$NO_MB" == "" ]; then set_rippled_server echo -e "Using Rippled server '$rippled_server'.\n" - set_host_xrpl_account - echo -e "Using xrpl account $xrpl_account_address with the specified secret.\n" fi set_email_address echo -e "Using the contact email address '$email_address'.\n" + # TODO - CHECKPOINT - 01 set_inet_addr echo -e "Using '$inetaddr' as host internet address.\n" @@ -1319,10 +1644,22 @@ if [ "$mode" == "install" ]; then if [ "$NO_MB" == "" ]; then set_lease_amount echo -e "Lease amount set as $lease_amount EVRs per Moment.\n" + + # TODO - CHECKPOINT - 02 + set_host_xrpl_account $operation + echo -e "\nAccount setup is complete." + fi + + set_auto_update + if [ "$enable_auto_update" = true ]; then + echo -e "Auto updater will be enabled." + else + echo -e "Auto updater will be disabled." fi $interactive && ! confirm "\n\nSetup will now begin the installation. Continue?" && exit 1 + # TODO - CHECKPOINT - 03 echo "Starting installation..." install_evernode 0 @@ -1333,29 +1670,43 @@ if [ "$mode" == "install" ]; then elif [ "$mode" == "uninstall" ]; then - # echomult "\nWARNING! Uninstalling will deregister your host from $evernode and you will LOSE YOUR XRPL ACCOUNT credentials - # stored in '$MB_XRPL_DATA/mb-xrpl.cfg' and '$MB_XRPL_DATA/secret.cfg'. This is irreversible. Make sure you have your account address and - # secret elsewhere before proceeding.\n" + ! confirm "\nAre you sure you want to uninstall $evernode?" && exit 1 - # $interactive && ! confirm "\nHave you read above warning and backed up your account credentials?" && exit 1 - $interactive && ! confirm "\nAre you sure you want to uninstall $evernode?" && exit 1 + echomult "\nWARNING! Uninstalling will deregister your host from $evernode and you will LOSE YOUR ACCOUNT address + stored in '$MB_XRPL_DATA/mb-xrpl.cfg' and the secret in the specified path. + \nNOTE: Secret path can be found at '$MB_XRPL_DATA/mb-xrpl.cfg'. + \nThis is irreversible. Make sure you have your account address and + secret elsewhere before proceeding.\n" + + ! confirm "\nHave you read above warning and backed up your account credentials?" && exit 1 # Check contract condtion. check_exisiting_contracts 0 - # Force uninstall on quiet mode. - $interactive && uninstall_evernode 0 || uninstall_evernode 0 -f + # Perform Evernode uninstall + uninstall_evernode 0 echo "Uninstallation complete!" elif [ "$mode" == "transfer" ]; then # If evernode is not installed download setup helpers and call for transfer. if $installed ; then - $interactive && ! confirm "\nThis will uninstall and deregister this host from $evernode - while allowing you to transfer the registration to a preferred transferee. - \n\nAre you sure you want to transfer $evernode registration from this host?" && exit 1 if ! $interactive ; then transferee_address=${3} # Address of the transferee. + else + + ! confirm "\nThis will uninstall and deregister this host from $evernode + while allowing you to transfer the registration to a preferred transferee. + \n\nAre you sure you want to transfer $evernode registration from this host?" && exit 1 + + echomult "\nWARNING! By proceeding this you will LOSE YOUR ACCOUNT address + stored in '$MB_XRPL_DATA/mb-xrpl.cfg' and the secret in the specified path. + \nNOTE: Secret path can be found at '$MB_XRPL_DATA/mb-xrpl.cfg'. + \nThis is irreversible. Make sure you have your account address and + secret elsewhere before proceeding.\n" + + ! confirm "\nHave you read above warning and backed up your account credentials?" && exit 1 + fi # Set transferee based on the user input. @@ -1372,10 +1723,10 @@ elif [ "$mode" == "transfer" ]; then else if ! $interactive ; then - xrpl_account_address=${3} # XRPL account address. - xrpl_account_secret=${4} # XRPL account secret. - transferee_address=${5} # Address of the transferee. - rippled_server=${6} # Rippled server URL + xrpl_account_address=${3} # XRPL account address. + xrpl_account_secret=$(<"${4}") # XRPL account secret based on the provided path. + transferee_address=${5} # Address of the transferee. + rippled_server=${6} # Rippled server URL fi init_setup_helpers @@ -1409,6 +1760,9 @@ elif [ "$mode" == "list" ]; then sashi list elif [ "$mode" == "update" ]; then + config_json_path="$SASHIMONO_BIN/evernode-setup-helpers/configuration.json" + export EVERNODE_GOVERNOR_ADDRESS=${OVERRIDE_EVERNODE_GOVERNOR_ADDRESS:-$(jq -r ".$NETWORK.governorAddress" $config_json_path)} + update_evernode elif [ "$mode" == "log" ]; then @@ -1437,6 +1791,17 @@ elif [ "$mode" == "governance" ]; then \nhelp - Print help." && exit 0 ! MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN ${*:1} && exit 1 +elif [ "$mode" == "auto-update" ]; then + if [ "$2" == "enable" ]; then + enable_evernode_auto_updater && exit 0 + elif [ "$2" == "disable" ]; then + remove_evernode_auto_updater && exit 0 + else + echomult "$evernode auto update + \nSupported commands: + \nenable - Enable $evernode auto updater service. + \ndisable - Disable $evernode auto updater service." && exit 1 + fi fi [ "$mode" != "uninstall" ] && check_installer_pending_finish diff --git a/mb-xrpl/app.js b/mb-xrpl/app.js index f407744..b77a49e 100644 --- a/mb-xrpl/app.js +++ b/mb-xrpl/app.js @@ -15,25 +15,26 @@ async function main() { if (process.argv.length >= 3) { if (process.argv.length >= 9 && process.argv[2] === 'new') { const accountAddress = process.argv[3]; - const accountSecret = process.argv[4]; + const accountSecretPath = process.argv[4]; const governorAddress = process.argv[5]; - const domain = process.argv[6]; + // const domain = process.argv[6]; const leaseAmount = process.argv[7]; const rippledServer = process.argv[8]; const ipv6Subnet = (process.argv[9] === '-') ? null : process.argv[9]; const ipv6NetInterface = (process.argv[10] === '-') ? null : process.argv[10]; + const network = process.argv.length > 11 ? process.argv[11] : appenv.NETWORK; const setup = new Setup(); - const acc = await setup.setupHostAccount(accountAddress, accountSecret, rippledServer, governorAddress, domain); - setup.newConfig(acc.address, acc.secret, governorAddress, parseFloat(leaseAmount), rippledServer, ipv6Subnet, ipv6NetInterface); + setup.newConfig(accountAddress, accountSecretPath, governorAddress, parseFloat(leaseAmount), rippledServer, ipv6Subnet, ipv6NetInterface, network); } else if (process.argv.length === 7 && process.argv[2] === 'betagen') { const governorAddress = process.argv[3]; const domain = process.argv[4]; const leaseAmount = process.argv[5]; const rippledServer = process.argv[6]; + const network = process.argv.length > 7 ? process.argv[7] : appenv.NETWORK; const setup = new Setup(); - const acc = await setup.generateBetaHostAccount(rippledServer, governorAddress, domain); - setup.newConfig(acc.address, acc.secret, governorAddress, parseFloat(leaseAmount), rippledServer); + const acc = await setup.generateBetaHostAccount(rippledServer, governorAddress, domain, network); + setup.newConfig(acc.address, acc.secret, governorAddress, parseFloat(leaseAmount), rippledServer, null, null, network); } else if (process.argv.length >= 13 && process.argv[2] === 'register') { await new Setup().register(process.argv[3], parseInt(process.argv[4]), parseInt(process.argv[5]), @@ -77,9 +78,9 @@ async function main() { console.log(`Usage: node index.js - Run message board. node index.js version - Print version. - node index.js new [address] [secret] [governorAddress] [leaseAmount] [rippledServer] [ipv6Subnet] [ipv6Interface] - Create new config files. + node index.js new [address] [secret] [governorAddress] [leaseAmount] [rippledServer] [ipv6Subnet] [ipv6Interface] [network] - Create new config files. node index.js betagen [governorAddress] [domain or ip] [leaseAmount] [rippledServer] - Generate beta host account and populate the configs. - node index.js register [countryCode] [cpuMicroSec] [ramKb] [swapKb] [diskKb] [totalInstanceCount] [description] - Register the host on Evernode. + node index.js register [countryCode] [cpuMicroSec] [ramKb] [swapKb] [diskKb] [totalInstanceCount] [description] [network] - Register the host on Evernode. node index.js transfer [transfereeAddress] - Initiate a transfer. node index.js deregister - Deregister the host from Evernode. node index.js reginfo - Display Evernode registration info. diff --git a/mb-xrpl/lib/appenv.js b/mb-xrpl/lib/appenv.js index 81bfbc9..00a6adc 100644 --- a/mb-xrpl/lib/appenv.js +++ b/mb-xrpl/lib/appenv.js @@ -1,19 +1,18 @@ const process = require('process'); const path = require('path'); +const fs = require('fs'); let appenv = { IS_DEV_MODE: process.env.MB_DEV === "1", FILE_LOG_ENABLED: process.env.MB_FILE_LOG === "1", DATA_DIR: process.env.MB_DATA_DIR || __dirname, FAUCET_URL: process.env.MB_FAUCET_URL || "https://hooks-testnet-v3.xrpl-labs.com/newcreds", - DEFAULT_RIPPLED_SERVER: 'wss://hooks-testnet-v3.xrpl-labs.com', DEFAULT_FULL_HISTORY_NODE: 'wss://hooks-testnet-v3.xrpl-labs.com' // If we migrate to Main NET, this should be configured with the relevant full history Node WebSocket. } appenv = { ...appenv, CONFIG_PATH: appenv.DATA_DIR + '/mb-xrpl.cfg', - SECRET_CONFIG_PATH: appenv.DATA_DIR + '/secret.cfg', GOVERNANCE_CONFIG_PATH: appenv.DATA_DIR + '/governance.cfg', LOG_PATH: appenv.DATA_DIR + '/log/mb-xrpl.log', DB_PATH: appenv.DATA_DIR + '/mb-xrpl.sqlite', @@ -28,9 +27,17 @@ appenv = { ORPHAN_PRUNE_SCHEDULER_INTERVAL_HOURS: 4, SASHIMONO_SCHEDULER_INTERVAL_SECONDS: 2, SASHI_CLI_PATH: appenv.IS_DEV_MODE ? "../build/sashi" : "/usr/bin/sashi", - MB_VERSION: '0.7.2', - TOS_HASH: '757A0237B44D8B2BBB04AE2BAD5813858E0AECD2F0B217075E27E0630BA74314' // This is the sha256 hash of TOS text. + MB_VERSION: '0.8.0', + TOS_HASH: '757A0237B44D8B2BBB04AE2BAD5813858E0AECD2F0B217075E27E0630BA74314', // This is the sha256 hash of TOS text. + NETWORK: 'testnet' } + +const getSecretPath = () => { + return fs.existsSync(appenv.CONFIG_PATH) ? JSON.parse(fs.readFileSync(appenv.CONFIG_PATH).toString()).xrpl.secretPath : ""; +} + +appenv = { ...appenv, SECRET_CONFIG_PATH: getSecretPath() } + Object.freeze(appenv); module.exports = { diff --git a/mb-xrpl/lib/config-helper.js b/mb-xrpl/lib/config-helper.js index bc72faa..938b24c 100644 --- a/mb-xrpl/lib/config-helper.js +++ b/mb-xrpl/lib/config-helper.js @@ -28,15 +28,10 @@ class ConfigHelper { return config; } - static writeConfig(config, configPath, secretConfigPath) { + static writeConfig(config, configPath) { let publicCfg = JSON.parse(JSON.stringify(config)); // Make a copy. So, referenced object won't get changed. - const secretCfg = { - xrpl: { - secret: publicCfg.xrpl.secret - } - } - delete publicCfg.xrpl.secret; - fs.writeFileSync(secretConfigPath, JSON.stringify(secretCfg, null, 2), { mode: 0o600 }); // Set file permission so only current user can read/write. + if ('secret' in publicCfg.xrpl) + delete publicCfg.xrpl.secret; fs.writeFileSync(configPath, JSON.stringify(publicCfg, null, 2), { mode: 0o644 }); // Set file permission so only current user can read/write and others can read. } diff --git a/mb-xrpl/lib/governance-manager.js b/mb-xrpl/lib/governance-manager.js index e591b7b..029ce2f 100644 --- a/mb-xrpl/lib/governance-manager.js +++ b/mb-xrpl/lib/governance-manager.js @@ -4,12 +4,18 @@ const fs = require('fs'); const { appenv } = require('./appenv'); const { ConfigHelper } = require('./config-helper'); -function setEvernodeDefaults(governorAddress, rippledServer, xrplApi) { - evernode.Defaults.set({ - governorAddress: governorAddress, - rippledServer: rippledServer, - xrplApi: xrplApi - }); +async function setEvernodeDefaults(network, governorAddress, rippledServer) { + await evernode.Defaults.useNetwork(network || appenv.NETWORK); + + if (governorAddress) + evernode.Defaults.set({ + governorAddress: governorAddress + }); + + if (rippledServer) + evernode.Defaults.set({ + rippledServer: rippledServer + }); } class GovernanceManager { @@ -181,7 +187,7 @@ class GovernanceManager { // Secret is needed for propose, withdraw, and report in order to send the transaction const sashiMBConfig = ConfigHelper.readConfig(appenv.CONFIG_PATH, (command == 'propose' || command === 'withdraw' || command === 'report') ? appenv.SECRET_CONFIG_PATH : null); - setEvernodeDefaults(sashiMBConfig.xrpl.governorAddress, sashiMBConfig.xrpl.rippledServer); + await setEvernodeDefaults(sashiMBConfig.xrpl.network, sashiMBConfig.xrpl.governorAddress, sashiMBConfig.xrpl.rippledServer); hostClient = new evernode.HostClient(sashiMBConfig.xrpl.address, sashiMBConfig.xrpl.secret); } const mgr = new GovernanceManager(appenv.GOVERNANCE_CONFIG_PATH); diff --git a/mb-xrpl/lib/message-board.js b/mb-xrpl/lib/message-board.js index 8c9ce02..7449a43 100644 --- a/mb-xrpl/lib/message-board.js +++ b/mb-xrpl/lib/message-board.js @@ -52,9 +52,20 @@ class MessageBoard { if (!this.cfg.version || !this.cfg.xrpl.address || !this.cfg.xrpl.secret || !this.cfg.xrpl.governorAddress) throw "Required cfg fields cannot be empty."; - this.xrplApi = new evernode.XrplApi(this.cfg.xrpl.rippledServer); + 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 + }); + + this.xrplApi = new evernode.XrplApi(); evernode.Defaults.set({ - governorAddress: this.cfg.xrpl.governorAddress, xrplApi: this.xrplApi }) await this.xrplApi.connect(); @@ -1076,7 +1087,7 @@ class MessageBoard { } persistConfig() { - ConfigHelper.writeConfig(this.cfg, this.configPath, this.secretConfigPath); + ConfigHelper.writeConfig(this.cfg, this.configPath); } } diff --git a/mb-xrpl/lib/setup.js b/mb-xrpl/lib/setup.js index bf327b7..e69dec2 100644 --- a/mb-xrpl/lib/setup.js +++ b/mb-xrpl/lib/setup.js @@ -8,12 +8,18 @@ const { ConfigHelper } = require('./config-helper'); const { SashiCLI } = require('./sashi-cli'); const { UtilHelper } = require('./util-helper'); -function setEvernodeDefaults(governorAddress, rippledServer, xrplApi = null) { - evernode.Defaults.set({ - governorAddress: governorAddress, - rippledServer: rippledServer || appenv.DEFAULT_RIPPLED_SERVER, - xrplApi: xrplApi - }); +async function setEvernodeDefaults(network, governorAddress, rippledServer) { + await evernode.Defaults.useNetwork(network || appenv.NETWORK); + + if (governorAddress) + evernode.Defaults.set({ + governorAddress: governorAddress + }); + + if (rippledServer) + evernode.Defaults.set({ + rippledServer: rippledServer + }); } class Setup { @@ -54,15 +60,16 @@ class Setup { } #saveConfig(cfg) { - ConfigHelper.writeConfig(cfg, appenv.CONFIG_PATH, appenv.SECRET_CONFIG_PATH); + ConfigHelper.writeConfig(cfg, appenv.CONFIG_PATH); } - newConfig(address = "", secret = "", governorAddress = "", leaseAmount = 0, rippledServer = null, ipv6Subnet = null, ipv6NetInterface = null) { + newConfig(address = "", secretPath = "", governorAddress = "", leaseAmount = 0, rippledServer = null, ipv6Subnet = null, ipv6NetInterface = null, network = "") { const baseConfig = { version: appenv.MB_VERSION, xrpl: { + network: network, address: address, - secret: secret, + secretPath: secretPath, governorAddress: governorAddress, rippledServer: rippledServer || appenv.DEFAULT_RIPPLED_SERVER, leaseAmount: leaseAmount @@ -72,50 +79,9 @@ class Setup { this.#saveConfig(ipv6NetInterface ? { ...baseConfig, networking: { ipv6: { subnet: ipv6Subnet, interface: ipv6NetInterface } } } : baseConfig); } - async setupHostAccount(address, secret, rippledServer, governorAddress, domain) { + async generateBetaHostAccount(rippledServer, governorAddress, domain, network = null) { - setEvernodeDefaults(governorAddress, rippledServer); - - const xrplApi = new evernode.XrplApi(rippledServer); - const acc = new evernode.XrplAccount(address, secret, { xrplApi: xrplApi }); - - // Prepare host account. - { - const hostClient = new evernode.HostClient(acc.address, acc.secret); - await hostClient.connect(); - - console.log(`Preparing host account:${acc.address} (domain:${domain} registry:${hostClient.config.registryAddress})`); - - // Sometimes we may get 'account not found' error from rippled when some servers in the cluster - // haven't still updated the ledger. In such cases, we retry several times before giving up. - { - let attempts = 0; - while (attempts >= 0) { - try { - await hostClient.prepareAccount(domain); - break; - } - catch (err) { - if (err.data?.error === 'actNotFound' && ++attempts <= 5) { - console.log("actNotFound - retrying...") - // Wait and retry. - await new Promise(resolve => setTimeout(resolve, 3000)); - continue; - } - throw err; - } - } - } - - await hostClient.disconnect(); - } - - return acc; - } - - async generateBetaHostAccount(rippledServer, governorAddress, domain) { - - setEvernodeDefaults(governorAddress, rippledServer); + await setEvernodeDefaults(network, governorAddress, rippledServer); const acc = await this.#generateFaucetAccount(); @@ -181,13 +147,15 @@ class Setup { let cpuModelFormatted = cpuModel.replaceAll('_', ' '); const config = this.#getConfig(); const acc = config.xrpl; - setEvernodeDefaults(acc.governorAddress, acc.rippledServer); + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); const hostClient = new evernode.HostClient(acc.address, acc.secret); await hostClient.connect(); // Update the Defaults with "xrplApi" of the client. - setEvernodeDefaults(acc.governorAddress, acc.rippledServer, hostClient.xrplApi); + evernode.Defaults.set({ + xrplApi: hostClient.xrplApi + }); const isAReReg = await hostClient.isTransferee(); const evrBalance = await hostClient.getEVRBalance(); @@ -230,13 +198,15 @@ class Setup { async deregister() { console.log("Deregistering host..."); const acc = this.#getConfig().xrpl; - setEvernodeDefaults(acc.governorAddress, acc.rippledServer); + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); const hostClient = new evernode.HostClient(acc.address, acc.secret); await hostClient.connect(); // Update the Defaults with "xrplApi" of the client. - setEvernodeDefaults(acc.governorAddress, acc.rippledServer, hostClient.xrplApi); + evernode.Defaults.set({ + xrplApi: hostClient.xrplApi + }); await this.burnMintedURITokens(hostClient.xrplAcc); await hostClient.deregister(); @@ -249,7 +219,7 @@ class Setup { console.log(`Governor address: ${acc?.governorAddress}`); if (!isBasic) { - setEvernodeDefaults(acc.governorAddress, acc.rippledServer); + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); try { const hostClient = new evernode.HostClient(acc.address); @@ -257,7 +227,9 @@ class Setup { console.log(`Registry address: ${hostClient.config.registryAddress}`); console.log(`Heartbeat address: ${hostClient.config.heartbeatAddress}`); - setEvernodeDefaults(acc.governorAddress, acc.rippledServer, hostClient.xrplApi); + evernode.Defaults.set({ + xrplApi: hostClient.xrplApi + }); const [evrBalance, hostInfo] = await Promise.all([hostClient.getEVRBalance(), hostClient.getRegistration()]); if (hostInfo) { @@ -290,12 +262,14 @@ class Setup { cfg.xrpl.rippledServer = appenv.DEFAULT_RIPPLED_SERVER if (!cfg.xrpl.governorAddress) { - setEvernodeDefaults(governorAddress, cfg.xrpl.rippledServer); + await setEvernodeDefaults(cfg.xrpl.network, governorAddress, cfg.xrpl.rippledServer); const hostClient = new evernode.HostClient(cfg.xrpl.address, cfg.xrpl.secret); await hostClient.connect(); - setEvernodeDefaults(governorAddress, cfg.xrpl.rippledServer, hostClient.xrplApi); + evernode.Defaults.set({ + xrplApi: hostClient.xrplApi + }); cfg.xrpl.governorAddress = governorAddress; @@ -311,13 +285,15 @@ class Setup { async hostInfo() { const acc = this.#getConfig(false).xrpl; - setEvernodeDefaults(acc.governorAddress, acc.rippledServer); + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); const hostClient = new evernode.HostClient(acc.address); await hostClient.connect(); // Update the Defaults with "xrplApi" of the client. - setEvernodeDefaults(acc.governorAddress, acc.rippledServer, hostClient.xrplApi); + evernode.Defaults.set({ + xrplApi: hostClient.xrplApi + }); const hostInfo = await hostClient.getHostInfo(); @@ -331,13 +307,15 @@ class Setup { console.log("Updating host..."); const acc = this.#getConfig().xrpl; - setEvernodeDefaults(acc.governorAddress, acc.rippledServer); + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); const hostClient = new evernode.HostClient(acc.address, acc.secret); await hostClient.connect(); // Update the Defaults with "xrplApi" of the client. - setEvernodeDefaults(acc.governorAddress, acc.rippledServer, hostClient.xrplApi); + evernode.Defaults.set({ + xrplApi: hostClient.xrplApi + }); const hostInfo = await hostClient.getHostInfo(); await hostClient.updateRegInfo(hostInfo.activeInstances, null, null, null, null, null, null, null, null, emailAddress); @@ -385,12 +363,14 @@ class Setup { async transfer(transfereeAddress) { console.log("Transferring host..."); const acc = this.#getConfig().xrpl; - setEvernodeDefaults(acc.governorAddress, acc.rippledServer); + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); const hostClient = new evernode.HostClient(acc.address, acc.secret); await hostClient.connect(); - setEvernodeDefaults(acc.governorAddress, acc.rippledServer, hostClient.xrplApi); + evernode.Defaults.set({ + xrplApi: hostClient.xrplApi + }); await hostClient.transfer(transfereeAddress); await this.burnMintedURITokens(hostClient.xrplAcc); @@ -448,13 +428,14 @@ class Setup { let hostClient; async function initClients(rippledServer) { - setEvernodeDefaults(acc.governorAddress, rippledServer); + await setEvernodeDefaults(acc.network, acc.governorAddress, rippledServer); xrplApi = new evernode.XrplApi(); hostClient = new evernode.HostClient(acc.address, acc.secret, { xrplApi: xrplApi }); await xrplApi.connect(); await hostClient.connect(); - setEvernodeDefaults(acc.governorAddress, acc.rippledServer, xrplApi); - + evernode.Defaults.set({ + xrplApi: xrplApi + }); } async function deinitClients() { @@ -594,12 +575,14 @@ class Setup { lease = lease[0]; const acc = this.#getConfig().xrpl; - setEvernodeDefaults(acc.governorAddress, acc.rippledServer); + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); xrplApi = new evernode.XrplApi(acc.rippledServer); await xrplApi.connect(); - setEvernodeDefaults(acc.governorAddress, acc.rippledServer, xrplApi); + evernode.Defaults.set({ + xrplApi: xrplApi + }); // Get the existing uriToken of the lease. const uriToken = (await (new evernode.XrplAccount(lease.tenant_xrp_address, null, { xrplApi: xrplApi }).getURITokens()))?.find(n => n.index == lease.container_name); diff --git a/mb-xrpl/package-lock.json b/mb-xrpl/package-lock.json index 80aa29d..b1f2c1e 100644 --- a/mb-xrpl/package-lock.json +++ b/mb-xrpl/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "mb-xrpl", "dependencies": { - "evernode-js-client": "0.6.20", + "evernode-js-client": "0.6.21", "ip6addr": "0.2.5", "sqlite3": "5.0.2" }, @@ -980,9 +980,9 @@ } }, "node_modules/evernode-js-client": { - "version": "0.6.20", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.20.tgz", - "integrity": "sha512-OC6VNAhwqnNvUc0NhffxwNI9bTDH+BkD/KBTC5Xuwoiq8BhRfYhmfHBnD6M9K5AvLqv+Jxdufc3l1AlzHgILWg==", + "version": "0.6.21", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.21.tgz", + "integrity": "sha512-Q5P6caMTzx3xaUNKhnP1vJTw3wTP/d2J2xSQEMn4m1+t/t67d8+eii3/FeQapRBSZEbNRHm9EbRry9PJhb9xcg==", "dependencies": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", @@ -4010,9 +4010,9 @@ "dev": true }, "evernode-js-client": { - "version": "0.6.20", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.20.tgz", - "integrity": "sha512-OC6VNAhwqnNvUc0NhffxwNI9bTDH+BkD/KBTC5Xuwoiq8BhRfYhmfHBnD6M9K5AvLqv+Jxdufc3l1AlzHgILWg==", + "version": "0.6.21", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.21.tgz", + "integrity": "sha512-Q5P6caMTzx3xaUNKhnP1vJTw3wTP/d2J2xSQEMn4m1+t/t67d8+eii3/FeQapRBSZEbNRHm9EbRry9PJhb9xcg==", "requires": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", diff --git a/mb-xrpl/package.json b/mb-xrpl/package.json index 680ea10..80e8938 100644 --- a/mb-xrpl/package.json +++ b/mb-xrpl/package.json @@ -5,7 +5,7 @@ "build": "npm run lint && ncc build app.js --minify -o dist" }, "dependencies": { - "evernode-js-client": "0.6.20", + "evernode-js-client": "0.6.21", "sqlite3": "5.0.2", "ip6addr": "0.2.5" }, diff --git a/src/version.hpp b/src/version.hpp index 45f7209..19fa068 100644 --- a/src/version.hpp +++ b/src/version.hpp @@ -6,7 +6,7 @@ namespace version { // Sashimono agent version. Written to new configs. - constexpr const char *AGENT_VERSION = "0.7.2"; + constexpr const char *AGENT_VERSION = "0.8.0"; // Minimum compatible config version (this will be used to validate configs). constexpr const char *MIN_CONFIG_VERSION = "0.5.0"; From 7e79812ab25867d825d68c9f901dabfdd808a8c7 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Fri, 24 Nov 2023 16:43:21 +0530 Subject: [PATCH 08/26] GitHub raw file path modification (#300) --- installer/setup.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installer/setup.sh b/installer/setup.sh index e1afa40..c18d77c 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -22,7 +22,7 @@ log_dir=/tmp/evernode-beta repo_owner="EvernodeXRPL" repo_name="evernode-resources" -desired_branch="main" +desired_branch="release" latest_version_endpoint="https://api.github.com/repos/$repo_owner/$repo_name/releases/latest" latest_version_data=$(curl -s "$latest_version_endpoint") @@ -34,7 +34,7 @@ fi # Prepare resources URLs resource_storage="https://github.com/$repo_owner/$repo_name/releases/download/$latest_version" -licence_url="https://raw.githubusercontent.com/$repo_owner/$repo_name/$desired_branch/installer/licence.txt" +licence_url="https://raw.githubusercontent.com/$repo_owner/$repo_name/$desired_branch/sashimono/installer/licence.txt" config_url="https://raw.githubusercontent.com/$repo_owner/$repo_name/$desired_branch/definitions/definitions.json" setup_script_url="$resource_storage/setup.sh" installer_url="$resource_storage/installer.tar.gz" From 09e7aa3796fcd14422233835a5bd70d31f0276d3 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Thu, 30 Nov 2023 11:26:39 +0530 Subject: [PATCH 09/26] Added additional account condition check. (#301) --- installer/jshelper/index.js | 46 +++++++++++++++++++++++ installer/setup.sh | 75 +++++++++++++++++++++++-------------- 2 files changed, 93 insertions(+), 28 deletions(-) diff --git a/installer/jshelper/index.js b/installer/jshelper/index.js index 897764f..0f675e5 100644 --- a/installer/jshelper/index.js +++ b/installer/jshelper/index.js @@ -241,6 +241,52 @@ const funcs = { return { success: false }; }, + 'check-acc-condition': async (args) => { + checkParams(args, 3); + const rippledUrl = args[0]; + const governorAddress = args[1]; + const accountAddress = args[2]; + + await evernode.Defaults.useNetwork(appenv.NETWORK); + + evernode.Defaults.set({ + rippledServer: rippledUrl, + governorAddress: governorAddress + }); + + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); + await xrplApi.connect(); + + evernode.Defaults.set({ + xrplApi: xrplApi + }); + + const hostClient = new evernode.HostClient(accountAddress, null); + const terminateConnections = async () => { + await hostClient.disconnect(); + await xrplApi.disconnect(); + } + + try { + // In order to handle the account not found issue via catch block. + await hostClient.connect(); + const trustline = await hostClient.xrplAcc.getTrustLines(evernode.EvernodeConstants.EVR, hostClient.config.evrIssuerAddress); + if (trustline.length > 0) { + await terminateConnections(); + return { success: true, result: 'RC-PREPARED' } + } else { + await terminateConnections(); + return { success: true, result: 'RC-FRESH' }; + } + } catch (err) { + await terminateConnections(); + + if ((err.data?.error === 'actNotFound')) + return { success: true, result: "RC-FRESH" }; + return { success: false, result: "Error occurred in account condition check." }; + } + }, + 'check-balance': async (args) => { checkParams(args, 5); const rippledUrl = args[0]; diff --git a/installer/setup.sh b/installer/setup.sh index c18d77c..2c28261 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -142,7 +142,7 @@ function wait_call() { wait $spin_pid echo -ne "\r" - [ $return_code -eq 0 ] && echo -e ${output_template/\[OUTPUT\]/$command_output} || echo -e $command_output + [ $return_code -eq 0 ] && echo -e ${output_template/\[OUTPUT\]/$command_output} || echo -e "\r$command_output" return $return_code } @@ -239,7 +239,7 @@ function check_prereq() { if ! command -v node &>/dev/null; then echo "Installing nodejs..." - install_nodejs_utility >/dev/null + ! install_nodejs_utility >/dev/null || exit 1 else version=$(node -v | cut -d '.' -f1) version=${version:1} @@ -901,8 +901,7 @@ function set_host_xrpl_account() { # Modify the permissions accordingly chown $MB_XRPL_USER: $key_file_path && \ - chmod 600 $key_file_path && \ - echomult "Retrived account details via the backed-up secret." || (echomult "Error occurred in secret restoring." && exit 1) + chmod 600 $key_file_path || (echomult "Error occurred in secret restoring." && exit 1) else echomult "Error: Backup secret file format does not support." && exit 1 fi @@ -912,41 +911,61 @@ function set_host_xrpl_account() { generate_and_save_keyfile "$key_file_path" fi - echomult "Your host account with the address $xrpl_address has been generated on Xahau $NETWORK. - \nThe secret key of the account is located at $key_file_path. - \n\nThis is the account that will represent this host on the Evernode host registry. You need to load up the account with following funds in order to continue with the installation. - \n1. At least $min_xrp_amount_per_month XAH (Xahau XRP) to cover regular transaction fees for first month. - \n2. At least $reg_fee EVR to cover Evernode registration fee. - \n\nYou can scan the following QR code in your wallet app to send funds:\n" + echomult "Your host account with the address $xrpl_address will be on Xahau $NETWORK. + \nThe secret key of the account is located at $key_file_path. + \n\nThis is the account that will represent this host on the Evernode host registry. You need to load up the account with following funds in order to continue with the installation. + \n1. At least $min_xrp_amount_per_month XAH (Xahau XRP) to cover regular transaction fees for first month. + \n2. At least $reg_fee EVR to cover Evernode registration fee. + \n\nYou can scan the following QR code in your wallet app to send funds based on the account condition:\n" - generate_qrcode "$xrpl_address" + generate_qrcode "$xrpl_address" - required_balance=$min_xrp_amount_per_month + account_condition='-' + + echomult "\nChecking the account condition..." while true ; do - wait_call "exec_jshelper check-balance $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address NATIVE $required_balance" "Thank you. [OUTPUT] XAH balance is there in your host account." \ + account_condition=$(exec_jshelper check-acc-condition $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address) \ && break - confirm "\nDo you want to re-check the balance?\nPressing 'n' would terminate the installation." || exit 1 + confirm "\nDo you want to re-check the account condition?\nPressing 'n' would terminate the installation." || exit 1 done - echomult "\nPreparing account with EVR trust-line..." - while true ; do - wait_call "exec_jshelper prepare-host $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address $xrpl_secret $inetaddr" "Account preparation is successfull." && break - confirm "\nDo you want to re-try account preparation?\nPressing 'n' would terminate the installation." || exit 1 - done + declare -Ar AccCondtionArry=( [0]="RC-FRESH" [1]="RC-PREPARED" ) - echomult "\n\nIn order to register in Evernode you need to have $reg_fee EVR balance in your host account. Please deposit the required registration fee in EVRs. - \nYou can scan the following QR code in your wallet app to send funds:" + if [ "$account_condition" == "${AccCondtionArry[0]}" ]; then - required_balance=$reg_fee - while true ; do - wait_call "exec_jshelper check-balance $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address ISSUED $required_balance" "Thank you. [OUTPUT] EVR balance is there in your host account." \ - && break - confirm "\nDo you want to re-check the balance?\nPressing 'n' would terminate the installation." || exit 1 - done + echomult "To set up your host account, ensure a deposit of $min_xrp_amount_per_month XAH (Xahau XRP) to cover the regular transaction fees for the first month." + + required_balance=$min_xrp_amount_per_month + while true ; do + wait_call "exec_jshelper check-balance $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address NATIVE $required_balance" "Thank you. [OUTPUT] XAH balance is there in your host account." \ + && break + confirm "\nDo you want to re-check the balance?\nPressing 'n' would terminate the installation." || exit 1 + done + + echomult "\nPreparing account with EVR trust-line..." + while true ; do + wait_call "exec_jshelper prepare-host $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address $xrpl_secret $inetaddr" "Account preparation is successfull." && break + confirm "\nDo you want to re-try account preparation?\nPressing 'n' would terminate the installation." || exit 1 + done + + account_condition=${AccCondtionArry[1]} + fi + + if [ "$account_condition" == "${AccCondtionArry[1]}" ]; then + echomult "\n\nIn order to register in Evernode you need to have $reg_fee EVR balance in your host account. Please deposit the required registration fee in EVRs. + \nYou can scan the provided QR code in your wallet app to send funds:" + + required_balance=$reg_fee + while true ; do + wait_call "exec_jshelper check-balance $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address ISSUED $required_balance" "Thank you. [OUTPUT] EVR balance is there in your host account." \ + && break + confirm "\nDo you want to re-check the balance?\nPressing 'n' would terminate the installation." || exit 1 + done + fi elif [ "$account_validate_criteria" == "transfer" ] || [ "$account_validate_criteria" == "re-register" ]; then - + if [ "$account_validate_criteria" == "re-register" ]; then account_validate_criteria="register" fi From a2690d8351e335e10b3b09f49c72867755e0ca27 Mon Sep 17 00:00:00 2001 From: Dulana Peiris <57042272+du1ana@users.noreply.github.com> Date: Thu, 30 Nov 2023 17:13:42 +0530 Subject: [PATCH 10/26] Added regular key setup (#302) --- installer/setup.sh | 24 +++++++++++++++++++++--- mb-xrpl/app.js | 4 ++++ mb-xrpl/lib/setup.js | 25 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/installer/setup.sh b/installer/setup.sh index 2c28261..0f095b0 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -160,7 +160,7 @@ if $installed ; then && echo "$evernode is already installed on your host. Use the 'evernode' command to manage your host." \ && exit 1 - [ "$1" != "uninstall" ] && [ "$1" != "status" ] && [ "$1" != "list" ] && [ "$1" != "update" ] && [ "$1" != "log" ] && [ "$1" != "applyssl" ] && [ "$1" != "transfer" ] && [ "$1" != "config" ] && [ "$1" != "delete" ] && [ "$1" != "governance" ] && [ "$1" != "auto-update" ] \ + [ "$1" != "uninstall" ] && [ "$1" != "status" ] && [ "$1" != "list" ] && [ "$1" != "update" ] && [ "$1" != "log" ] && [ "$1" != "applyssl" ] && [ "$1" != "transfer" ] && [ "$1" != "config" ] && [ "$1" != "delete" ] && [ "$1" != "governance" ] && [ "$1" != "auto-update" ] && [ "$1" != "set-regkey" ] \ && echomult "$evernode host management tool \nYour host is registered on $evernode. \nSupported commands: @@ -174,7 +174,8 @@ if $installed ; then \ndelete - Remove an instance from the system and recreate the lease \nuninstall - Uninstall and deregister from $evernode \ngovernance - Governance candidate management - \nauto-update - Evernode Auto Updater management" \ + \nauto-update - Evernode Auto Updater management + \nset-regkey - Set regular key" \ && exit 1 elif [ -d $SASHIMONO_BIN ] ; then [ "$1" != "install" ] && [ "$1" != "uninstall" ] \ @@ -208,7 +209,8 @@ if [ "$mode" == "install" ] || [ "$mode" == "uninstall" ] || [ "$mode" == "updat [ -n "$2" ] && [ "$2" != "-q" ] && [ "$2" != "-i" ] && echo "Second arg must be -q (Quiet) or -i (Interactive)" && exit 1 [ "$2" == "-q" ] && interactive=false || interactive=true [ "$mode" == "transfer" ] && transfer=true || transfer=false - (! $transfer || $installed) && [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 + [ "$mode" == "set-regkey" ] && set_regkey=true || set_regkey=false + (! $transfer || $installed || $set_regkey) && [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 fi # Change the relevant setup helper path based on Evernode installation condition and the command mode. @@ -764,6 +766,13 @@ function set_auto_update() { fi } +function set_regular_key() { + [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 + + ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN set-regkey $1 && + echo "There was an error in setting the regular key." && return 1 +} + function set_transferee_address() { # Here we set the default transferee address as 'CURRENT_HOST_ADDRESS', but we set it to the exact current host address in host client side. [ -z $transferee_address ] && transferee_address='' @@ -1821,6 +1830,15 @@ elif [ "$mode" == "auto-update" ]; then \nenable - Enable $evernode auto updater service. \ndisable - Disable $evernode auto updater service." && exit 1 fi + +elif [ "$mode" == "set-regkey" ]; then + if [ -z "$2" ]; then + echo "Regular key to be set must be provided." && exit 1 + elif [[ ! "$2" =~ ^[[:alnum:]]{24,34}$ ]]; then + echo "Regular key is invalid." && exit 1 + fi + set_regular_key $2 + exit 0 fi [ "$mode" != "uninstall" ] && check_installer_pending_finish diff --git a/mb-xrpl/app.js b/mb-xrpl/app.js index b77a49e..cb8195c 100644 --- a/mb-xrpl/app.js +++ b/mb-xrpl/app.js @@ -74,6 +74,9 @@ async function main() { else if (process.argv.length >= 4 && process.argv[2] === 'governance') { await GovernanceManager.handleCommand(process.argv[3], ...process.argv.slice(4)); } + else if (process.argv.length === 4 && process.argv[2] === 'set-regkey') { + await new Setup().setRegularKey(process.argv[3]); + } else if (process.argv[2] === 'help') { console.log(`Usage: node index.js - Run message board. @@ -88,6 +91,7 @@ async function main() { node index.js reconfig [leaseAmount] [totalInstanceCount] [rippledServer] - Update message board configuration. node index.js delete [containerName] - Delete an instance and recreate the lease offer node index.js governance [command] [args] - Governance handling. + node index.js set-regkey [regularKey] - Set regular key. node index.js help - Print help.`); } else { diff --git a/mb-xrpl/lib/setup.js b/mb-xrpl/lib/setup.js index e69dec2..b3e9b46 100644 --- a/mb-xrpl/lib/setup.js +++ b/mb-xrpl/lib/setup.js @@ -626,7 +626,32 @@ class Setup { if (xrplApi) await xrplApi.disconnect(); } + } + async setRegularKey(regularKey) { + { + const acc = this.#getConfig().xrpl; + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); + + console.log(`Setting Regular Key...`); + + try { + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); + + const xrplApi = new evernode.XrplApi(acc.rippledServer, { autoReconnect: false }); + await xrplApi.connect(); + + const xrplAcc = new evernode.XrplAccount(acc.address, acc.secret, { xrplApi: xrplApi }); + + await xrplAcc.setRegularKey(regularKey); + console.log(`Regular key ${regularKey} was assigned to account ${acc.address} successfully.`); + + await xrplApi.disconnect(); + } + catch (e) { + throw e; + } + } } } From ac4ca9ee90504d39a05be298f20571727df3d2e9 Mon Sep 17 00:00:00 2001 From: Chalith Desaman Date: Thu, 30 Nov 2023 17:14:14 +0530 Subject: [PATCH 11/26] Show host account qr in evernode status command (#303) --- installer/setup.sh | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/installer/setup.sh b/installer/setup.sh index 0f095b0..43da4ec 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -1278,16 +1278,26 @@ function check_installer_pending_finish() { } function reg_info() { - echo "" - if MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN reginfo ; then - local sashimono_agent_status=$(systemctl is-active sashimono-agent.service) - local mb_user_id=$(id -u "$MB_XRPL_USER") - local mb_user_runtime_dir="/run/user/$mb_user_id" - local sashimono_mb_xrpl_status=$(sudo -u "$MB_XRPL_USER" XDG_RUNTIME_DIR="$mb_user_runtime_dir" systemctl --user is-active $MB_XRPL_SERVICE) - echo "Sashimono agent status: $sashimono_agent_status" - echo "Sashimono mb xrpl status: $sashimono_mb_xrpl_status" - echo -e "\nYour account details are stored in $MB_XRPL_DATA/mb-xrpl.cfg" - fi + local reg_info=$( MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN reginfo || echo ERROR) + local error=$(echo "$reg_info" | tail -1) + [ "$error" == "ERROR" ] && echo "${reg_info/ERROR/""}" && exit 1 + + # Get raddress from first line. + local address_line=$(echo "$reg_info" | head -1) + local host_address=$( echo "$address_line" | awk -F : ' { print $2 } ') + echo -e "\n$address_line\n" + generate_qrcode "$host_address" + + # Remove first line and print. + echo -e "\n${reg_info/$address_line/""}" + + local sashimono_agent_status=$(systemctl is-active sashimono-agent.service) + local mb_user_id=$(id -u "$MB_XRPL_USER") + local mb_user_runtime_dir="/run/user/$mb_user_id" + local sashimono_mb_xrpl_status=$(sudo -u "$MB_XRPL_USER" XDG_RUNTIME_DIR="$mb_user_runtime_dir" systemctl --user is-active $MB_XRPL_SERVICE) + echo "Sashimono agent status: $sashimono_agent_status" + echo "Sashimono mb xrpl status: $sashimono_mb_xrpl_status" + echo -e "\nYour account details are stored in $MB_XRPL_DATA/mb-xrpl.cfg" } function apply_ssl() { From 65390ce8d3690d30cf0a9112bb26746fedd49d97 Mon Sep 17 00:00:00 2001 From: Dulana Peiris <57042272+du1ana@users.noreply.github.com> Date: Thu, 7 Dec 2023 20:21:08 +0530 Subject: [PATCH 12/26] Added env variable to enable sashi create (#305) --- mb-xrpl/lib/sashi-cli.js | 8 +++++++- sashi-cli/main.cpp | 22 ++++++++++++++++++++++ test/vm-cluster/cluster.sh | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/mb-xrpl/lib/sashi-cli.js b/mb-xrpl/lib/sashi-cli.js index 6c0616a..b57ad4c 100644 --- a/mb-xrpl/lib/sashi-cli.js +++ b/mb-xrpl/lib/sashi-cli.js @@ -49,7 +49,13 @@ class SashiCLI { execSashiCli(msg) { this.#waiting = true; return new Promise((resolve, reject) => { - exec(`${this.cliPath} json -m '${JSON.stringify(msg)}'`, { stdio: 'pipe' }, (err, stdout, stderr) => { + let command = `${this.cliPath} json -m '${JSON.stringify(msg)}'`; + + if (msg.type === "create") { + command = `DEV_MODE=1 ${command}`; + } + + exec(command, { stdio: 'pipe' }, (err, stdout, stderr) => { this.#waiting = false; if (err || stderr) { diff --git a/sashi-cli/main.cpp b/sashi-cli/main.cpp index 1c2fa35..208094d 100644 --- a/sashi-cli/main.cpp +++ b/sashi-cli/main.cpp @@ -5,6 +5,8 @@ #include "cli-manager.hpp" #include "version.hpp" +#define DEV_MODE "DEV_MODE" + std::string exec_dir; /** @@ -90,6 +92,7 @@ int parse_cmd(int argc, char **argv) std::string json_message; json->add_option("-m,--message", json_message, "JSON message"); + create->group(""); //Hides 'create' command from help-all std::string owner, contract_id, image, outbound_ipv6, outbound_net_interface; create->add_option("-o,--owner", owner, "Hex (ed-prefixed) public key of the instance owner"); create->add_option("-c,--contract-id", contract_id, "Contract Id (GUID) of the instance"); @@ -126,6 +129,25 @@ int parse_cmd(int argc, char **argv) } // Verifying subcommands. + + bool is_dev_mode = (getenv(DEV_MODE) != nullptr); + + if (!is_dev_mode) + { + if(create->parsed()){ + std::cout << "Developer mode must be enabled to access this command." << std::endl; + return -1; + } + if(json->parsed()){ + jsoncons::json json_data = jsoncons::json::parse(json_message); + if (json_data.contains("type") && json_data["type"].as_string() == "create") + { + std::cout << "Developer mode must be enabled to access this command." << std::endl; + return -1; + } + } + } + if (version->parsed()) { std::cout << "Sashimono CLI version " << version::CLI_VERSION << std::endl; diff --git a/test/vm-cluster/cluster.sh b/test/vm-cluster/cluster.sh index f6924d5..ffb5b3f 100755 --- a/test/vm-cluster/cluster.sh +++ b/test/vm-cluster/cluster.sh @@ -473,7 +473,7 @@ if [ $mode == "create" ] || [ $mode == "createall" ]; then config=$(echo "$config" | jq -c ".mesh.known_peers = [$peers]" | jq -c ".contract.unl = [\"$pubkey\"]") fi - command="sashi json -m '{\"type\":\"create\",\"owner_pubkey\":\"$ownerpubkey\",\"contract_id\":\"$contractid\",\"image\":\"$image\",\"config\":$config}'" + command="DEV_MODE=1 sashi json -m '{\"type\":\"create\",\"owner_pubkey\":\"$ownerpubkey\",\"contract_id\":\"$contractid\",\"image\":\"$image\",\"config\":$config}'" output=$(sshskp $sshuser@$hostaddr $command | tr '\0' '\n') # If an output received consider updating the json file. if [ ! "$output" = "" ]; then From 97ab49a8b3ea021e522286d5d5b92ebbd3dfcd1e Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Thu, 7 Dec 2023 20:21:48 +0530 Subject: [PATCH 13/26] Changed docker id to evernode (#307) --- src/conf.cpp | 2 +- test/docker/Dockerfile.ubt.20.04 | 2 +- test/docker/Dockerfile.ubt.20.04-njs | 2 +- test/docker/build.sh | 2 +- test/docker/push.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/conf.cpp b/src/conf.cpp index 5c91cc1..faa9852 100644 --- a/src/conf.cpp +++ b/src/conf.cpp @@ -69,7 +69,7 @@ namespace conf cfg.system.max_cpu_us = !cpu_us ? 900000 : cpu_us; // Total CPU allocation out of 1000000 microsec (1 sec). cfg.system.max_storage_kbytes = !disk_kbytes ? 5242880 : disk_kbytes; - cfg.docker.image_prefix = "evernodedev/sashimono:"; + cfg.docker.image_prefix = "evernode/sashimono:"; cfg.docker.registry_port = docker_registry_port; cfg.log.max_file_count = 50; diff --git a/test/docker/Dockerfile.ubt.20.04 b/test/docker/Dockerfile.ubt.20.04 index 32deac1..34b4051 100644 --- a/test/docker/Dockerfile.ubt.20.04 +++ b/test/docker/Dockerfile.ubt.20.04 @@ -1,4 +1,4 @@ -FROM evernodedev/hotpocket:0.6.4-ubt.20.04 +FROM evernode/hotpocket:0.6.4-ubt.20.04 RUN apt-get update \ && apt-get install --no-install-recommends -y unzip jq \ diff --git a/test/docker/Dockerfile.ubt.20.04-njs b/test/docker/Dockerfile.ubt.20.04-njs index d45e7d7..2d740a3 100644 --- a/test/docker/Dockerfile.ubt.20.04-njs +++ b/test/docker/Dockerfile.ubt.20.04-njs @@ -1,4 +1,4 @@ -FROM evernodedev/hotpocket:0.6.4-ubt.20.04-njs.20 +FROM evernode/hotpocket:0.6.4-ubt.20.04-njs.20 RUN apt-get update \ && apt-get install --no-install-recommends -y unzip jq \ diff --git a/test/docker/build.sh b/test/docker/build.sh index fa3afaf..37b2c47 100755 --- a/test/docker/build.sh +++ b/test/docker/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -img=evernodedev/sashimono +img=evernode/sashimono docker build -t $img:hp.latest-ubt.20.04 -t $img:hp.0.6.4-ubt.20.04 -f ./Dockerfile.ubt.20.04 . docker build -t $img:hp.latest-ubt.20.04-njs.20 -t $img:hp.0.6.4-ubt.20.04-njs.20 -f ./Dockerfile.ubt.20.04-njs . diff --git a/test/docker/push.sh b/test/docker/push.sh index 009c795..487adfe 100755 --- a/test/docker/push.sh +++ b/test/docker/push.sh @@ -1,5 +1,5 @@ #!/bin/bash -img=evernodedev/sashimono +img=evernode/sashimono docker image push --all-tags $img \ No newline at end of file From 0c3fb2daeb8f7ce480c2cd00ca7d7ba395347b27 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Fri, 8 Dec 2023 10:11:03 +0530 Subject: [PATCH 14/26] Updated JS library and added modifications to support dev mode. (#306) --- README.md | 2 +- installer/jshelper/index.js | 90 +++++++++++++++------------- installer/jshelper/package-lock.json | 14 ++--- installer/jshelper/package.json | 2 +- installer/setup.sh | 25 +++----- mb-xrpl/app.js | 8 ++- mb-xrpl/lib/setup.js | 44 ++++++++++++++ mb-xrpl/package-lock.json | 14 ++--- mb-xrpl/package.json | 2 +- 9 files changed, 121 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 51f47cf..adc2afe 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Run `make installer` ('installer.tar.gz' will be placed in build directory) 1. Node app which is listening to the host xrpl account. 1. `cd mb-xrpl && npm install` (You only have to do this once) -1. `node app.js new [address] [secret] [governerAddress] [domain or ip] [leaseAmount] [rippledServer]` will create new config files called `mb-xrpl.cfg` and `secret.cfg` +1. `node app.js new [address] [secretPath] [governorAddress] [domain or ip] [leaseAmount] [rippledServer] [ipv6Subnet] [ipv6Interface] [network]` will create new config files called `mb-xrpl.cfg` and `secret.cfg` 1. `node app.js betagen [governerAddress] [domain or ip] [leaseAmount]` will generate beta host account and populate the configs. 1. `node app.js register [countryCode] [cpuMicroSec] [ramKb] [swapKb] [diskKb] [totalInstanceCount] [cpuModel] [cpuCount] [cpuSpeed] [emailAddress] [description(optional)]` will register the host on Evernode. 1. `node app.js deregister` will deregister the host from Evernode. diff --git a/installer/jshelper/index.js b/installer/jshelper/index.js index 0f675e5..e49dd9b 100644 --- a/installer/jshelper/index.js +++ b/installer/jshelper/index.js @@ -304,53 +304,57 @@ const funcs = { governorAddress: governorAddress }); - const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); - await xrplApi.connect(); + try { + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); + await xrplApi.connect(); - evernode.Defaults.set({ - xrplApi: xrplApi - }); + evernode.Defaults.set({ + xrplApi: xrplApi + }); - const hostClient = new evernode.HostClient(accountAddress, null); - const terminateConnections = async () => { - await hostClient.disconnect(); - await xrplApi.disconnect(); - } - - 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(); - - await new Promise(resolve => setTimeout(resolve, 1000)); - if (tokenType === 'NATIVE') - balance = Number((await hostClient.xrplAcc.getInfo()).Balance) / 1000000; - else - balance = Number(await hostClient.getEVRBalance()); - - if (balance < expectedBalance) { - if (++attempts <= WAIT_PERIOD) - continue; - - await terminateConnections(); - return { success: false, result: "Funds not received within timeout." }; - } - - break; - } catch (err) { - if (err.data?.error === 'actNotFound' && ++attempts <= WAIT_PERIOD) { - await new Promise(resolve => setTimeout(resolve, 1000)); - continue; - } - await terminateConnections(); - return { success: false, result: (err.data?.error === 'actNotFound') ? "Funds not received within timeout." : "Error occurred in account balance check." }; + const hostClient = new evernode.HostClient(accountAddress, null); + const terminateConnections = async () => { + await hostClient.disconnect(); + await xrplApi.disconnect(); } - } - await terminateConnections(); - return { success: true, result: `${balance}` }; + 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(); + + await new Promise(resolve => setTimeout(resolve, 1000)); + if (tokenType === 'NATIVE') + balance = Number((await hostClient.xrplAcc.getInfo()).Balance) / 1000000; + else + balance = Number(await hostClient.getEVRBalance()); + + if (balance < expectedBalance) { + if (++attempts <= WAIT_PERIOD) + continue; + + await terminateConnections(); + return { success: false, result: "Funds not received within timeout." }; + } + + break; + } catch (err) { + if (err.data?.error === 'actNotFound' && ++attempts <= WAIT_PERIOD) { + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + await terminateConnections(); + return { success: false, result: (err.data?.error === 'actNotFound') ? "Funds not received within timeout." : "Error occurred in account balance check." }; + } + } + + await terminateConnections(); + return { success: true, result: `${balance}` }; + } catch { + return { success: false, result: "Error occurred in websocket connection." }; + } }, 'generate-account': async (args) => { diff --git a/installer/jshelper/package-lock.json b/installer/jshelper/package-lock.json index 95e80b7..b442faa 100644 --- a/installer/jshelper/package-lock.json +++ b/installer/jshelper/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "evernode-setup-helper", "dependencies": { - "evernode-js-client": "0.6.21", + "evernode-js-client": "0.6.23", "ip6addr": "0.2.5", "ripple-keypairs": "1.3.1" } @@ -364,9 +364,9 @@ } }, "node_modules/evernode-js-client": { - "version": "0.6.21", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.21.tgz", - "integrity": "sha512-Q5P6caMTzx3xaUNKhnP1vJTw3wTP/d2J2xSQEMn4m1+t/t67d8+eii3/FeQapRBSZEbNRHm9EbRry9PJhb9xcg==", + "version": "0.6.23", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.23.tgz", + "integrity": "sha512-MXnQLkusq8UGDcf91bSmm9AVOQ3DSfSCJwWbuQpZDsXflbK5X6Ivxjj3ABQafGjAtQLn2KDKtlfzEwRzZvpkOQ==", "dependencies": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", @@ -1543,9 +1543,9 @@ } }, "evernode-js-client": { - "version": "0.6.21", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.21.tgz", - "integrity": "sha512-Q5P6caMTzx3xaUNKhnP1vJTw3wTP/d2J2xSQEMn4m1+t/t67d8+eii3/FeQapRBSZEbNRHm9EbRry9PJhb9xcg==", + "version": "0.6.23", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.23.tgz", + "integrity": "sha512-MXnQLkusq8UGDcf91bSmm9AVOQ3DSfSCJwWbuQpZDsXflbK5X6Ivxjj3ABQafGjAtQLn2KDKtlfzEwRzZvpkOQ==", "requires": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", diff --git a/installer/jshelper/package.json b/installer/jshelper/package.json index 6cd21ba..c195561 100644 --- a/installer/jshelper/package.json +++ b/installer/jshelper/package.json @@ -4,7 +4,7 @@ "build": "ncc build index.js --minify -o dist" }, "dependencies": { - "evernode-js-client": "0.6.21", + "evernode-js-client": "0.6.23", "ip6addr": "0.2.5", "ripple-keypairs": "1.3.1" } diff --git a/installer/setup.sh b/installer/setup.sh index 43da4ec..4db7ec9 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -69,6 +69,7 @@ export MB_XRPL_USER="sashimbxrpl" export CG_SUFFIX="-cg" export EVERNODE_AUTO_UPDATE_SERVICE="evernode-auto-update" +# TODO Change this to mainnet in release branch while using devnet in main branch. export NETWORK="${NETWORK:-devnet}" # Private docker registry (not used for now) @@ -891,29 +892,17 @@ function set_host_xrpl_account() { # Check for saved secrets due to a previous installation. if [[ -f "$secret_backup_location" || -f "$key_file_path" ]]; then + key_file_dir=$(dirname "$key_file_path") + if [ ! -d "$key_file_dir" ]; then + mkdir -p "$key_file_dir" + fi + if [ -f "$secret_backup_location" ]; then echomult "Retrived account details via a backed-up secret." && mv $secret_backup_location $key_file_path - else - echomult "Retrived account details via a previously specified secret." fi - local existing_secret=$(jq -r '.xrpl.secret' "$key_file_path" 2>/dev/null) - if [ "$existing_secret" != "null" ] && [ "$existing_secret" != "-" ]; then - account_json=$(exec_jshelper generate-account $existing_secret) - xrpl_address=$(jq -r '.address' <<< "$account_json") - xrpl_secret=$(jq -r '.secret' <<< "$account_json") + generate_and_save_keyfile "$key_file_path" - key_file_dir=$(dirname "$key_file_path") - if [ ! -d "$key_file_dir" ]; then - mkdir -p "$key_file_dir" - fi - - # Modify the permissions accordingly - chown $MB_XRPL_USER: $key_file_path && \ - chmod 600 $key_file_path || (echomult "Error occurred in secret restoring." && exit 1) - else - echomult "Error: Backup secret file format does not support." && exit 1 - fi else echomult "Generating new keypair for the host...\n" diff --git a/mb-xrpl/app.js b/mb-xrpl/app.js index cb8195c..ddb3d75 100644 --- a/mb-xrpl/app.js +++ b/mb-xrpl/app.js @@ -17,7 +17,7 @@ async function main() { const accountAddress = process.argv[3]; const accountSecretPath = process.argv[4]; const governorAddress = process.argv[5]; - // const domain = process.argv[6]; + const domain = process.argv[6]; const leaseAmount = process.argv[7]; const rippledServer = process.argv[8]; const ipv6Subnet = (process.argv[9] === '-') ? null : process.argv[9]; @@ -25,6 +25,10 @@ async function main() { const network = process.argv.length > 11 ? process.argv[11] : appenv.NETWORK; const setup = new Setup(); setup.newConfig(accountAddress, accountSecretPath, governorAddress, parseFloat(leaseAmount), rippledServer, ipv6Subnet, ipv6NetInterface, network); + + if (appenv.IS_DEV_MODE) { + await setup.prepareHostAccount(domain); + } } else if (process.argv.length === 7 && process.argv[2] === 'betagen') { const governorAddress = process.argv[3]; @@ -81,7 +85,7 @@ async function main() { console.log(`Usage: node index.js - Run message board. node index.js version - Print version. - node index.js new [address] [secret] [governorAddress] [leaseAmount] [rippledServer] [ipv6Subnet] [ipv6Interface] [network] - Create new config files. + node index.js new [address] [secretPath] [governorAddress] [domain or ip] [leaseAmount] [rippledServer] [ipv6Subnet] [ipv6Interface] [network] - Create new config files. node index.js betagen [governorAddress] [domain or ip] [leaseAmount] [rippledServer] - Generate beta host account and populate the configs. node index.js register [countryCode] [cpuMicroSec] [ramKb] [swapKb] [diskKb] [totalInstanceCount] [description] [network] - Register the host on Evernode. node index.js transfer [transfereeAddress] - Initiate a transfer. diff --git a/mb-xrpl/lib/setup.js b/mb-xrpl/lib/setup.js index b3e9b46..1e3a38a 100644 --- a/mb-xrpl/lib/setup.js +++ b/mb-xrpl/lib/setup.js @@ -79,6 +79,50 @@ class Setup { this.#saveConfig(ipv6NetInterface ? { ...baseConfig, networking: { ipv6: { subnet: ipv6Subnet, interface: ipv6NetInterface } } } : baseConfig); } + async prepareHostAccount(domain) { + + const config = this.#getConfig(); + const acc = config.xrpl; + await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); + + // Prepare host account. + { + const hostClient = new evernode.HostClient(acc.address, acc.secret); + await hostClient.connect(); + + // Update the Defaults with "xrplApi" of the client. + evernode.Defaults.set({ + xrplApi: hostClient.xrplApi + }); + + console.log(`Preparing host account:${acc.address} (domain:${domain} registry:${hostClient.config.registryAddress})`); + + // Sometimes we may get 'account not found' error from rippled when some servers in the cluster + // haven't still updated the ledger. In such cases, we retry several times before giving up. + { + let attempts = 0; + while (attempts >= 0) { + try { + await hostClient.prepareAccount(domain); + break; + } + catch (err) { + if (err.data?.error === 'actNotFound' && ++attempts <= 5) { + console.log("actNotFound - retrying...") + // Wait and retry. + await new Promise(resolve => setTimeout(resolve, 3000)); + continue; + } + throw err; + } + } + } + + await hostClient.disconnect(); + } + + } + async generateBetaHostAccount(rippledServer, governorAddress, domain, network = null) { await setEvernodeDefaults(network, governorAddress, rippledServer); diff --git a/mb-xrpl/package-lock.json b/mb-xrpl/package-lock.json index b1f2c1e..18cfcc2 100644 --- a/mb-xrpl/package-lock.json +++ b/mb-xrpl/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "mb-xrpl", "dependencies": { - "evernode-js-client": "0.6.21", + "evernode-js-client": "0.6.23", "ip6addr": "0.2.5", "sqlite3": "5.0.2" }, @@ -980,9 +980,9 @@ } }, "node_modules/evernode-js-client": { - "version": "0.6.21", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.21.tgz", - "integrity": "sha512-Q5P6caMTzx3xaUNKhnP1vJTw3wTP/d2J2xSQEMn4m1+t/t67d8+eii3/FeQapRBSZEbNRHm9EbRry9PJhb9xcg==", + "version": "0.6.23", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.23.tgz", + "integrity": "sha512-MXnQLkusq8UGDcf91bSmm9AVOQ3DSfSCJwWbuQpZDsXflbK5X6Ivxjj3ABQafGjAtQLn2KDKtlfzEwRzZvpkOQ==", "dependencies": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", @@ -4010,9 +4010,9 @@ "dev": true }, "evernode-js-client": { - "version": "0.6.21", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.21.tgz", - "integrity": "sha512-Q5P6caMTzx3xaUNKhnP1vJTw3wTP/d2J2xSQEMn4m1+t/t67d8+eii3/FeQapRBSZEbNRHm9EbRry9PJhb9xcg==", + "version": "0.6.23", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.23.tgz", + "integrity": "sha512-MXnQLkusq8UGDcf91bSmm9AVOQ3DSfSCJwWbuQpZDsXflbK5X6Ivxjj3ABQafGjAtQLn2KDKtlfzEwRzZvpkOQ==", "requires": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", diff --git a/mb-xrpl/package.json b/mb-xrpl/package.json index 80e8938..6ace3e6 100644 --- a/mb-xrpl/package.json +++ b/mb-xrpl/package.json @@ -5,7 +5,7 @@ "build": "npm run lint && ncc build app.js --minify -o dist" }, "dependencies": { - "evernode-js-client": "0.6.21", + "evernode-js-client": "0.6.23", "sqlite3": "5.0.2", "ip6addr": "0.2.5" }, From 93b3a5ca015543e7e8c01d6f063dd3c75b916e44 Mon Sep 17 00:00:00 2001 From: Chalith Desaman Date: Fri, 8 Dec 2023 10:53:11 +0530 Subject: [PATCH 15/26] Remove unnssasary variables (#308) --- installer/sashimono-install.sh | 8 +--- installer/setup.sh | 7 ++-- mb-xrpl/app.js | 11 ----- mb-xrpl/lib/appenv.js | 6 +-- mb-xrpl/lib/message-board.js | 2 +- mb-xrpl/lib/setup.js | 75 ---------------------------------- 6 files changed, 8 insertions(+), 101 deletions(-) diff --git a/installer/sashimono-install.sh b/installer/sashimono-install.sh index 9042fd4..ee0b40a 100755 --- a/installer/sashimono-install.sh +++ b/installer/sashimono-install.sh @@ -307,18 +307,14 @@ if [ "$NO_MB" == "" ]; then # Change ownership to message board user. chown -R "$MB_XRPL_USER":"$MB_XRPL_USER" $MB_XRPL_DATA - # Betage and register if not upgrade mode. + # Register if not upgrade mode. if [ "$UPGRADE" == "0" ]; then # Setup and register the account. if ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN reginfo basic >/dev/null 2>&1; then stage "Configuring host xrpl account" echo "Using registry: $EVERNODE_REGISTRY_ADDRESS" - # Commented for now, because 'betagen' will no longer be used. - # ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN betagen $EVERNODE_GOVERNOR_ADDRESS $inetaddr $lease_amount $rippled_server $xrpl_account_secret && echo "XRPLACC_FAILURE" && rollback - # doreg=1 - - ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN new $xrpl_account_address $xrpl_account_secret_path $EVERNODE_GOVERNOR_ADDRESS $inetaddr $lease_amount $rippled_server $ipv6_subnet $ipv6_net_interface && echo "XRPLACC_FAILURE" && rollback + ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN new $xrpl_account_address $xrpl_account_secret_path $EVERNODE_GOVERNOR_ADDRESS $inetaddr $lease_amount $rippled_server $ipv6_subnet $ipv6_net_interface $NETWORK && echo "XRPLACC_FAILURE" && rollback doreg=1 fi diff --git a/installer/setup.sh b/installer/setup.sh index 4db7ec9..210f65b 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -18,7 +18,7 @@ instances_per_core=3 max_non_ipv6_instances=5 max_ipv6_prefix_len=112 evernode_alias=/usr/bin/evernode -log_dir=/tmp/evernode-beta +log_dir=/tmp/evernode repo_owner="EvernodeXRPL" repo_name="evernode-resources" @@ -41,7 +41,7 @@ installer_url="$resource_storage/installer.tar.gz" jshelper_url="$resource_storage/setup-jshelper.tar.gz" installer_version_timestamp_file="installer.version.timestamp" -default_rippled_server="wss://hooks-testnet-v3.xrpl-labs.com" +default_rippled_server="wss://xahau.network" setup_helper_dir="/tmp/evernode-setup-helpers" nodejs_util_bin="/usr/bin/node" jshelper_bin="$setup_helper_dir/jshelper/index.js" @@ -69,8 +69,7 @@ export MB_XRPL_USER="sashimbxrpl" export CG_SUFFIX="-cg" export EVERNODE_AUTO_UPDATE_SERVICE="evernode-auto-update" -# TODO Change this to mainnet in release branch while using devnet in main branch. -export NETWORK="${NETWORK:-devnet}" +export NETWORK="${NETWORK:-mainnet}" # Private docker registry (not used for now) export DOCKER_REGISTRY_USER="sashidockerreg" diff --git a/mb-xrpl/app.js b/mb-xrpl/app.js index ddb3d75..98a7682 100644 --- a/mb-xrpl/app.js +++ b/mb-xrpl/app.js @@ -30,16 +30,6 @@ async function main() { await setup.prepareHostAccount(domain); } } - else if (process.argv.length === 7 && process.argv[2] === 'betagen') { - const governorAddress = process.argv[3]; - const domain = process.argv[4]; - const leaseAmount = process.argv[5]; - const rippledServer = process.argv[6]; - const network = process.argv.length > 7 ? process.argv[7] : appenv.NETWORK; - const setup = new Setup(); - const acc = await setup.generateBetaHostAccount(rippledServer, governorAddress, domain, network); - setup.newConfig(acc.address, acc.secret, governorAddress, parseFloat(leaseAmount), rippledServer, null, null, network); - } else if (process.argv.length >= 13 && process.argv[2] === 'register') { await new Setup().register(process.argv[3], parseInt(process.argv[4]), parseInt(process.argv[5]), parseInt(process.argv[6]), parseInt(process.argv[7]), parseInt(process.argv[8]), process.argv[9], parseInt(process.argv[10]), parseInt(process.argv[11]), process.argv[12], process.argv[13]); @@ -86,7 +76,6 @@ async function main() { node index.js - Run message board. node index.js version - Print version. node index.js new [address] [secretPath] [governorAddress] [domain or ip] [leaseAmount] [rippledServer] [ipv6Subnet] [ipv6Interface] [network] - Create new config files. - node index.js betagen [governorAddress] [domain or ip] [leaseAmount] [rippledServer] - Generate beta host account and populate the configs. node index.js register [countryCode] [cpuMicroSec] [ramKb] [swapKb] [diskKb] [totalInstanceCount] [description] [network] - Register the host on Evernode. node index.js transfer [transfereeAddress] - Initiate a transfer. node index.js deregister - Deregister the host from Evernode. diff --git a/mb-xrpl/lib/appenv.js b/mb-xrpl/lib/appenv.js index 00a6adc..44d08e5 100644 --- a/mb-xrpl/lib/appenv.js +++ b/mb-xrpl/lib/appenv.js @@ -5,9 +5,7 @@ const fs = require('fs'); let appenv = { IS_DEV_MODE: process.env.MB_DEV === "1", FILE_LOG_ENABLED: process.env.MB_FILE_LOG === "1", - DATA_DIR: process.env.MB_DATA_DIR || __dirname, - FAUCET_URL: process.env.MB_FAUCET_URL || "https://hooks-testnet-v3.xrpl-labs.com/newcreds", - DEFAULT_FULL_HISTORY_NODE: 'wss://hooks-testnet-v3.xrpl-labs.com' // If we migrate to Main NET, this should be configured with the relevant full history Node WebSocket. + DATA_DIR: process.env.MB_DATA_DIR || __dirname } appenv = { @@ -29,7 +27,7 @@ appenv = { SASHI_CLI_PATH: appenv.IS_DEV_MODE ? "../build/sashi" : "/usr/bin/sashi", MB_VERSION: '0.8.0', TOS_HASH: '757A0237B44D8B2BBB04AE2BAD5813858E0AECD2F0B217075E27E0630BA74314', // This is the sha256 hash of TOS text. - NETWORK: 'testnet' + NETWORK: 'mainnet' } const getSecretPath = () => { diff --git a/mb-xrpl/lib/message-board.js b/mb-xrpl/lib/message-board.js index 7449a43..334b01f 100644 --- a/mb-xrpl/lib/message-board.js +++ b/mb-xrpl/lib/message-board.js @@ -624,7 +624,7 @@ class MessageBoard { } async #catchupMissedLeases() { - const fullHistoryXrplApi = new evernode.XrplApi(appenv.DEFAULT_FULL_HISTORY_NODE); + const fullHistoryXrplApi = new evernode.XrplApi(); await fullHistoryXrplApi.connect(); this.db.open(); diff --git a/mb-xrpl/lib/setup.js b/mb-xrpl/lib/setup.js index 1e3a38a..8434702 100644 --- a/mb-xrpl/lib/setup.js +++ b/mb-xrpl/lib/setup.js @@ -43,18 +43,6 @@ class Setup { }) } - async #generateFaucetAccount() { - console.log("Generating faucet account..."); - const resp = await this.#httpPost(appenv.FAUCET_URL); - const json = JSON.parse(resp); - - // If Hooks TEST NET is used. - return { - address: json.address, - secret: json.secret - }; - } - #getConfig(readSecret = true) { return ConfigHelper.readConfig(appenv.CONFIG_PATH, readSecret ? appenv.SECRET_CONFIG_PATH : null); } @@ -123,69 +111,6 @@ class Setup { } - async generateBetaHostAccount(rippledServer, governorAddress, domain, network = null) { - - await setEvernodeDefaults(network, governorAddress, rippledServer); - - const acc = await this.#generateFaucetAccount(); - - // Prepare host account. - { - const hostClient = new evernode.HostClient(acc.address, acc.secret); - await hostClient.connect(); - - console.log(`Preparing host account:${acc.address} (domain:${domain} registry:${hostClient.config.registryAddress})`); - - // Sometimes we may get 'account not found' error from rippled when some servers in the testnet cluster - // haven't still updated the ledger. In such cases, we retry several times before giving up. - { - let attempts = 0; - while (attempts >= 0) { - try { - await hostClient.prepareAccount(domain); - break; - } - catch (err) { - if (err.data?.error === 'actNotFound' && ++attempts <= 5) { - console.log("actNotFound - retrying...") - // Wait and retry. - await new Promise(resolve => setTimeout(resolve, 3000)); - continue; - } - throw err; - } - } - } - - // Get beta EVRs from foundation to host account. - { - console.log("Requesting beta EVRs..."); - await hostClient.xrplAcc.makePayment(hostClient.config.foundationAddress, - evernode.XrplConstants.MIN_XRP_AMOUNT, - evernode.XrplConstants.XRP, - null, - [{ type: 'giftBetaEvr', format: '', data: '' }]); - - // Keep watching our EVR balance. - let attempts = 0; - while (attempts >= 0) { - await new Promise(resolve => setTimeout(resolve, 1000)); - const balance = await hostClient.getEVRBalance(); - if (balance === '0') { - if (++attempts <= 20) - continue; - throw "EVR funds not received within timeout."; - } - break; - } - } - - await hostClient.disconnect(); - } - - return acc; - } - async register(countryCode, cpuMicroSec, ramKb, swapKb, diskKb, totalInstanceCount, cpuModel, cpuCount, cpuSpeed, emailAddress, description) { console.log("Registering host..."); let cpuModelFormatted = cpuModel.replaceAll('_', ' '); From a60d9287307f29608e76d911dc0f48cae37a71f9 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Fri, 8 Dec 2023 13:04:28 +0530 Subject: [PATCH 16/26] Changed resource branch (#309) --- installer/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/setup.sh b/installer/setup.sh index 210f65b..19c039e 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -22,7 +22,7 @@ log_dir=/tmp/evernode repo_owner="EvernodeXRPL" repo_name="evernode-resources" -desired_branch="release" +desired_branch="main" latest_version_endpoint="https://api.github.com/repos/$repo_owner/$repo_name/releases/latest" latest_version_data=$(curl -s "$latest_version_endpoint") From 23965c98b0ff8e1881b2c542bfe1d7b61f264ce0 Mon Sep 17 00:00:00 2001 From: chalith Date: Mon, 11 Dec 2023 10:59:04 +0530 Subject: [PATCH 17/26] Changed the content --- evernode-bootstrap-contract | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evernode-bootstrap-contract b/evernode-bootstrap-contract index 1e0e816..d2b01a0 160000 --- a/evernode-bootstrap-contract +++ b/evernode-bootstrap-contract @@ -1 +1 @@ -Subproject commit 1e0e816abbb12f317f0a447c059e0081b78c266a +Subproject commit d2b01a0700534c3f404f11f7e7cfa4ba692d147d From b1e062eb7a9f63839a99cc6c94a50f0ad5db3d42 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Tue, 12 Dec 2023 10:29:45 +0530 Subject: [PATCH 18/26] Modified Domain Specification (#310) --- installer/setup.sh | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/installer/setup.sh b/installer/setup.sh index 19c039e..48a2e0c 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -485,36 +485,35 @@ function validate_email_address() { function set_inet_addr() { - # TODO : Remove NO_DOMAIN usage (Kept for local testing) - if [ "$NO_DOMAIN" == "" ] ; then + # Skip system requirement check in non-production environments if $NO_DOMAIN=1. + if [ "$NETWORK" == "mainnet" ] || [[ "$NETWORK" != "mainnet" && "$NO_DOMAIN" == "" ]] ; then echo "" while [ -z "$inetaddr" ]; do read -ep "Please specify the domain name that this host is reachable at: " inetaddr Date: Thu, 14 Dec 2023 10:25:33 +0530 Subject: [PATCH 19/26] Modified message-board user management. (#311) --- installer/sashimono-install.sh | 52 --------------------- installer/sashimono-uninstall.sh | 22 --------- installer/setup.sh | 80 +++++++++++--------------------- 3 files changed, 28 insertions(+), 126 deletions(-) diff --git a/installer/sashimono-install.sh b/installer/sashimono-install.sh index ee0b40a..cc5752a 100755 --- a/installer/sashimono-install.sh +++ b/installer/sashimono-install.sh @@ -28,49 +28,16 @@ ipv6_net_interface=${20} script_dir=$(dirname "$(realpath "$0")") desired_slirp4netns_version="1.2.1" setup_helper_dir="/tmp/evernode-setup-helpers" -secret_backup_location="/root/.evernode/.host-account-secret.key" -previous_secret_path_note=/root/.evernode/previous_secret_path.txt -default_key_filepath="/home/$MB_XRPL_USER/.evernode-host/.host-account-secret.key" - -secret_stored_path="-" function stage() { echo "STAGE $1" # This is picked up by the setup console output filter. } -function confirm() { - echo -en $1" [Y/n] " - local yn="" - read yn $previous_secret_path_note - fi - fi # Uninstall all contract instance users--------------------------- @@ -182,15 +170,6 @@ if grep -q "^$MB_XRPL_USER:" /etc/passwd; then ! confirm "Evernode host deregistration failed. Still do you want to continue uninstallation?" && echo "Aborting uninstallation. Try again later." && exit 1 echo "Continuing uninstallation..." fi - - mb_xrpl_config_data=$(cat $MB_XRPL_DATA/mb-xrpl.cfg) - current_secret_path=$(echo $mb_xrpl_config_data | jq -r '.xrpl.secretPath') - - # Remove secret from the saved location.(This is applied for secrets not specified in default path) - rm -f $current_secret_path - - # Remove Evernode util directory - [ -d "/root/.evernode" ] && rm -rf "/root/.evernode" fi echo "Deleting message board user..." @@ -200,7 +179,6 @@ if grep -q "^$MB_XRPL_USER:" /etc/passwd; then pkill -u $MB_XRPL_USER # Kill any running processes. sleep 0.5 userdel -f "$MB_XRPL_USER" - rm -r /home/"${MB_XRPL_USER:?}" fi diff --git a/installer/setup.sh b/installer/setup.sh index 48a2e0c..087f010 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -82,11 +82,6 @@ noroot_user=${SUDO_USER:-$(whoami)} # Default key path is set to a path in MB_XRPL_USER home default_key_filepath="/home/$MB_XRPL_USER/.evernode-host/.host-account-secret.key" -# Backed up secret location. -# Used to restore secret related to a previous installation attempt -secret_backup_location="/root/.evernode/.host-account-secret.key" - - # Helper to print multi line text. # (When passed as a parameter, bash auto strips spaces and indentation which is what we want) function echomult() { @@ -820,33 +815,39 @@ function generate_and_save_keyfile() { mkdir -p "$key_dir" fi + if [ "$key_file_path" == "$default_key_filepath" ]; then + parent_directory=$(dirname "$key_file_path") + chmod -R 500 "$parent_directory" && \ + chown -R $MB_XRPL_USER: "$parent_directory" || (echomult "Error occurred in permission and ownership assignment of key file directory." && exit 1) + fi + if [ -e "$key_file_path" ]; then - if ! confirm "The file '$key_file_path' already exists. Do you want to override it?"; then + if ! confirm "The file '$key_file_path' already exists. Do you want to override it?" "n"; then + echomult "Continuing with the existing key file." existing_secret=$(jq -r '.xrpl.secret' "$key_file_path" 2>/dev/null) if [ "$existing_secret" != "null" ] && [ "$existing_secret" != "-" ]; then - account_json=$(exec_jshelper generate-account $existing_secret) + account_json=$(exec_jshelper generate-account $existing_secret) || exit 1 xrpl_address=$(jq -r '.address' <<< "$account_json") xrpl_secret=$(jq -r '.secret' <<< "$account_json") - echomult "Retrived account details via secret." + + chmod 400 "$key_file_path" && \ + chown $MB_XRPL_USER: $key_file_path || (echomult "Error occurred in permission and ownership assignment of key file." && exit 1) + echomult "Retrived account details via secret.\n" return 0 else echomult "Error: Existing secret file does not have the expected format." return 1 fi + else + ! confirm "Are you sure you want to override the existing key file?" && exit 1 fi fi - if [ "$key_file_path" == "$default_key_filepath" ]; then - parent_directory=$(dirname "$key_file_path") - chown -R $MB_XRPL_USER: "$parent_directory" - chmod -R 700 "$parent_directory" - fi - echo "{ \"xrpl\": { \"secret\": \"$xrpl_secret\" } }" > "$key_file_path" - chmod 600 "$key_file_path" - echomult "Key file saved successfully at $key_file_path" - - chown $MB_XRPL_USER: $key_file_path + echo "{ \"xrpl\": { \"secret\": \"$xrpl_secret\" } }" > "$key_file_path" && \ + chmod 400 "$key_file_path" && \ + chown $MB_XRPL_USER: $key_file_path && \ + echomult "Key file saved successfully at $key_file_path" || (echomult "Error occurred in permission and ownership assignment of key file." && exit 1) return 0 } @@ -887,25 +888,7 @@ function set_host_xrpl_account() { done fi - # Check for saved secrets due to a previous installation. - if [[ -f "$secret_backup_location" || -f "$key_file_path" ]]; then - - key_file_dir=$(dirname "$key_file_path") - if [ ! -d "$key_file_dir" ]; then - mkdir -p "$key_file_dir" - fi - - if [ -f "$secret_backup_location" ]; then - echomult "Retrived account details via a backed-up secret." && mv $secret_backup_location $key_file_path - fi - - generate_and_save_keyfile "$key_file_path" - - else - - echomult "Generating new keypair for the host...\n" - generate_and_save_keyfile "$key_file_path" - fi + generate_and_save_keyfile "$key_file_path" echomult "Your host account with the address $xrpl_address will be on Xahau $NETWORK. \nThe secret key of the account is located at $key_file_path. @@ -983,8 +966,8 @@ function set_host_xrpl_account() { ! exec_jshelper validate-keys $rippled_server $xrpl_address $xrpl_secret && xrpl_secret="" && continue # Modifying key file ownership to MB_XRPL_USER. - chown $MB_XRPL_USER: $key_file_path - chmod 600 $key_file_path + chown $MB_XRPL_USER: $key_file_path && \ + chmod 400 $key_file_path || (echomult "Error occurred in permission and ownership assignment of key file." && exit 1) xrpl_account_secret=$xrpl_secret @@ -1695,15 +1678,11 @@ if [ "$mode" == "install" ]; then elif [ "$mode" == "uninstall" ]; then + echomult "\nNOTE: By continuing with this, you will not LOSE the SECRET; it remains within the specified path. + \nThe secret path can be found inside the configuration stored at '$MB_XRPL_DATA/mb-xrpl.cfg'." + ! confirm "\nAre you sure you want to uninstall $evernode?" && exit 1 - echomult "\nWARNING! Uninstalling will deregister your host from $evernode and you will LOSE YOUR ACCOUNT address - stored in '$MB_XRPL_DATA/mb-xrpl.cfg' and the secret in the specified path. - \nNOTE: Secret path can be found at '$MB_XRPL_DATA/mb-xrpl.cfg'. - \nThis is irreversible. Make sure you have your account address and - secret elsewhere before proceeding.\n" - - ! confirm "\nHave you read above warning and backed up your account credentials?" && exit 1 # Check contract condtion. check_exisiting_contracts 0 @@ -1724,13 +1703,10 @@ elif [ "$mode" == "transfer" ]; then while allowing you to transfer the registration to a preferred transferee. \n\nAre you sure you want to transfer $evernode registration from this host?" && exit 1 - echomult "\nWARNING! By proceeding this you will LOSE YOUR ACCOUNT address - stored in '$MB_XRPL_DATA/mb-xrpl.cfg' and the secret in the specified path. - \nNOTE: Secret path can be found at '$MB_XRPL_DATA/mb-xrpl.cfg'. - \nThis is irreversible. Make sure you have your account address and - secret elsewhere before proceeding.\n" + echomult "\nNOTE: By continuing with this, you will not LOSE the SECRET; it remains within the specified path. + \nThe secret path can be found inside the configuration stored at '$MB_XRPL_DATA/mb-xrpl.cfg'." - ! confirm "\nHave you read above warning and backed up your account credentials?" && exit 1 + ! confirm "\nAre you sure you want to continue?" && exit 1 fi From eb669106b2d223886b1716f0b47dbf3cdcc8f275 Mon Sep 17 00:00:00 2001 From: Dulana Peiris <57042272+du1ana@users.noreply.github.com> Date: Fri, 15 Dec 2023 09:15:08 +0530 Subject: [PATCH 20/26] Added Regular Key Deletion (#314) --- installer/setup.sh | 36 +++++++++++++++++++++++------------- mb-xrpl/app.js | 4 ++-- mb-xrpl/lib/setup.js | 15 +++++++++++++-- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/installer/setup.sh b/installer/setup.sh index 087f010..c9c1082 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -155,7 +155,7 @@ if $installed ; then && echo "$evernode is already installed on your host. Use the 'evernode' command to manage your host." \ && exit 1 - [ "$1" != "uninstall" ] && [ "$1" != "status" ] && [ "$1" != "list" ] && [ "$1" != "update" ] && [ "$1" != "log" ] && [ "$1" != "applyssl" ] && [ "$1" != "transfer" ] && [ "$1" != "config" ] && [ "$1" != "delete" ] && [ "$1" != "governance" ] && [ "$1" != "auto-update" ] && [ "$1" != "set-regkey" ] \ + [ "$1" != "uninstall" ] && [ "$1" != "status" ] && [ "$1" != "list" ] && [ "$1" != "update" ] && [ "$1" != "log" ] && [ "$1" != "applyssl" ] && [ "$1" != "transfer" ] && [ "$1" != "config" ] && [ "$1" != "delete" ] && [ "$1" != "governance" ] && [ "$1" != "auto-update" ] && [ "$1" != "regkey" ] \ && echomult "$evernode host management tool \nYour host is registered on $evernode. \nSupported commands: @@ -170,7 +170,7 @@ if $installed ; then \nuninstall - Uninstall and deregister from $evernode \ngovernance - Governance candidate management \nauto-update - Evernode Auto Updater management - \nset-regkey - Set regular key" \ + \nregkey - Regular key management" \ && exit 1 elif [ -d $SASHIMONO_BIN ] ; then [ "$1" != "install" ] && [ "$1" != "uninstall" ] \ @@ -204,8 +204,8 @@ if [ "$mode" == "install" ] || [ "$mode" == "uninstall" ] || [ "$mode" == "updat [ -n "$2" ] && [ "$2" != "-q" ] && [ "$2" != "-i" ] && echo "Second arg must be -q (Quiet) or -i (Interactive)" && exit 1 [ "$2" == "-q" ] && interactive=false || interactive=true [ "$mode" == "transfer" ] && transfer=true || transfer=false - [ "$mode" == "set-regkey" ] && set_regkey=true || set_regkey=false - (! $transfer || $installed || $set_regkey) && [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 + [ "$mode" == "regkey" ] && regkey=true || regkey=false + (! $transfer || $installed || $regkey) && [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 fi # Change the relevant setup helper path based on Evernode installation condition and the command mode. @@ -763,8 +763,8 @@ function set_auto_update() { function set_regular_key() { [ "$EUID" -ne 0 ] && echo "Please run with root privileges (sudo)." && exit 1 - ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN set-regkey $1 && - echo "There was an error in setting the regular key." && return 1 + ! sudo -u $MB_XRPL_USER MB_DATA_DIR=$MB_XRPL_DATA node $MB_XRPL_BIN regkey $1 && + echo "There was an error in changing the regular key." && return 1 } function set_transferee_address() { @@ -1804,14 +1804,24 @@ elif [ "$mode" == "auto-update" ]; then \ndisable - Disable $evernode auto updater service." && exit 1 fi -elif [ "$mode" == "set-regkey" ]; then - if [ -z "$2" ]; then - echo "Regular key to be set must be provided." && exit 1 - elif [[ ! "$2" =~ ^[[:alnum:]]{24,34}$ ]]; then - echo "Regular key is invalid." && exit 1 +elif [ "$mode" == "regkey" ]; then + if [ "$2" == "set" ]; then + if [ -z "$3" ]; then + echo "Regular key to be set must be provided." && exit 1 + elif [[ ! "$3" =~ ^[[:alnum:]]{24,34}$ ]]; then + echo "Regular key is invalid." && exit 1 + fi + set_regular_key $3 + exit 0 + elif [ "$2" == "delete" ]; then + set_regular_key + exit 0 + else + echomult "Regular key management tool + \nSupported commands: + \nset [regularKey] - Assign or update the regular key. + \ndelete - Delete the regular key" && exit 1 fi - set_regular_key $2 - exit 0 fi [ "$mode" != "uninstall" ] && check_installer_pending_finish diff --git a/mb-xrpl/app.js b/mb-xrpl/app.js index 98a7682..9bcb790 100644 --- a/mb-xrpl/app.js +++ b/mb-xrpl/app.js @@ -68,7 +68,7 @@ async function main() { else if (process.argv.length >= 4 && process.argv[2] === 'governance') { await GovernanceManager.handleCommand(process.argv[3], ...process.argv.slice(4)); } - else if (process.argv.length === 4 && process.argv[2] === 'set-regkey') { + else if (process.argv.length >= 3 && process.argv[2] === 'regkey') { await new Setup().setRegularKey(process.argv[3]); } else if (process.argv[2] === 'help') { @@ -84,7 +84,7 @@ async function main() { node index.js reconfig [leaseAmount] [totalInstanceCount] [rippledServer] - Update message board configuration. node index.js delete [containerName] - Delete an instance and recreate the lease offer node index.js governance [command] [args] - Governance handling. - node index.js set-regkey [regularKey] - Set regular key. + node index.js regkey [regularKey] - Regular key management. node index.js help - Print help.`); } else { diff --git a/mb-xrpl/lib/setup.js b/mb-xrpl/lib/setup.js index 8434702..ba3f5c7 100644 --- a/mb-xrpl/lib/setup.js +++ b/mb-xrpl/lib/setup.js @@ -602,7 +602,12 @@ class Setup { const acc = this.#getConfig().xrpl; await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); - console.log(`Setting Regular Key...`); + if(regularKey){ + console.log(`Setting Regular Key...`); + } + else{ + console.log(`Deleting Regular Key...`); + } try { await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); @@ -613,7 +618,13 @@ class Setup { const xrplAcc = new evernode.XrplAccount(acc.address, acc.secret, { xrplApi: xrplApi }); await xrplAcc.setRegularKey(regularKey); - console.log(`Regular key ${regularKey} was assigned to account ${acc.address} successfully.`); + + if(regularKey){ + console.log(`Regular key ${regularKey} was assigned to account ${acc.address} successfully.`); + } + else{ + console.log(`Regular key was deleted from account ${acc.address} successfully.`); + } await xrplApi.disconnect(); } From 50027f6dc17a4b38450124b1f17fccfee7855ce5 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Fri, 15 Dec 2023 09:50:15 +0530 Subject: [PATCH 21/26] Modifications related to min XAH amount derivation and licence text. (#312) --- installer/jshelper/index.js | 68 +++++++++++++++++++--- installer/jshelper/package-lock.json | 14 ++--- installer/jshelper/package.json | 2 +- installer/setup.sh | 87 +++++++++++++++++++--------- mb-xrpl/package-lock.json | 14 ++--- mb-xrpl/package.json | 2 +- 6 files changed, 135 insertions(+), 52 deletions(-) diff --git a/installer/jshelper/index.js b/installer/jshelper/index.js index e49dd9b..fb7bd2f 100644 --- a/installer/jshelper/index.js +++ b/installer/jshelper/index.js @@ -9,6 +9,8 @@ const http = require('http'); const crypto = require('crypto'); const { appenv } = require("../../mb-xrpl/lib/appenv"); +let NETWORK = appenv.NETWORK; + function checkParams(args, count) { for (let i = 0; i < count; i++) { if (!args[i]) throw "Params not specified."; @@ -22,7 +24,7 @@ const funcs = { 'validate-server': async (args) => { checkParams(args, 1); const rippledUrl = args[0]; - await evernode.Defaults.useNetwork(appenv.NETWORK); + await evernode.Defaults.useNetwork(NETWORK); evernode.Defaults.set({ rippledServer: rippledUrl }); @@ -39,7 +41,7 @@ const funcs = { const accountAddress = args[2]; const validateFor = args[3] || "register"; - await evernode.Defaults.useNetwork(appenv.NETWORK); + await evernode.Defaults.useNetwork(NETWORK); evernode.Defaults.set({ rippledServer: rippledUrl, @@ -94,7 +96,7 @@ const funcs = { const accountAddress = args[1]; const accountSecret = args[2]; - await evernode.Defaults.useNetwork(appenv.NETWORK); + await evernode.Defaults.useNetwork(NETWORK); evernode.Defaults.set({ rippledServer: rippledUrl @@ -123,7 +125,7 @@ const funcs = { const governorAddress = args[1]; const configName = args[2]; - await evernode.Defaults.useNetwork(appenv.NETWORK); + await evernode.Defaults.useNetwork(NETWORK); evernode.Defaults.set({ rippledServer: rippledUrl, @@ -155,7 +157,7 @@ const funcs = { const accountSecret = args[3]; const transfereeAddress = args[4]; - await evernode.Defaults.useNetwork(appenv.NETWORK); + await evernode.Defaults.useNetwork(NETWORK); evernode.Defaults.set({ rippledServer: rippledUrl, @@ -247,7 +249,7 @@ const funcs = { const governorAddress = args[1]; const accountAddress = args[2]; - await evernode.Defaults.useNetwork(appenv.NETWORK); + await evernode.Defaults.useNetwork(NETWORK); evernode.Defaults.set({ rippledServer: rippledUrl, @@ -297,7 +299,7 @@ const funcs = { const WAIT_PERIOD = 120; // seconds - await evernode.Defaults.useNetwork(appenv.NETWORK); + await evernode.Defaults.useNetwork(NETWORK); evernode.Defaults.set({ rippledServer: rippledUrl, @@ -383,7 +385,7 @@ const funcs = { const WAIT_PERIOD = 120; // seconds - await evernode.Defaults.useNetwork(appenv.NETWORK); + await evernode.Defaults.useNetwork(NETWORK); evernode.Defaults.set({ rippledServer: rippledUrl, @@ -506,7 +508,47 @@ const funcs = { } catch (errorCode) { return { success: false, result: errorCode }; } + }, + + 'compute-xah-requirement': async (args) => { + checkParams(args, 2); + const rippledUrl = args[0]; + const incReserveCount = Number(args[1]); + + await evernode.Defaults.useNetwork(NETWORK); + + evernode.Defaults.set({ + rippledServer: rippledUrl + }); + + try { + const xrplApi = new evernode.XrplApi(null, { autoReconnect: false }); + await xrplApi.connect(); + + evernode.Defaults.set({ + xrplApi: xrplApi + }); + + const serverInfo = await xrplApi.getServerInfo(); + if (serverInfo?.info?.validated_ledger) { + const reserves = serverInfo.info.validated_ledger + const estimate = (reserves?.reserve_base_native ?? reserves?.reserve_base_xrp) + (reserves?.reserve_inc_native ?? reserves?.reserve_inc_xrp) * incReserveCount; + + if (estimate > 0) { + await xrplApi.disconnect(); + return { success: true, result: `${estimate}` }; + } + } + + await xrplApi.disconnect(); + return { success: false, result: "Failed to retrieve the estimation." }; + + + } catch { + return { success: false, result: "Error occurred in websocket connection." }; + } } + } function handleResponse(resp) { @@ -525,10 +567,20 @@ function handleResponse(resp) { async function app() { try { + const networkIdx = process.argv.findIndex(a => a.startsWith('network:')); + if (networkIdx >= 0) { + const sp = process.argv[networkIdx].split(':'); + if (sp.length > 1 && sp[1]) { + NETWORK = sp[1]; + process.argv.splice(networkIdx, 1); + } + } + const command = process.argv[2]; if (!command) throw "Command not specified."; + const resp = await funcs[command](process.argv.splice(3)); if (!resp) throw "No response."; diff --git a/installer/jshelper/package-lock.json b/installer/jshelper/package-lock.json index b442faa..90e3428 100644 --- a/installer/jshelper/package-lock.json +++ b/installer/jshelper/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "evernode-setup-helper", "dependencies": { - "evernode-js-client": "0.6.23", + "evernode-js-client": "0.6.24", "ip6addr": "0.2.5", "ripple-keypairs": "1.3.1" } @@ -364,9 +364,9 @@ } }, "node_modules/evernode-js-client": { - "version": "0.6.23", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.23.tgz", - "integrity": "sha512-MXnQLkusq8UGDcf91bSmm9AVOQ3DSfSCJwWbuQpZDsXflbK5X6Ivxjj3ABQafGjAtQLn2KDKtlfzEwRzZvpkOQ==", + "version": "0.6.24", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.24.tgz", + "integrity": "sha512-sT7eoN796ueo0+yZl6KpQOzINuXrYW88YlZCm2PxzRqv5G4TqgqUGFgs+GSESkxuVRkw2LBX9WcUCGwbAt6K9g==", "dependencies": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", @@ -1543,9 +1543,9 @@ } }, "evernode-js-client": { - "version": "0.6.23", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.23.tgz", - "integrity": "sha512-MXnQLkusq8UGDcf91bSmm9AVOQ3DSfSCJwWbuQpZDsXflbK5X6Ivxjj3ABQafGjAtQLn2KDKtlfzEwRzZvpkOQ==", + "version": "0.6.24", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.6.24.tgz", + "integrity": "sha512-sT7eoN796ueo0+yZl6KpQOzINuXrYW88YlZCm2PxzRqv5G4TqgqUGFgs+GSESkxuVRkw2LBX9WcUCGwbAt6K9g==", "requires": { "elliptic": "6.5.4", "libsodium-wrappers": "0.7.10", diff --git a/installer/jshelper/package.json b/installer/jshelper/package.json index c195561..edd39eb 100644 --- a/installer/jshelper/package.json +++ b/installer/jshelper/package.json @@ -4,7 +4,7 @@ "build": "ncc build index.js --minify -o dist" }, "dependencies": { - "evernode-js-client": "0.6.23", + "evernode-js-client": "0.6.24", "ip6addr": "0.2.5", "ripple-keypairs": "1.3.1" } diff --git a/installer/setup.sh b/installer/setup.sh index c9c1082..11651c1 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -34,7 +34,7 @@ fi # Prepare resources URLs resource_storage="https://github.com/$repo_owner/$repo_name/releases/download/$latest_version" -licence_url="https://raw.githubusercontent.com/$repo_owner/$repo_name/$desired_branch/sashimono/installer/licence.txt" +licence_url="https://raw.githubusercontent.com/$repo_owner/$repo_name/$desired_branch/license/evernode-license.pdf" config_url="https://raw.githubusercontent.com/$repo_owner/$repo_name/$desired_branch/definitions/definitions.json" setup_script_url="$resource_storage/setup.sh" installer_url="$resource_storage/installer.tar.gz" @@ -47,7 +47,9 @@ nodejs_util_bin="/usr/bin/node" jshelper_bin="$setup_helper_dir/jshelper/index.js" config_json_path="$setup_helper_dir/configuration.json" operation="register" -min_xrp_amount_per_month=25 +min_operational_cost_per_month=5 +# 3 Month initial operational duration is considered. +initial_operational_duration=3 spinner=( '|' '/' '-' '\'); xrpl_address="-" xrpl_secret="-" @@ -260,7 +262,7 @@ function check_prereq() { # Check qrencode command is installed. if ! command -v qrencode &>/dev/null; then - stage "qrencode command not found. Installing.." + echo "qrencode command not found. Installing.." apt-get install -y qrencode >/dev/null fi } @@ -348,7 +350,7 @@ function exec_jshelper() { [ -p $resp_file ] || sudo -u $noroot_user mkfifo $resp_file # Execute js helper asynchronously while collecting response to fifo file. - sudo -u $noroot_user RESPFILE=$resp_file $nodejs_util_bin $jshelper_bin "$@" >/dev/null 2>&1 & + sudo -u $noroot_user RESPFILE=$resp_file $nodejs_util_bin $jshelper_bin "$@" "network:$NETWORK" >/dev/null 2>&1 & local pid=$! local result=$(cat $resp_file) && [ "$result" != "-" ] && echo $result @@ -364,7 +366,7 @@ function exec_jshelper_root() { [ -p $resp_file ] || mkfifo $resp_file # Execute js helper asynchronously while collecting response to fifo file. - RESPFILE=$resp_file $nodejs_util_bin $jshelper_bin "$@" >/dev/null 2>&1 & + RESPFILE=$resp_file $nodejs_util_bin $jshelper_bin "$@" "network:$NETWORK" >/dev/null 2>&1 & local pid=$! local result=$(cat $resp_file) && [ "$result" != "-" ] && echo $result @@ -754,7 +756,7 @@ function set_rippled_server() { function set_auto_update() { enable_auto_update=false if $interactive; then - if confirm "Do you want to enable auto updates?" "n" ; then + if confirm "\nDo you want to subscribe for auto-updates?\nNOTE: The auto-update service is offered subject to the terms set out in the Evernode Software Licence." "n" ; then enable_auto_update=true fi fi @@ -799,7 +801,7 @@ function generate_qrcode() { function generate_and_save_keyfile() { - local account_json=$(exec_jshelper generate-account) + account_json=$(exec_jshelper generate-account) || { echo "Error occurred in account setting up."; exit 1; } xrpl_address=$(jq -r '.address' <<< "$account_json") xrpl_secret=$(jq -r '.secret' <<< "$account_json") @@ -818,38 +820,40 @@ function generate_and_save_keyfile() { if [ "$key_file_path" == "$default_key_filepath" ]; then parent_directory=$(dirname "$key_file_path") chmod -R 500 "$parent_directory" && \ - chown -R $MB_XRPL_USER: "$parent_directory" || (echomult "Error occurred in permission and ownership assignment of key file directory." && exit 1) + chown -R $MB_XRPL_USER: "$parent_directory" || { echomult "Error occurred in permission and ownership assignment of key file directory."; exit 1; } fi if [ -e "$key_file_path" ]; then - if ! confirm "The file '$key_file_path' already exists. Do you want to override it?" "n"; then + if confirm "The file '$key_file_path' already exists. Do you want to continue using that key file?\nPressing 'n' would terminate the installation." ; then echomult "Continuing with the existing key file." existing_secret=$(jq -r '.xrpl.secret' "$key_file_path" 2>/dev/null) if [ "$existing_secret" != "null" ] && [ "$existing_secret" != "-" ]; then - account_json=$(exec_jshelper generate-account $existing_secret) || exit 1 + account_json=$(exec_jshelper generate-account $existing_secret) || { echomult "Error occurred when existing account retrieval."; exit 1; } xrpl_address=$(jq -r '.address' <<< "$account_json") xrpl_secret=$(jq -r '.secret' <<< "$account_json") chmod 400 "$key_file_path" && \ - chown $MB_XRPL_USER: $key_file_path || (echomult "Error occurred in permission and ownership assignment of key file." && exit 1) + chown $MB_XRPL_USER: $key_file_path || { echomult "Error occurred in permission and ownership assignment of key file."; exit 1; } echomult "Retrived account details via secret.\n" return 0 else echomult "Error: Existing secret file does not have the expected format." - return 1 + exit 1 fi else - ! confirm "Are you sure you want to override the existing key file?" && exit 1 + exit 1 fi + else + + echo "{ \"xrpl\": { \"secret\": \"$xrpl_secret\" } }" > "$key_file_path" && \ + chmod 400 "$key_file_path" && \ + chown $MB_XRPL_USER: $key_file_path && \ + echomult "Key file saved successfully at $key_file_path" || { echomult "Error occurred in permission and ownership assignment of key file."; exit 1; } + + return 0 fi - - echo "{ \"xrpl\": { \"secret\": \"$xrpl_secret\" } }" > "$key_file_path" && \ - chmod 400 "$key_file_path" && \ - chown $MB_XRPL_USER: $key_file_path && \ - echomult "Key file saved successfully at $key_file_path" || (echomult "Error occurred in permission and ownership assignment of key file." && exit 1) - - return 0 + exit 1 } function set_host_xrpl_account() { @@ -887,13 +891,20 @@ function set_host_xrpl_account() { done fi + + # min_xah_requirement => reserve_base_xrp + reserve_inc_xrp * n + # reserve_inc_xrp * n => trustline reserve + reg_token_reserve + (reserve_inc_xrp * instance_count) + local inc_reserves_count=$((1 + 1 + $alloc_instcount)) + min_reserve_requirement=$(exec_jshelper compute-xah-requirement $rippled_server $inc_reserves_count) || { echomult "Error occuured in checking XAH requirement."; exit 1; } + + min_xah_requirement=$(echo "$min_operational_cost_per_month*$initial_operational_duration + $min_reserve_requirement" | bc ) generate_and_save_keyfile "$key_file_path" echomult "Your host account with the address $xrpl_address will be on Xahau $NETWORK. \nThe secret key of the account is located at $key_file_path. \n\nThis is the account that will represent this host on the Evernode host registry. You need to load up the account with following funds in order to continue with the installation. - \n1. At least $min_xrp_amount_per_month XAH (Xahau XRP) to cover regular transaction fees for first month. + \n1. At least $min_xah_requirement XAH to cover regular transaction fees for the first three months. \n2. At least $reg_fee EVR to cover Evernode registration fee. \n\nYou can scan the following QR code in your wallet app to send funds based on the account condition:\n" @@ -912,9 +923,9 @@ function set_host_xrpl_account() { if [ "$account_condition" == "${AccCondtionArry[0]}" ]; then - echomult "To set up your host account, ensure a deposit of $min_xrp_amount_per_month XAH (Xahau XRP) to cover the regular transaction fees for the first month." + echomult "To set up your host account, ensure a deposit of $min_xah_requirement XAH to cover the regular transaction fees for the first three months." - required_balance=$min_xrp_amount_per_month + required_balance=$min_xah_requirement while true ; do wait_call "exec_jshelper check-balance $rippled_server $EVERNODE_GOVERNOR_ADDRESS $xrpl_address NATIVE $required_balance" "Thank you. [OUTPUT] XAH balance is there in your host account." \ && break @@ -958,6 +969,17 @@ function set_host_xrpl_account() { read -ep "Specify the path of the Host Account secret: " key_file_path Date: Fri, 15 Dec 2023 11:05:19 +0530 Subject: [PATCH 22/26] Added content --- evernode-bootstrap-contract | 2 +- evernode-license.pdf | Bin 0 -> 128503 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 evernode-license.pdf diff --git a/evernode-bootstrap-contract b/evernode-bootstrap-contract index d2b01a0..13b6f70 160000 --- a/evernode-bootstrap-contract +++ b/evernode-bootstrap-contract @@ -1 +1 @@ -Subproject commit d2b01a0700534c3f404f11f7e7cfa4ba692d147d +Subproject commit 13b6f708bf86af5a8518d27f7573291a5fdeeaab diff --git a/evernode-license.pdf b/evernode-license.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2a9691559a8d56cc08ba75cf8cd0bb074e790fb3 GIT binary patch literal 128503 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%0Awdt5>W&w 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,sZZt1b>I_gJ^=J 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 Date: Fri, 15 Dec 2023 14:02:42 +0530 Subject: [PATCH 23/26] SHA256 hash of TOS and message modification (#316) --- installer/setup.sh | 3 +++ mb-xrpl/lib/appenv.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/installer/setup.sh b/installer/setup.sh index 11651c1..f6298b3 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -903,6 +903,9 @@ function set_host_xrpl_account() { echomult "Your host account with the address $xrpl_address will be on Xahau $NETWORK. \nThe secret key of the account is located at $key_file_path. + \nNOTE: It is your responsibility to safeguard/backup this file in a secure manner. + \nIf you lose it, you will not be able to access any funds in your Host account. NO ONE else can recover it. + \n\nThis is the account that will represent this host on the Evernode host registry. You need to load up the account with following funds in order to continue with the installation. \n1. At least $min_xah_requirement XAH to cover regular transaction fees for the first three months. \n2. At least $reg_fee EVR to cover Evernode registration fee. diff --git a/mb-xrpl/lib/appenv.js b/mb-xrpl/lib/appenv.js index 44d08e5..23747c7 100644 --- a/mb-xrpl/lib/appenv.js +++ b/mb-xrpl/lib/appenv.js @@ -26,7 +26,7 @@ appenv = { SASHIMONO_SCHEDULER_INTERVAL_SECONDS: 2, SASHI_CLI_PATH: appenv.IS_DEV_MODE ? "../build/sashi" : "/usr/bin/sashi", MB_VERSION: '0.8.0', - TOS_HASH: '757A0237B44D8B2BBB04AE2BAD5813858E0AECD2F0B217075E27E0630BA74314', // This is the sha256 hash of TOS text. + TOS_HASH: '0801677EBCB2F76EF97D531549D8B27DB2C7A4A8EE7F60032AE40184247F0810', // This is the sha256 hash of EVERNODE-HOSTING-PRINCIPLES.pdf. NETWORK: 'mainnet' } From db3d08fbdc95bc30b954fe551040ee18e5554184 Mon Sep 17 00:00:00 2001 From: Kithmini Gunawardhana Date: Fri, 15 Dec 2023 19:19:55 +0530 Subject: [PATCH 24/26] Added additional emptiness check for sashi json command (#318) --- sashi-cli/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sashi-cli/main.cpp b/sashi-cli/main.cpp index 208094d..fc37b4f 100644 --- a/sashi-cli/main.cpp +++ b/sashi-cli/main.cpp @@ -135,14 +135,14 @@ int parse_cmd(int argc, char **argv) if (!is_dev_mode) { if(create->parsed()){ - std::cout << "Developer mode must be enabled to access this command." << std::endl; + std::cout << "Command not supported: Run with --help or --help-all for more information." << std::endl; return -1; } - if(json->parsed()){ + if(json->parsed() && !json_message.empty()){ jsoncons::json json_data = jsoncons::json::parse(json_message); if (json_data.contains("type") && json_data["type"].as_string() == "create") { - std::cout << "Developer mode must be enabled to access this command." << std::endl; + std::cout << "Command not supported: Run with --help or --help-all for more information." << std::endl; return -1; } } From beb19d4a3d54702dba4800171acfd88516ccd1c1 Mon Sep 17 00:00:00 2001 From: Chalith Desaman Date: Sun, 17 Dec 2023 18:56:00 +0530 Subject: [PATCH 25/26] Changed parseInt to parseFloat in change config (#319) --- mb-xrpl/lib/setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mb-xrpl/lib/setup.js b/mb-xrpl/lib/setup.js index ba3f5c7..9b6e1d4 100644 --- a/mb-xrpl/lib/setup.js +++ b/mb-xrpl/lib/setup.js @@ -359,7 +359,7 @@ class Setup { else if (totalInstanceCount && isNaN(totalInstanceCount)) throw 'Maximum instance count should be a number'; - const leaseAmountParsed = leaseAmount ? parseInt(leaseAmount) : 0; + const leaseAmountParsed = leaseAmount ? parseFloat(leaseAmount) : 0; const totalInstanceCountParsed = totalInstanceCount ? parseInt(totalInstanceCount) : 0; // Return if not changed. From e99441c75246d04df03406bb5eaaf06e416d6e2f Mon Sep 17 00:00:00 2001 From: Chalith Desaman Date: Wed, 20 Dec 2023 10:27:41 +0530 Subject: [PATCH 26/26] Removed unnecessary env variable pass (#321) --- installer/sashimono-install.sh | 2 +- installer/setup.sh | 3 --- mb-xrpl/app.js | 4 ++-- mb-xrpl/lib/setup.js | 27 ++++++--------------------- 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/installer/sashimono-install.sh b/installer/sashimono-install.sh index cc5752a..9663229 100755 --- a/installer/sashimono-install.sh +++ b/installer/sashimono-install.sh @@ -326,7 +326,7 @@ else fi if [[ "$NO_MB" == "" && -f $MB_XRPL_DATA/mb-xrpl.cfg ]]; then - ! sudo -u "$MB_XRPL_USER" MB_DATA_DIR="$MB_XRPL_DATA" node "$MB_XRPL_BIN" upgrade $EVERNODE_GOVERNOR_ADDRESS && rollback + ! sudo -u "$MB_XRPL_USER" MB_DATA_DIR="$MB_XRPL_DATA" node "$MB_XRPL_BIN" upgrade && rollback fi # Install Sashimono Agent systemd service. diff --git a/installer/setup.sh b/installer/setup.sh index f6298b3..eae5e18 100755 --- a/installer/setup.sh +++ b/installer/setup.sh @@ -1795,9 +1795,6 @@ elif [ "$mode" == "list" ]; then sashi list elif [ "$mode" == "update" ]; then - config_json_path="$SASHIMONO_BIN/evernode-setup-helpers/configuration.json" - export EVERNODE_GOVERNOR_ADDRESS=${OVERRIDE_EVERNODE_GOVERNOR_ADDRESS:-$(jq -r ".$NETWORK.governorAddress" $config_json_path)} - update_evernode elif [ "$mode" == "log" ]; then diff --git a/mb-xrpl/app.js b/mb-xrpl/app.js index 9bcb790..6143755 100644 --- a/mb-xrpl/app.js +++ b/mb-xrpl/app.js @@ -46,8 +46,8 @@ async function main() { else if (process.argv.length === 4 && process.argv[2] === 'reginfo' && process.argv[3] === 'basic') { await new Setup().regInfo(true); } - else if (process.argv.length === 4 && process.argv[2] === 'upgrade') { - await new Setup().upgrade(process.argv[3]); + else if (process.argv.length >= 3 && process.argv[2] === 'upgrade') { + await new Setup().upgrade(); } else if ((process.argv.length === 8) && process.argv[2] === 'reconfig') { if (process.argv[5] == '-') process.argv[5] = null; diff --git a/mb-xrpl/lib/setup.js b/mb-xrpl/lib/setup.js index 9b6e1d4..031f99e 100644 --- a/mb-xrpl/lib/setup.js +++ b/mb-xrpl/lib/setup.js @@ -220,7 +220,7 @@ class Setup { } // Upgrades existing message board data to the new version. - async upgrade(governorAddress) { + async upgrade() { // Do a simple version change in the config. const cfg = this.#getConfig(); @@ -230,21 +230,6 @@ class Setup { if (!cfg.xrpl.rippledServer) cfg.xrpl.rippledServer = appenv.DEFAULT_RIPPLED_SERVER - if (!cfg.xrpl.governorAddress) { - await setEvernodeDefaults(cfg.xrpl.network, governorAddress, cfg.xrpl.rippledServer); - - const hostClient = new evernode.HostClient(cfg.xrpl.address, cfg.xrpl.secret); - await hostClient.connect(); - - evernode.Defaults.set({ - xrplApi: hostClient.xrplApi - }); - - cfg.xrpl.governorAddress = governorAddress; - - await hostClient.disconnect(); - } - this.#saveConfig(cfg); await Promise.resolve(); // async placeholder. @@ -602,10 +587,10 @@ class Setup { const acc = this.#getConfig().xrpl; await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer); - if(regularKey){ + if (regularKey) { console.log(`Setting Regular Key...`); } - else{ + else { console.log(`Deleting Regular Key...`); } @@ -619,10 +604,10 @@ class Setup { await xrplAcc.setRegularKey(regularKey); - if(regularKey){ + if (regularKey) { console.log(`Regular key ${regularKey} was assigned to account ${acc.address} successfully.`); - } - else{ + } + else { console.log(`Regular key was deleted from account ${acc.address} successfully.`); }