#!/bin/bash account_addr="" docker_swarm_started=0 voi_home="${HOME}/voi" headless_install=0 is_root=0 skip_account_setup=0 migrate_host_based_setup=0 wallet_password="" new_user_setup=0 new_network=0 network_status_url="" network_identifier="" staking_url="" container_id="" latest_key_end_block="" previous_network_addresses="" bold=$(tput bold) normal=$(tput sgr0) execute_sudo() { if sudo -v &> /dev/null; then if [[ $(id -u) -eq 0 ]]; then bash -c "$1" else sudo bash -c "$1" fi else abort "Your user does not have sudo privileges. Exiting the program." fi } abort() { if [[ -z "$2" || "$2" != "true" ]]; then if [[ ${docker_swarm_started} -eq 1 ]]; then echo "Shutting down docker swarm" echo "" execute_sudo 'docker swarm leave --force' fi fi echo "$1" exit 1 } execute_docker_command_internal() { local interactive=$1 local command=$2 local options="" if [[ $interactive = "yes" ]]; then options="-it" fi execute_sudo "docker exec -e account_addr=${account_addr} ${options} \"${container_id}\" bash -c \"${command}\"" } execute_interactive_docker_command() { local retries=0 while [[ $retries -lt 10 ]]; do execute_docker_command_internal "yes" "$1" local exit_code=$? if [[ ${exit_code} -eq 130 ]]; then exit ${exit_code} >&2 fi if [[ ${exit_code} -ne 0 ]]; then echo "Error executing command. Please try again or press CTRL-C to exit." ((retries++)) else break fi done if [[ $retries -eq 10 ]]; then abort "Command failed after 10 attempts. Exiting the program." fi } execute_docker_command() { execute_docker_command_internal "no" "$1" } cleanup_deprecated_files_and_folders() { if [[ -d ${voi_home}/docker-swarm ]]; then rm -rf "${voi_home}/docker-swarm" 2>/dev/null || echo "Failed to delete deprecated folder: ${voi_home}/docker-swarm" fi if [[ -f ${voi_home}/profile ]]; then rm -rf "${voi_home}/profile" 2>/dev/null || echo "Failed to delete deprecated file: ${voi_home}/profile" fi if [[ -f ${voi_home}/.profile ]]; then if grep -q "^export VOINETWORK_IMPORT_ACCOUNT=" "${voi_home}/.profile"; then sed -i '/^export VOINETWORK_IMPORT_ACCOUNT=/d' "${voi_home}/.profile" unset VOINETWORK_IMPORT_ACCOUNT fi fi } start_docker_swarm() { local docker_swarm docker_swarm=$(execute_sudo 'docker info | grep Swarm | cut -d\ -f3') if [[ ${docker_swarm} != "active" ]]; then command="docker swarm init" if [[ -n ${VOINETWORK_DOCKER_SWARM_INIT_SETTINGS} ]]; then command+=" ${VOINETWORK_DOCKER_SWARM_INIT_SETTINGS}" else command+=" --listen-addr lo --advertise-addr lo" fi execute_sudo "$command" # shellcheck disable=SC2181 if [ $? -ne 0 ]; then docker_swarm_instructions fi docker_swarm_started=1 fi } start_stack() { case ${VOINETWORK_PROFILE} in "relay") docker_file="${voi_home}/docker/relay.yml" ;; "archiver") docker_file="${voi_home}/docker/archiver.yml" ;; "developer") docker_file="${voi_home}/docker/developer.yml" ;; "participation") docker_file="${voi_home}/docker/compose.yml" ;; *) abort "Invalid profile. Exiting the program." ;; esac command="source ${voi_home}/.profile && docker stack deploy -c ${docker_file}" if [[ -f "${voi_home}/docker/notification.yml" ]]; then command+=" -c ${voi_home}/docker/notification.yml" fi command+=" voinetwork" execute_sudo "$command" # shellcheck disable=SC2181 if [[ $? -ne 0 ]]; then abort "Error starting stack. Exiting the program." fi } # shellcheck disable=SC2120 wait_for_stack_to_be_ready() { while true; do service_info=$(execute_sudo 'docker stack ps voinetwork --format json') stack_ready=true while read -r line; do current_state=$(echo "${line}" | jq -r '.CurrentState') desired_state=$(echo "${line}" | jq -r '.DesiredState') service_error=$(echo "${line}" | jq -r '.Error') if [[ (${desired_state} == "Ready" || ${desired_state} == "Running") && ${current_state} != Running* ]]; then stack_ready=false break fi done < <(echo "${service_info}") if [[ ${stack_ready} == false && -n ${service_error} ]]; then local number_of_interfaces number_of_interfaces=$(execute_sudo "ip -d link show | grep vx | wc -l") if [[ number_of_interfaces -le 2 ]]; then echo "Docker has a network interface that is lingering and preventing startup. We'll attempt to auto-delete it." execute_sudo "ip -d link show | grep vx | grep DOWN | awk '{print $2}' | tr -d ':' | xargs -rn1 ip link delete" # shellcheck disable=SC2181 if [[ $? -ne 0 ]]; then echo "Docker swarm is unable to start services. https://github.com/moby/libnetwork/issues/1765" abort "Exiting the program." fi elif [[ number_of_interfaces -ge 3 ]]; then echo "Docker swarm is unable to start services. https://github.com/moby/libnetwork/issues/1765" abort "Multiple vx interfaces found. Please delete all vx interfaces or reboot the server" fi fi echo "Waiting for stack to be ready..." sleep 2 if [[ ${stack_ready} == true ]]; then break fi done display_banner "Stack is ready!" } function get_container_id() { case ${VOINETWORK_PROFILE} in "relay") container_id=$(docker ps -q -f name=voinetwork_relay) ;; "developer") container_id=$(docker ps -q -f name=voinetwork_developer) ;; "archiver") container_id=$(docker ps -q -f name=voinetwork_archiver) ;; "participation") container_id=$(docker ps -q -f name=voinetwork_algod) ;; *) abort "Invalid profile. Exiting the program." ;; esac } verify_node_is_running() { local retries=0 local max_retries=5 while [[ $retries -lt $max_retries ]]; do if [[ -n "$container_id" ]]; then execute_sudo "docker exec ${container_id} bash -c \"curl -sS http://localhost:8080/health > /dev/null\"" local exit_code=$? if [[ $exit_code -eq 0 ]]; then break fi fi echo "Error connecting to node. Retrying in 10 seconds..." sleep 10 ((retries++)) done if [[ $retries -eq $max_retries ]]; then abort "Error connecting to node after $max_retries attempts. Exiting the program." fi } get_current_net_round() { local retries=0 local max_retries=5 while [[ $retries -lt $max_retries ]]; do current_net_round=$(curl -s ${network_status_url} | jq -r '.["last-round"]' ) exit_code=$? if [[ $exit_code -eq 0 ]] && [[ -n $current_net_round ]]; then break fi echo "Error fetching network status. Retrying in 5 seconds..." sleep 5 ((retries++)) done if [[ $retries -eq $max_retries ]]; then abort "Error fetching network status after $max_retries attempts. Please check your internet connection and try again." fi } get_node_status() { current_node_round=$(execute_docker_command '/node/bin/goal node lastround | head -n 1') if [[ ${network_status_url} != "" ]]; then get_current_net_round current_node_round=${current_node_round//[!0-9]/} fi } set_network_status_url() { case ${VOINETWORK_NETWORK} in "mainnet") network_status_url="https://mainnet-api.voi.nodely.dev/v2/status" ;; "betanet") network_status_url="https://betanet-api.voi.nodely.dev/v2/status" ;; "testnet") network_status_url="https://testnet-api.voi.nodely.dev/v2/status" ;; "testnet-v1.1") network_status_url="https://testnet-api.voi.nodely.dev/v2/status" ;; *) abort "Unable to find status URL. Swarm is running in the background." true ;; esac } set_staking_url() { case ${VOINETWORK_NETWORK} in "mainnet") staking_url="https://mainnet-idx.nautilus.sh/v1/scs/accounts" ;; "betanet") staking_url="https://betanet-idx.nautilus.sh/v1/scs/accounts" ;; "testnet") staking_url="https://arc72-idx.nautilus.sh/v1/scs/accounts" ;; "testnet-v1.1") staking_url="https://arc72-idx.nautilus.sh/v1/scs/accounts" ;; *) abort "Unable to find staking URL. Exiting the program." ;; esac } set_network_identifier() { case ${VOINETWORK_NETWORK} in "mainnet") network_identifier="voimain-v1.0" ;; "betanet") network_identifier="voibeta-v1.0" ;; "testnet") network_identifier="voitest-v1.1" ;; "testnet-v1.1") network_identifier="voitest-v1.1" ;; *) network_identifier="voimain-v1.0" ;; esac } set_docker_image() { if [[ -z ${VOINETWORK_DOCKER_IMAGE} ]]; then case ${VOINETWORK_PROFILE} in "relay") VOINETWORK_DOCKER_IMAGE="ghcr.io/voinetwork/voi-node-${VOINETWORK_NETWORK}:latest" ;; "developer") VOINETWORK_DOCKER_IMAGE="ghcr.io/voinetwork/voi-node-${VOINETWORK_NETWORK}:latest" ;; "archiver") VOINETWORK_DOCKER_IMAGE="ghcr.io/voinetwork/voi-node-${VOINETWORK_NETWORK}:latest" ;; "participation") VOINETWORK_DOCKER_IMAGE="ghcr.io/voinetwork/voi-node-participation-${VOINETWORK_NETWORK}:latest" ;; *) abort "Invalid profile. Exiting the program." ;; esac fi update_profile_setting "VOINETWORK_DOCKER_IMAGE" "${VOINETWORK_DOCKER_IMAGE}" } get_network_identifier() { case $1 in "mainnet") echo "voimain-v1.0" ;; "betanet") echo "voibeta-v1.0" ;; "testnet") echo "voitest-v1.1" ;; "testnet-v1.1") echo "voitest-v1.1" ;; *) echo "voitest-v1.1" ;; esac } catchup_node() { display_banner "Catching up with the network... This might take some time, and numbers might briefly increase" set_network_status_url get_node_status while [[ ${current_node_round} -lt ${current_net_round} ]]; do rounds_to_go=$((current_net_round - current_node_round)) if [[ ${rounds_to_go} -gt 1 ]]; then printf "\rWaiting for catchup: %d blocks to go " "${rounds_to_go}" else printf "\rWaiting for catchup: One more block to go! " fi get_node_status sleep 5 done echo "" display_banner "Caught up with the network!" } ask_for_password() { local password local password_repeat while true; do # shellcheck disable=SC2162 read -sp "Password: " password echo # shellcheck disable=SC2162 read -sp "Repeat password: " password_repeat echo if [[ ${password} == "${password_repeat}" ]]; then wallet_password=${password} break else # shellcheck disable=SC2028 echo -e "\n" echo "Passwords do not match. Please try again." fi done } create_wallet() { if [[ $(execute_docker_command "/node/bin/goal wallet list | wc -l") -eq 1 ]]; then local kmd_token echo "Let's create a new wallet for you. Please provide a password for security." kmd_token=$(get_kmd_token) ask_for_password read -r -d '' json_data <> ~/.bashrc && source ~/.bashrc" echo "" # if [[ ${skip_account_setup} -eq 0 && ${VOINETWORK_PROFILE} == "participation" ]]; then # echo "${bold}*********************************** READ THIS! ***********************************${normal}" # echo "After joining the network, it might take up to 2 hours for your server to appear on telemetry" # echo "tracking services. Initially, you can identify your server using the 12-digit short GUID shown by" # echo "the command ${voi_home}/bin/get-node-status." # echo "" # echo "At first, your node's health scores ${bold}will be low${normal}. ${bold}This is normal.${normal}" # echo "After running your node for 5-7 days, you should see the health score increase." # echo "" # fi } check_staking_accounts() { account_addresses=$(get_account_addresses) set_staking_url for account in ${account_addresses}; do local staking_endpoint local response local http_code local json_response staking_endpoint="${staking_url}?owner=${account}&deleted=0" response=$(curl -s --max-time 5 -w "%{http_code}" "${staking_endpoint}") http_code=$(echo "${response}" | awk '{print substr($0, length($0) - 2)}') json_response=$(echo "${response}" | awk '{print substr($0, 1, length($0) - 3)}') if [[ "${http_code}" -eq 200 ]]; then accounts_length=$(jq '.accounts | length' <<< "${json_response}") if [[ "${accounts_length}" -gt 0 ]]; then display_banner "Staking contract detected" echo "Staking contract has been detected for owner ${account}" echo "" local balance balance=$(get_account_balance "${account}") if [[ ${balance} -lt "1000" ]]; then echo "Balance is below 0.001 Voi. Skipping staking account setup." continue fi local staking_accounts staking_accounts=$(jq -c '.accounts[]' <<< "${json_response}") for staking_account in ${staking_accounts}; do local last_committed_block local contract_address local contract_id local part_vote_k local part_vote_lst local participation_key_id contract_id=$(jq -r '.contractId' <<< "${staking_account}") contract_address=$(jq -r '.contractAddress' <<< "${staking_account}") last_committed_block=$(get_last_committed_block) part_vote_k=$(jq -r '.part_vote_k' <<< "${staking_account}") part_vote_lst=$(jq -r '.part_vote_lst' <<< "${staking_account}") # shellcheck disable=SC2046 participation_key_id=$(get_participation_key_id_from_vote_key "${part_vote_k}") if [[ "${part_vote_k}" == "null" || $((part_vote_lst-last_committed_block)) -le 417104 || -z "${participation_key_id}" ]]; then if [[ "${part_vote_k}" == "null" ]]; then echo "No staking participation key detected." elif [[ -z ${participation_key_id} ]]; then echo "Registered staking participation key is not installed locally." else local existing_expiration_date existing_expiration_date=$(get_participation_expiration_eta "${part_vote_lst}" "${last_committed_block}") echo "Current participation key for account ${account} is expected to expire at: ${existing_expiration_date}" echo "Currently the network is at block: ${last_committed_block}" echo "Current participation key expires at block: ${part_vote_lst}" echo "" echo "This is below the required threshold of 417,104 blocks / ~14 days." fi echo "" echo "Generating new key for owner ${account} and contract address ${contract_address}" generate_new_key "${contract_address}" local partkey_info local voting_key local selection_key local first_round local last_round local key_dilution local stateproof_key partkey_info=$(get_most_recent_participation_key_details "${contract_address}") voting_key=$(jq -r '.voting_key' <<< "${partkey_info}") selection_key=$(jq -r '.selection_key' <<< "${partkey_info}") first_round=$(jq -r '.first_round' <<< "${partkey_info}") last_round=$(jq -r '.last_valid' <<< "${partkey_info}") key_dilution=$(jq -r '.key_dilution' <<< "${partkey_info}") stateproof_key=$(jq -r '.stateproof_key' <<< "${partkey_info}") echo "Sending transactions to staking contract ${contract_address} for owner ${account}" echo "This will update the staking contract with the new participation key info, allowing participation in the network" echo "You will be asked to enter your password multiple times to confirm the transactions." execute_interactive_docker_command "/node/bin/goal clerk send -a 1000 -f ${account} -t ${contract_address} --out=/tmp/payment.txn" execute_interactive_docker_command "/node/bin/goal app call --app-id ${contract_id} --from ${account} --app-arg 'b64:zSTeiA==' --app-arg 'b64:${voting_key}' --app-arg 'b64:${selection_key}' --app-arg 'int:${first_round}' --app-arg 'int:${last_round}' --app-arg 'int:${key_dilution}' --app-arg 'b64:${stateproof_key}' --out=/tmp/app_call.txn" execute_docker_command "cat /tmp/{payment,app_call}.txn > /tmp/combined.txn" execute_interactive_docker_command "/node/bin/goal clerk group -i /tmp/combined.txn -o /tmp/grouped.txn > /dev/null" execute_interactive_docker_command "/node/bin/goal clerk split -i /tmp/grouped.txn -o /tmp/split.txn > /dev/null" execute_interactive_docker_command "/node/bin/goal clerk sign -i /tmp/split-0.txn -o /tmp/signed-0.txn" execute_interactive_docker_command "/node/bin/goal clerk sign -i /tmp/split-1.txn -o /tmp/signed-1.txn" execute_docker_command "cat /tmp/signed-{0,1}.txn > /tmp/signed-combined.txn" execute_interactive_docker_command "/node/bin/goal clerk rawsend -f /tmp/signed-combined.txn" if [[ -n ${participation_key_id} ]]; then execute_interactive_docker_command "/node/bin/goal account deletepartkey --partkeyid ${participation_key_id}" fi else local existing_expiration_date existing_expiration_date=$(get_participation_expiration_eta "${part_vote_lst}" "${last_committed_block}") echo "Current staking participation key for owner ${account}, and" echo "contract address ${contract_address} is expected to expire at: ${existing_expiration_date}" echo "This is above the required threshold of 417,104 blocks / ~14 days." echo "No new staking participation key will be generated." echo "" fi done echo "" echo "${bold}To learn how to use your staking contract visit: https://staking.voi.network${normal}" else display_banner "No staking contract detected" echo "No staking contracts found for owner ${account}. To learn more visit: https://staking.voi.network" fi fi done } change_account_online_status() { local account account=$1 echo "Enter your password to join the network for account ${account}." execute_interactive_docker_command "/node/bin/goal account changeonlinestatus -a ${account}" } join_as_new_user() { new_user_setup=1 local account_addresses account_addresses=$(get_account_addresses) display_banner "Joining network" generate_participation_key # If there are multiple accounts, we don't want to go online automatically, unless previously done # as part of participation key generation. if [[ $(echo "$account_addresses" | wc -l) -gt 1 ]]; then display_banner "Welcome to Voi!" joined_network_instructions return fi busy_wait_until_balance_is_sufficient change_account_online_status "${account}" account_status=$(execute_docker_command "/node/bin/goal account dump -a ${account}" | jq -r .onl) check_staking_accounts ## This step is late in the process and does require a restart of the service to take effect. ## Container ID from verify_node_running will have to be re-fetched if any use of the node is to be done after this point. ## Intentionally not doing this here to avoid confusion. migrate_host_based_voi_setup if [[ ${account_status} -eq 1 ]]; then display_banner "Welcome to Voi! You are now online!" joined_network_instructions else display_banner "Your account ${account_addr} is currently offline." echo "There seems to be an issue with going online. Please seek assistance in the #node-runner channel on the Voi Network Discord." echo "Join us at: https://discord.com/invite/vnFbrJrHeW" abort "Exiting the program." fi echo "SAVE THIS INFORMATION SECURELY" echo "***********************************" echo "Your Voi address: ${account_addr}" echo "Enter password to get your Voi account recovery mnemonic. Store your mnemonic safely:" execute_interactive_docker_command "/node/bin/goal account export -a ${account_addr}" } add_docker_groups() { if [[ is_root -eq 0 ]]; then if [[ ! $(getent group docker) ]]; then execute_sudo "groupadd docker" fi execute_sudo "usermod -aG docker ${USER}" fi } get_telemetry_name() { if [[ ${VOINETWORK_PROFILE} != "participation" ]] ; then return fi if [[ -f "/var/lib/voi/algod/data/logging.config" ]]; then VOINETWORK_TELEMETRY_NAME=$(execute_sudo "cat /var/lib/voi/algod/data/logging.config" | jq -r '.Name') fi } update_profile_setting() { local setting_name="$1" local new_value="$2" local profile_file="${voi_home}/.profile" if [[ -f "$profile_file" ]]; then if grep -q "^export ${setting_name}=" "$profile_file"; then escaped_value=$(printf '%s\n' "$new_value" | sed 's/[\/&]/\\&/g') sed -i "s/^export ${setting_name}=.*/export ${setting_name}=\"${escaped_value}\"/" "$profile_file" else echo "export ${setting_name}=\"${new_value}\"" >> "$profile_file" fi else touch "$profile_file" echo "export ${setting_name}=\"${new_value}\"" >> "$profile_file" fi } clone_environment_settings_to_profile() { printenv | while IFS='=' read -r name value; do if [[ $name == VOINETWORK_* && $name != VOINETWORK_IMPORT_ACCOUNT ]]; then update_profile_setting "$name" "$value" fi done } set_relay_name() { echo "If you are operating a relay node you need to set a relay name in accordance with the naming convention." echo "The relay name should be in the format: ----" echo "" echo "Supported IATA airport codes can be found here: https://github.com/grafana/grafana/blob/main/public/gazetteer/airports.geojson" # shellcheck disable=SC2162 read -p "Relay name: " VOINETWORK_TELEMETRY_NAME while [[ ${VOINETWORK_TELEMETRY_NAME} == "" ]] do # shellcheck disable=SC2162 read -p "Please enter a relay name: " VOINETWORK_TELEMETRY_NAME done update_profile_setting "VOINETWORK_TELEMETRY_NAME" "${VOINETWORK_TELEMETRY_NAME}" } set_telemetry_name() { if [[ ${headless_install} -eq 1 ]]; then ## Allow headless install to skip telemetry name setup in case people bring their own wallets / use CI return fi if [[ ${VOINETWORK_PROFILE} == "relay" && -z ${VOINETWORK_TELEMETRY_NAME} ]]; then set_relay_name return elif [[ ${VOINETWORK_PROFILE} == "developer" || ${VOINETWORK_PROFILE} == "archiver" ]]; then return fi # detect_existing_host_based_setup # # if [[ ${migrate_host_based_setup} -eq 1 ]]; then # return # fi # display_banner "Telemetry" # if [[ -z ${VOINETWORK_TELEMETRY_NAME} && ! -f "/var/lib/voi/algod/data/logging.config" ]]; then # echo "Voi uses telemetry to make the network better and reward users with Voi if participating." # echo "" # echo "If you wish to opt-in to telemetry sharing, you can provide a telemetry name below." # echo "We'll add 'VOI:' at the start to show you're using this package." # echo "" # echo "To skip telemetry sharing, type 'opt-out' below." # echo "" # echo "Visit https://voinetwork.github.io/voi-swarm/getting-started/telemetry/ to learn how to set your own custom name." # echo "" # echo "Enter your telemetry name below to get started." # # shellcheck disable=SC2162 # read -p "Telemetry name: " VOINETWORK_TELEMETRY_NAME # if [[ ${VOINETWORK_TELEMETRY_NAME} == "opt-out" ]]; then # unset VOINETWORK_TELEMETRY_NAME # return # else # VOINETWORK_TELEMETRY_NAME="VOI:$VOINETWORK_TELEMETRY_NAME" # update_profile_setting "VOINETWORK_TELEMETRY_NAME" "${VOINETWORK_TELEMETRY_NAME}" # fi # elif [[ -n ${VOINETWORK_TELEMETRY_NAME} ]]; then # echo "Your telemetry name is already set to '${VOINETWORK_TELEMETRY_NAME}'. To change your telemetry settings, execute the command ${HOME}/voi/bin/set-telemetry-name" # else # echo "Telemetry is disabled. To enable telemetry, execute the command ${HOME}/voi/bin/set-telemetry-name" # fi } detect_existing_host_based_setup() { if [[ -f /var/lib/algorand/logging.config && ! -f /var/lib/voi/algod/data/logging.config ]]; then echo "An existing Voi or Algorand installation has been detected on your system." echo "We can migrate your existing telemetry configuration to Voi Swarm." echo "As part of this process, we will also stop and uninstall the existing service." echo "This is necessary to prevent conflicts and ensure that your node can join Voi Swarm as a healthy node." echo "" echo "Do you want to migrate your existing setup to Voi Swarm? (yes/no)" # shellcheck disable=SC2162 read -p "Migrate existing setup: " prompt while [[ ${prompt} != "yes" && ${prompt} != "no" ]] do # shellcheck disable=SC2162 read -p "Type either yes or no: " prompt done if [[ ${prompt} == "yes" ]]; then migrate_host_based_setup=1 fi fi } migrate_host_based_voi_setup() { if [[ ${migrate_host_based_setup} -eq 1 ]]; then display_banner "Migrating from host-based setup" VOINETWORK_TELEMETRY_NAME=$(execute_sudo "cat /var/lib/algorand/logging.config" | jq -r '.Name') bash -c "env VOINETWORK_TELEMETRY_NAME=\"${VOINETWORK_TELEMETRY_NAME}\" ${voi_home}/bin/migrate-from-host-setup" fi } check_minimum_requirements() { if [[ ${headless_install} -eq 1 || ${VOINETWORK_PROFILE} == "developer" || ${VOINETWORK_PROFILE} == "archiver" ]]; then ## Allow headless install to skip telemetry name setup in case people bring their own wallets / use CI return fi minimum_cpus=4 minimum_memory_bytes=6710886 minimum_memory_gigabytes_pretty=8 if [[ -n ${VOINETWORK_PROFILE} && ${VOINETWORK_PROFILE} == "relay" ]]; then minimum_cpus=8 minimum_memory_bytes=15938355 minimum_memory_gigabytes_pretty=16 fi echo "Checking system requirements.." echo "" num_cores=$(lscpu | grep "^CPU(s):" | awk '{print $2}') total_memory=$(grep MemTotal /proc/meminfo | awk '{print $2}') # Check if the number of cores is less than 4 and less (8 GB * 0.8) memory. Reported memory from # /proc/meminfo prints out accessible memory, not total memory. We use 80% of the total memory as an approximation, # intentionally going too low to allow variability from various cloud providers. if [[ ${num_cores} -lt ${minimum_cpus} || ${total_memory} -lt ${minimum_memory_bytes} ]]; then echo "*************************************************************************************" echo "* ${bold}WARNING: Your system does not meet the minimum requirements to run Voi Swarm effectively.${normal}" echo "*************************************************************************************" echo "*" echo "* Voi Swarm requires at least 4 CPU cores and 8 GB of memory to run effectively." echo "*" echo "* Your system has:" echo "* - CPU cores: ${bold}${num_cores}${normal} CPU cores. ${bold}${minimum_cpus}${normal} is required." echo "* - Memory: ${bold}$((total_memory / 1024 / 1024))${normal} GB of accessible memory. ${bold}${minimum_memory_gigabytes_pretty}${normal} GB is required." if [[ ${VOINETWORK_PROFILE} == "relay" ]]; then echo "*" echo "* You are running a relay node profile. Relay nodes require at least 8 CPU cores and 16 GB of memory." echo "*" echo "* Upgrade your system to meet the minimum requirements to run as a relay node." echo "*" abort "Exiting the program." else echo "*" echo "* You can still proceed, however, it may not be as beneficial to the network or to you," echo "* as your node won't be able to contribute or earn rewards effectively." echo "* You should ${bold}expect poor performance${normal}, and the community may ${bold}not be able to help${normal} you with issues." echo "* " echo "* If you are running this on a cloud provider, you should consider upgrading your instance to" echo "* meet the requirements." echo "* " echo "* Read more about other options for running a node on:" echo "* - https://voinetwork.github.io/voi-swarm/getting-started/introduction/" echo "*" fi echo "* Find other ways to contribute to the network by joining the Voi Network Discord:" echo "* - https://discord.com/invite/vnFbrJrHeW" echo "*" # shellcheck disable=SC2162 read -p "Type '${bold}acknowledged${normal}' when you're ready to continue: " prompt while [[ ${prompt} != "acknowledged" ]] do # shellcheck disable=SC2162 read -p "Type 'acknowledged' to continue: " prompt done else echo "Your system meets the minimum requirements to run Voi Swarm effectively." echo "" fi } get_existing_network() { profile_path="${voi_home}/.profile" if [[ -f "${profile_path}" ]]; then voinetwork_network=$(grep -E '^export VOINETWORK_NETWORK=' "${profile_path}" | cut -d'=' -f2 | tr -d '"') if [[ -n "${voinetwork_network}" ]]; then echo "$voinetwork_network" return fi fi echo "" } set_profile() { if [[ -f "${voi_home}/.profile" ]]; then source "${voi_home}/.profile" fi if [[ -z ${VOINETWORK_PROFILE} ]]; then # If VOINETWORK_PROFILE is not set, use default "participation" to profile VOINETWORK_PROFILE="participation" echo "export VOINETWORK_PROFILE=${VOINETWORK_PROFILE}" >> "${voi_home}/.profile" fi if [[ -z ${VOINETWORK_NETWORK} ]]; then # If VOINETWORK_NETWORK is not set, use default "mainnet" to profile VOINETWORK_NETWORK="mainnet" echo "export VOINETWORK_NETWORK=${VOINETWORK_NETWORK}" >> "${voi_home}/.profile" fi display_banner "Setting up Voi Swarm using profile: ${VOINETWORK_PROFILE}, and network: ${VOINETWORK_NETWORK}" } get_tarball() { local branch local filename="voi-swarm.tar.gz" if [[ -n ${VOINETWORK_BRANCH} ]]; then branch=${VOINETWORK_BRANCH} else branch="main" fi curl -sSL https://api.github.com/repos/VoiNetwork/voi-swarm/tarball/"${branch}" --output "${voi_home}/${filename}" tar -xzf "${voi_home}/${filename}" -C "${voi_home}" --strip-components=1 rm "${voi_home}/${filename}" } preserve_autoupdate() { if [[ ${VOINETWORK_PROFILE} == "relay" ]]; then docker_filename="${voi_home}/docker/relay.yml" elif [[ ${VOINETWORK_PROFILE} == "developer" ]]; then docker_filename="${voi_home}/docker/developer.yml" elif [[ ${VOINETWORK_PROFILE} == "archiver" ]]; then docker_filename="${voi_home}/docker/archiver.yml" else docker_filename="${voi_home}/docker/compose.yml" fi autoupdate_state=$(awk -F'=' '/swarm.cronjob.enable=/ {print $2}' "${docker_filename}") if [[ ${autoupdate_state} == "false" ]]; then sed -i -E "s/(swarm.cronjob.enable=).*/\1false" "${docker_filename}" fi } add_update_jitter() { case ${VOINETWORK_PROFILE} in "relay") schedule_filename="${voi_home}/docker/relay.yml" ;; "developer") schedule_filename="${voi_home}/docker/developer.yml" ;; "archiver") schedule_filename="${voi_home}/docker/archiver.yml" ;; "participation") schedule_filename="${voi_home}/docker/compose.yml" ;; *) schedule_filename="${voi_home}/docker/compose.yml" ;; esac random_minute=$(( RANDOM % 60 )) # Generate a random number between 0 and 2, and add +1 to shift the range to 1-3 random_hour=$(( RANDOM % 3 + 1 )) new_cron_schedule="${random_minute} */${random_hour} * * *" sed -i -E "s|(swarm.cronjob.schedule=).*|\1${new_cron_schedule}|" "${schedule_filename}" } if [ -z "${BASH_VERSION:-}" ]; then abort "Bash is required to interpret this script." fi if [[ $(id -u) -eq 0 ]]; then is_root=1 else echo "Checking for sudo access, you may be prompted for your password." if ! sudo -v &> /dev/null; then abort "User does not have sudo access. Please run this script as a user with sudo access." fi fi # Get Linux OS distribution if [[ -f /etc/os-release ]]; then . /etc/os-release operating_system_distribution="${ID}" else abort "This script is only meant to be run on Debian or Ubuntu." fi # Detect if running in a container; this method is not foolproof # shellcheck disable=SC2143 if [[ -f /.dockerenv ]]; then abort "This script is not meant to be run in a container." fi # shellcheck disable=SC2143 if [[ $(uname -r | grep -i "microsoft") ]]; then abort "Windows Subsystem for Linux is not supported. Please run this script on a native Linux installation." fi if [[ ! (${operating_system_distribution} == "ubuntu" || ${operating_system_distribution} == "debian") ]]; then echo "Detected operating system: ${operating_system_distribution}" abort "This script is only meant to be run on Debian or Ubuntu." fi if [[ -n ${VOINETWORK_SKIP_WALLET_SETUP} && -n ${VOINETWORK_IMPORT_ACCOUNT} ]]; then echo "VOINETWORK_IMPORT_ACCOUNT and VOINETWORK_SKIP_WALLET_SETUP are both set. This is not supported at the same time." echo "" echo "To import an existing account, set VOINETWORK_IMPORT_ACCOUNT=1 and unset VOINETWORK_SKIP_WALLET_SETUP." echo "" echo "Your Voi are linked to your account. Wallets are created automatically if needed." echo "Voi Swarm doesn't support wallet import." abort "Exiting the program." fi if [[ -n ${VOINETWORK_SKIP_WALLET_SETUP} && ${VOINETWORK_SKIP_WALLET_SETUP} -eq 1 ]] || [[ -n $VOINETWORK_HEADLESS_INSTALL ]]; then headless_install=1 fi display_banner "${bold}Welcome to Voi Swarm${normal}. Let's get started!" mkdir -p "${voi_home}" set_network_identifier existing_network=$(get_existing_network) clone_environment_settings_to_profile set_profile set_docker_image if [[ ${VOINETWORK_NETWORK} != "${existing_network}" && -n ${existing_network} ]]; then new_network=1 elif [[ ${VOINETWORK_NETWORK} == "mainnet" && -z ${existing_network} ]]; then ## This is temporary to allow for people that have not run the installer since ## mid-August 2024, as there prior to this was no network profile. if [[ -e /var/lib/voi/algod/data/voitest-v1 ]]; then existing_network="testnet" new_network=1 fi fi check_minimum_requirements if [[ ${new_network} -eq 1 ]]; then get_container_id ensure_accounts_are_offline "${existing_network}" fi #get_telemetry_name set_telemetry_name display_banner "Installing Docker" execute_sudo "apt-get update" execute_sudo "apt-get install -y ca-certificates curl gnupg" execute_sudo "install -m 0755 -d /etc/apt/keyrings" case ${operating_system_distribution} in "ubuntu") execute_sudo "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --yes --dearmor -o /etc/apt/keyrings/docker.gpg" execute_sudo "chmod a+r /etc/apt/keyrings/docker.gpg" execute_sudo "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo \""$VERSION_CODENAME\"") stable\" > /etc/apt/sources.list.d/docker.list" ;; "debian") execute_sudo "curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --yes --dearmor -o /etc/apt/keyrings/docker.gpg" execute_sudo "chmod a+r /etc/apt/keyrings/docker.gpg" execute_sudo "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo \""$VERSION_CODENAME\"") stable\" > /etc/apt/sources.list.d/docker.list" ;; esac execute_sudo "apt-get update" execute_sudo "apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin" add_docker_groups if ! docker --version | grep -q 'Docker version'; then echo "Docker installation failed." abort "Exiting the program." fi ## Install script dependencies execute_sudo "apt-get install -y jq bc pv" display_banner "Starting stack" start_docker_swarm if [[ ! -e /var/lib/voi/algod/data ]]; then execute_sudo "mkdir -p /var/lib/voi/algod/data" fi if [[ ! -e /var/lib/voi/algod/metrics ]]; then execute_sudo "mkdir -p /var/lib/voi/algod/metrics" fi if [[ ! -e /var/lib/voi/algod/data/cold-storage && ${VOINETWORK_PROFILE} == "archiver" ]]; then execute_sudo "mkdir -p /var/lib/voi/algod/data/cold-storage" fi display_banner "Fetching the latest Voi Network updates and scripts." get_tarball cleanup_deprecated_files_and_folders add_update_jitter start_stack wait_for_stack_to_be_ready if [[ ${VOINETWORK_PROFILE} == "participation" ]]; then get_container_id verify_node_is_running fi if [[ ${VOINETWORK_PROFILE} != "participation" || ( -n ${VOINETWORK_SKIP_WALLET_SETUP} && ${VOINETWORK_SKIP_WALLET_SETUP} -eq 1 ) ]]; then display_banner "Wallet setup will be skipped." joined_network_instructions true exit 0 fi start_kmd display_banner "Initiating setup for Voi wallets and accounts." create_wallet if [[ -n ${VOINETWORK_IMPORT_ACCOUNT} && ${VOINETWORK_IMPORT_ACCOUNT} -eq 1 ]]; then if ! (check_if_account_exists true); then echo "An account already exists. No need to import, skipping this step." else echo "" echo "Let's proceed to import an account using your existing account mnemonic." execute_interactive_docker_command "/node/bin/goal account import" get_account_address fi else if [[ ${new_network} -eq 1 && -n ${previous_network_addresses} ]]; then echo "" for account in ${previous_network_addresses}; do echo "Let's proceed to import ${account} into ${VOINETWORK_NETWORK} using your existing account mnemonic." echo " This is a two-step process, first we will delete the account from the previous network, followed" echo " by importing the account into the new network." echo "" execute_interactive_docker_command "/node/bin/goal account delete -a ${account}" execute_interactive_docker_command "/node/bin/goal account import" done get_account_address elif check_if_account_exists; then execute_interactive_docker_command "/node/bin/goal account new" get_account_address if [[ ${VOINETWORK_NETWORK} == "mainnet" ]]; then echo "****************************************************************************************************************" echo "* To join the Voi network, do one of these:" echo "*" echo "* a) Send at least 1 Voi to your account ${account_addr} from another account" echo "*" echo "* OR" echo "*" echo "* b) Acquire Voi and send it to your account" echo "* - Go to https://voinetwork.github.io/getting-started/setup-account/#adding-voi-to-your-account" echo "* - Follow the guidance and options available to acquire Voi" echo "*" echo "* After you've done this, type 'completed' to go on" echo "****************************************************************************************************************" elif [[ ${VOINETWORK_NETWORK} == "testnet-v1.1" ]]; then # Get Voi from faucet echo "****************************************************************************************************************" echo "* To join the Voi network, do one of these:" echo "*" echo "* a) Send at least 1 testnet Voi to your account ${account_addr} from another account" echo "*" echo "* OR" echo "*" echo "* b) Get 10 testnet Voi for free:" echo "* - Go to https://faucet.nautilus.sh/" echo "* - Enter your address: ${account_addr}" echo "*" echo "* After you've done this, type 'completed' to go on" echo "****************************************************************************************************************" fi # shellcheck disable=SC2162 read -p "Type 'completed' when you're ready to continue: " prompt while [[ ${prompt} != "completed" ]] do # shellcheck disable=SC2162 read -p "Type 'completed' to continue: " prompt done fi fi # Catchup node before creating participation key and going online catchup_node if [[ ${skip_account_setup} -eq 0 || ${new_network} -eq 1 ]]; then join_as_new_user else generate_participation_key participation_key_generation_status=$? ## Catch cases where an install was aborted / user didn't succeed in going online ## This can happen where there are no part keys present on the machine, or where there's multiple part keys but ## no key is active. if [[ ${participation_key_generation_status} -eq 1 ]]; then display_banner "Restarting participation key generation" echo "Wallet and account detected, but no active participation keys found." echo "This could be due to a failed setup or a previous setup that was aborted." join_as_new_user fi check_staking_accounts migrate_host_based_voi_setup display_banner "Welcome to Voi!" joined_network_instructions fi