#!/usr/bin/env bash # Install the Zecnero Testnet miner on Linux or Windows (WSL): # # curl -fsSL https://downloads.zecnero.org/install-miner.sh | bash # # Where downloads.zecnero.org is slow or blocked, the same script and files come # from the mirror on seed1, which is not behind Cloudflare: # # curl -fsSL https://mirror.zecnero.org/install-miner.sh | bash # # Either way, the script tries downloads.zecnero.org first and, if that does # not answer within about 10 seconds (or the transfer stalls), takes every file # from mirror.zecnero.org instead. The signature and checksum checks are the # same for both. # # It checks this computer, downloads the newest release named by # https://downloads.zecnero.org/LATEST, checks its signature and checksums # and that its signed SHA256SUMS names that same release (file RELEASE), # installs everything into ~/zecnero, makes a mining key, writes the settings, # adds the zecnero-miner command, asks whether to mine on the pool (the # default: no node to sync, mining starts in about a minute) or solo with # your own node, and starts mining in the background. # # Safe to run again: it upgrades the programs in place, keeps the mode you # mine in unless you choose the other one, and never replaces your mining key # or settings without asking (the default answer is No). # # Settings for automated runs (all optional): # ZECNERO_MODE=pool|solo mine on the pool, or solo with your own node # (default: pool, or on a re-run the mode you use) # ZECNERO_WORKER=NAME the worker name on the pool (default: this computer's name) # ZECNERO_POOL_LOGIN=ADDR the Sapling address the pool pays (ntestsapling1...), or # new: make a wallet with zecnero-wallet for it. From the # shielded-only upgrade (block 16529) the pool # pays shielded addresses only, so pool mode needs one; # without it the installer asks (and, with no terminal, # mines solo). Your nm... mining key is for solo mining. # ZECNERO_WALLET_PASSPHRASE the new wallet's passphrase, for ZECNERO_POOL_LOGIN=new # with no terminal (with ZECNERO_ASSUME_SAVED=1) # ZECNERO_ASSUME_SAVED=1 do not show the new key (or wallet seed) or wait for SAVED # ZECNERO_MINER_THREADS=N mine with N threads (default: XMRig decides) # ZECNERO_HUGEPAGES=yes|no answer the huge pages question (native Linux) # ZECNERO_AUTOSTART=yes|no answer the automatic start question (systemd) # ZECNERO_NO_START=1 install and set up, but do not start mining # ZECNERO_MIRROR_FIRST=1 try mirror.zecnero.org first, then downloads.zecnero.org # ZECNERO_RELEASE_BASE=URL download from this address only, with no fallback # (https only, unless in test mode) # ZECNERO_MIRROR_BASE=URL the address to fall back to (default: mirror.zecnero.org) # https_proxy, HTTPS_PROXY, all_proxy, ALL_PROXY # a proxy for the downloads, which curl reads by itself; # for https, https_proxy is used before ALL_PROXY, and # socks5h://HOST:PORT in ALL_PROXY also resolves names # through the proxy # ZECNERO_HOME=DIR install somewhere other than ~/zecnero # ZECNERO_EXPECT_RELEASE=NAME install only when LATEST names this release (install.ps1 # sets it to the LATEST it checked against the signed # INSTALLERS.SHA256SUMS) # Test mode (never for real use; it prints a warning box): # ZECNERO_TEST_MODE=1 with ZECNERO_TEST_KEY_FILE and ZECNERO_TEST_FINGERPRINT # trusts a test signing key; ZECNERO_PLATFORM=linux|wsl overrides detection. # # The whole script is inside main(), so a download cut short runs nothing. set -u set -o pipefail umask 022 ZHOME="${ZECNERO_HOME:-$HOME/zecnero}" # A synced Testnet node needs a few MB of chain state today (it grows slowly), # the programs about 100 MB, the download about 70 MB and the logs at most # 80 MB. 1 GB leaves room for months of chain growth. MIN_FREE_MB=1024 MIN_GLIBC="2.34" INSTALL_URL="https://downloads.zecnero.org/install-miner.sh" # >>> shared: release download and verification >>> # This block is identical in install-miner.sh and zecnero-miner. # tests/installer/check-shared.sh fails if the two copies differ. ZV_FINGERPRINT="FFFC8AC02BF7B4B450B43B1F653209A344E75767" ZV_BASE_DEFAULT="https://downloads.zecnero.org" # The same signed files from seed1, outside Cloudflare, for where downloads.zecnero.org # is slow or blocked. What comes from it is accepted only with a good signature from # the release key: without gpg, a run that would download from it stops (see # zv_mirror_needs_gpg). downloads.zecnero.org alone still works without gpg, on # checksums, with a warning. ZV_MIRROR_DEFAULT="https://mirror.zecnero.org" ZV_ARCHIVES="zecnerod-linux-x86_64.tar.xz zecnero-bridge-linux-x86_64.tar.xz xmrig-zecnero-linux-x86_64.tar.xz zecnero-keygen-linux-x86_64.tar.xz zecnero-wallet-linux-x86_64.tar.xz" ZV_SCRIPTS="zecnero-miner" zv_say() { printf '%s\n' "$*"; } zv_warn() { printf 'Warning: %s\n' "$*" >&2; } zv_die() { printf '\n' >&2 zv_box "STOPPED: $1" "${@:2}" >&2 exit 1 } # Prints its arguments as lines inside a box of #. zv_box() { local line width=0 for line in "$@"; do [ "${#line}" -gt "$width" ] && width=${#line} done local bar bar="$(printf '%*s' $((width + 6)) '' | tr ' ' '#')" printf '%s\n' "$bar" for line in "$@"; do printf '# %-*s #\n' "$width" "$line" done printf '%s\n' "$bar" } # The Zecnero release signing key, FFFC 8AC0 2BF7 B4B4 50B4 3B1F 6532 09A3 44E7 5767. # Carried here so that a changed key on the website cannot change what this # script trusts. Its fingerprint is checked against ZV_FINGERPRINT before use. zv_release_key() { cat <<'KEY' -----BEGIN PGP PUBLIC KEY BLOCK----- mDMEaq67ARYJKwYBBAHaRw8BAQdAPPAYHc+Eep6d+p1zS3LypG+BWxgBQGMRkea4 DpwzPj+0G3plY25lcm8gPHplY25lcm9AcHJvdG9uLm1lPoivBBMWCgBXFiEE//yK wCv3tLRQtDsfZTIJo0TnV2cFAmquuwEbFIAAAAAABAAObWFudTIsMi41KzEuMTIs MiwxAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEGUyCaNE51dnL0sA /1As3HtllJCcjejp2xd+9DuWLLx9hTSnUSw6ELrycA8KAQDdXzP/+yAxuuWh6QD2 dCa32OkpcDUn6EYPBEbM4MayB7g4BGquuwESCisGAQQBl1UBBQEBB0DEooQVfkdD TibibpLEln7uHuERwthV3xxB/D/39GObTgMBCAeIlAQYFgoAPBYhBP/8isAr97S0 ULQ7H2UyCaNE51dnBQJqrrsBGxSAAAAAAAQADm1hbnUyLDIuNSsxLjEyLDIsMQIb DAAKCRBlMgmjROdXZyOrAPoCI//0w/Ajtqlgy+CvihVwsytgAo0DCi1ySe8nSUGE EgEAzxgHzl7mXOfjp6DeSW7PmjDwayXrpkiGt9aukAnaLg4= =8MCI -----END PGP PUBLIC KEY BLOCK----- KEY } # Decides what to trust and where to download from. Test overrides only take # effect together with ZECNERO_TEST_MODE=1, and then loudly. # # Downloads come from ZV_BASE. When it cannot be reached (no connection, a TLS # failure, a timeout or a stalled transfer), the whole run switches to # ZV_FALLBACK once and takes every file from there, so that SHA256SUMS and the # files always come from the same host. A failed signature or checksum never # switches: it stops the run, wherever the files came from. # default downloads.zecnero.org, then mirror.zecnero.org # ZECNERO_MIRROR_FIRST=1 mirror.zecnero.org, then downloads.zecnero.org # ZECNERO_RELEASE_BASE=URL that address alone (no fallback) # ZECNERO_MIRROR_BASE=URL the address to fall back to (or to try first) # ZV_ON_MIRROR is 1 while the files come from the mirror (the fallback, the # first host with ZECNERO_MIRROR_FIRST=1, or mirror.zecnero.org named # directly); files from it need gpg (zv_mirror_needs_gpg). zv_setup_trust() { ZV_FPR="$ZV_FINGERPRINT" ZV_KEYFILE="" ZV_TEST=0 ZV_SWITCHED=0 ZV_BASE="${ZECNERO_RELEASE_BASE:-$ZV_BASE_DEFAULT}" ZV_BASE="${ZV_BASE%/}" if [ -n "${ZECNERO_MIRROR_BASE:-}" ]; then ZV_FALLBACK="${ZECNERO_MIRROR_BASE%/}" elif [ -z "${ZECNERO_RELEASE_BASE:-}" ]; then ZV_FALLBACK="$ZV_MIRROR_DEFAULT" else ZV_FALLBACK="" fi ZV_ON_MIRROR=0 if [ "${ZECNERO_MIRROR_FIRST:-}" = 1 ] && [ -n "$ZV_FALLBACK" ]; then set -- "$ZV_BASE" ZV_BASE="$ZV_FALLBACK" ZV_FALLBACK="$1" ZV_ON_MIRROR=1 fi [ "$ZV_BASE" = "$ZV_MIRROR_DEFAULT" ] && ZV_ON_MIRROR=1 if [ "${ZECNERO_TEST_MODE:-}" = 1 ]; then ZV_TEST=1 [ -n "${ZECNERO_TEST_FINGERPRINT:-}" ] && ZV_FPR="$ZECNERO_TEST_FINGERPRINT" [ -n "${ZECNERO_TEST_KEY_FILE:-}" ] && ZV_KEYFILE="$ZECNERO_TEST_KEY_FILE" zv_box "TEST MODE: NOT FOR REAL USE" \ "ZECNERO_TEST_MODE=1 is set. This run trusts the signing key" \ "fingerprint $ZV_FPR" \ "and downloads from $ZV_BASE ${ZV_FALLBACK:+(or $ZV_FALLBACK)}" \ "If you did not set this on purpose, stop now (Ctrl+C) and run:" \ "unset ZECNERO_TEST_MODE" >&2 else if [ -n "${ZECNERO_TEST_FINGERPRINT:-}${ZECNERO_TEST_KEY_FILE:-}" ]; then zv_warn "ignoring ZECNERO_TEST_FINGERPRINT and ZECNERO_TEST_KEY_FILE, which only work with ZECNERO_TEST_MODE=1." fi case "$ZV_BASE" in https://*) ;; *) zv_die "the download address is not https" "ZECNERO_RELEASE_BASE is $ZV_BASE" "Unset it to use $ZV_BASE_DEFAULT" ;; esac case "$ZV_FALLBACK" in "" | https://*) ;; *) zv_die "the download address is not https" "ZECNERO_MIRROR_BASE is $ZV_FALLBACK" "Unset it to use $ZV_MIRROR_DEFAULT" ;; esac fi if ! zv_on_mirror; then : elif [ "${ZECNERO_MIRROR_FIRST:-}" = 1 ] && [ -n "$ZV_FALLBACK" ]; then zv_mirror_needs_gpg "ZECNERO_MIRROR_FIRST=1 is set, so the files would come from it." \ "(Without gpg, remove ZECNERO_MIRROR_FIRST to use $(zv_host "$ZV_FALLBACK").)" else zv_mirror_needs_gpg "ZECNERO_RELEASE_BASE is $ZV_BASE." fi } # True while the files of this run come from the mirror. zv_on_mirror() { [ "${ZV_ON_MIRROR:-0}" = 1 ]; } # Files from the mirror are accepted only with a good signature, never on # checksums alone. Without gpg this stops the run before anything comes from # the mirror (and, as a last line, before a mirror download is checked). zv_mirror_needs_gpg() { command -v gpg >/dev/null 2>&1 && return 0 local host host="$(zv_host "$ZV_BASE")" set -- "$@" \ "Files from $host are accepted only with a good signature from" \ "the Zecnero release key, never on checksums alone, and checking" \ "it needs gpg, which is not installed. Nothing was changed." \ "Install gpg with this line, then run the same command again:" \ "sudo apt update && sudo apt install -y gnupg" if grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null; then set -- "$@" "On Windows, run that line in Ubuntu, or this one in PowerShell:" \ "wsl -u root -- sh -c 'apt-get update && apt-get install -y gnupg'" fi zv_die "gpg is needed to download from $host" "$@" } # zv_fetch URL FILE [small]. While there is another host to turn to, a host # gets little time: 10 s to connect (TLS included), one retry, and a transfer # slower than 1 KB/s for 20 s counts as stalled. The last host gets 3 retries # and 20 s to connect, and only a transfer stalled for 2 minutes is given up. # curl takes a proxy from the environment by itself (see zv_proxy_note). # (ZECNERO_TEST_TIMEOUT shortens the 10 s, in test mode only.) zv_fetch() { local url="$1" out="$2" size="${3:-}" quick=10 set -- -fsSL -o "$out" if [ "$ZV_TEST" = 1 ]; then quick="${ZECNERO_TEST_TIMEOUT:-10}" else set -- "$@" --proto '=https' --tlsv1.2 fi if [ -n "$ZV_FALLBACK" ]; then set -- "$@" --retry 1 --connect-timeout "$quick" --speed-limit 1024 --speed-time $((quick * 2)) [ "$size" = small ] && set -- "$@" --max-time $((quick * 3)) else set -- "$@" --retry 3 --connect-timeout 20 --speed-limit 1 --speed-time 120 fi curl "$@" "$url" } zv_host() { local h="${1#*://}" printf '%s' "${h%%/*}" } # Why curl failed, in words, from its exit code. zv_why() { case "$1" in 5) printf 'the proxy name could not be resolved' ;; 6) printf 'the name could not be resolved' ;; 7) printf 'no connection' ;; 22) printf 'an HTTP error, such as file not found' ;; 28) printf 'timed out' ;; 35) printf 'the TLS handshake failed' ;; 60) printf 'its TLS certificate was refused' ;; 16 | 18 | 52 | 55 | 56 | 92) printf 'the connection broke off' ;; 97) printf 'the proxy refused the connection' ;; *) printf 'curl error %s' "$1" ;; esac } # Says which proxy curl will use, as curl picks it: for https, https_proxy, # then HTTPS_PROXY, then all_proxy, then ALL_PROXY (socks5h:// included, which # also leaves the name lookup to the proxy). A password in it is not shown. zv_proxy_note() { local name value names="https_proxy HTTPS_PROXY all_proxy ALL_PROXY" case "$ZV_BASE" in http://*) names="http_proxy all_proxy ALL_PROXY" ;; esac for name in $names; do value="${!name:-}" [ -n "$value" ] || continue zv_say "Downloading through the proxy in $name: $(printf '%s' "$value" | sed -E 's#^([a-z0-9]+://)?[^/@]*@#\1***@#')" return 0 done } # Switches the rest of the run to the fallback host, once. # Stops instead when that is the mirror and gpg is missing. zv_switch() { zv_say "$(zv_host "$ZV_BASE") did not work from here ($1)." ZV_BASE="$ZV_FALLBACK" ZV_FALLBACK="" ZV_SWITCHED=1 if [ "$ZV_ON_MIRROR" = 1 ]; then ZV_ON_MIRROR=0; else ZV_ON_MIRROR=1; fi [ "$ZV_BASE" = "$ZV_MIRROR_DEFAULT" ] && ZV_ON_MIRROR=1 zv_on_mirror && zv_mirror_needs_gpg zv_say "Downloading everything from $(zv_host "$ZV_BASE") instead; it is checked the same way." } # Sets ZV_VERSION to the current release folder name, as named by LATEST. zv_latest() { local tmp version rc=0 tmp="$(mktemp)" || zv_die "could not make a temporary file" zv_proxy_note zv_fetch "$ZV_BASE/LATEST" "$tmp" small || rc=$? if [ "$rc" != 0 ] && [ -n "$ZV_FALLBACK" ]; then zv_switch "$(zv_why "$rc")" rc=0 zv_fetch "$ZV_BASE/LATEST" "$tmp" small || rc=$? fi if [ "$rc" != 0 ]; then rm -f "$tmp" zv_die "could not download $ZV_BASE/LATEST ($(zv_why "$rc"))" \ "Check your internet connection and try again. Behind a proxy, set" \ "https_proxy (or ALL_PROXY, socks5h:// works) before the command." fi version="$(head -c 64 "$tmp" | tr -d ' \r\n\t')" rm -f "$tmp" case "$version" in testnet-v[0-9]*.[0-9]*.[0-9]*) ;; *) zv_die "the release pointer is not a version name" "$ZV_BASE/LATEST says: $version" ;; esac printf '%s\n' "$version" | grep -Eq '^testnet-v[0-9]+\.[0-9]+\.[0-9]+$' || zv_die "the release pointer is not a version name" "$ZV_BASE/LATEST says: $version" ZV_VERSION="$version" } # Downloads the release folder VERSION: SHA256SUMS, its signature, RELEASE, then the # archives and scripts. A folder whose RELEASE is missing or names another release is # refused as soon as RELEASE is in (see zv_check_release), before the archives. zv_download() { local version="$1" dir="$2" f rc while :; do rc=0 for f in SHA256SUMS SHA256SUMS.asc RELEASE $ZV_ARCHIVES $ZV_SCRIPTS; do [ "$f" = RELEASE ] && zv_release_listed "$dir" "$version" zv_say " downloading $f" zv_fetch "$ZV_BASE/$version/$f" "$dir/$f" || { rc=$? break } [ "$f" = RELEASE ] && zv_check_release "$dir" "$version" done [ "$rc" = 0 ] && return 0 [ -n "$ZV_FALLBACK" ] || break zv_switch "$(zv_why "$rc")" # Start again from SHA256SUMS: all files from one host. for f in SHA256SUMS SHA256SUMS.asc RELEASE $ZV_ARCHIVES $ZV_SCRIPTS; do rm -f "$dir/$f"; done done if [ "$ZV_SWITCHED" = 1 ]; then zv_die "could not download $f ($(zv_why "$rc"))" "from $ZV_BASE/$version/$f" \ "A release published in the last few minutes may not be on" \ "$(zv_host "$ZV_BASE") yet: the mirror copies it within about 10" \ "minutes. Try again later." fi zv_die "could not download $f ($(zv_why "$rc"))" "from $ZV_BASE/$version/$f" "Check your internet connection and try again." } # Returns 0 for a good signature by the pinned key, 2 when gpg is missing # (downloads.zecnero.org only: files from the mirror need gpg). Anything else # stops the script. zv_check_signature() { local dir="$1" gh status primary if ! command -v gpg >/dev/null 2>&1; then zv_on_mirror && zv_mirror_needs_gpg return 2 fi gh="$(mktemp -d)" || zv_die "could not make a temporary folder" chmod 700 "$gh" if [ -n "$ZV_KEYFILE" ]; then GNUPGHOME="$gh" gpg --batch --quiet --no-tty --import "$ZV_KEYFILE" >/dev/null 2>&1 else zv_release_key | GNUPGHOME="$gh" gpg --batch --quiet --no-tty --import >/dev/null 2>&1 fi primary="$(GNUPGHOME="$gh" gpg --batch --no-tty --with-colons --fingerprint 2>/dev/null | awk -F: '$1 == "pub" { want = 1; next } want && $1 == "fpr" { print $10; exit }')" if [ "$primary" != "$ZV_FPR" ]; then GNUPGHOME="$gh" gpgconf --kill all >/dev/null 2>&1 rm -rf "$gh" zv_die "the signing key is not the Zecnero release key" \ "Expected fingerprint $ZV_FPR" "Got ${primary:-nothing}" fi status="$(GNUPGHOME="$gh" gpg --batch --no-tty --status-fd 1 \ --verify "$dir/SHA256SUMS.asc" "$dir/SHA256SUMS" 2>/dev/null)" GNUPGHOME="$gh" gpgconf --kill all >/dev/null 2>&1 rm -rf "$gh" if printf '%s\n' "$status" | grep -Eq '^\[GNUPG:\] (BADSIG|ERRSIG|EXPSIG|EXPKEYSIG|REVKEYSIG)'; then zv_die "the release signature is NOT valid" \ "SHA256SUMS was not signed by the Zecnero release key." \ "Nothing was installed. Do not run these files." \ "Please report this at https://gitlab.com/zecnero or on Discord." fi if ! printf '%s\n' "$status" | awk -v fpr="$ZV_FPR" \ '$1 == "[GNUPG:]" && $2 == "VALIDSIG" && $NF == fpr { ok = 1 } END { exit !ok }'; then zv_die "the release signature could not be checked" \ "gpg did not report a good signature from $ZV_FPR." \ "Nothing was installed." fi return 0 } # LATEST is not signed, and SHA256SUMS lists bare file names, so the signature alone # does not say which release a folder holds: a download server could serve an older # release's signed files under a newer folder name (and LATEST), and they would pass. # So every release from testnet-v0.2.0 on lists a file RELEASE in its SHA256SUMS that # holds exactly its name ("testnet-v0.2.0" and a newline), and a folder is accepted only # when its RELEASE is listed, matches its checksum and names that folder, which is the # one LATEST named. A release without RELEASE (testnet-v0.1.x) is refused. zv_release_listed() { grep -Eq '^[0-9a-f]{64} [ *]RELEASE$' "$1/SHA256SUMS" 2>/dev/null && return 0 zv_die "this release does not say which version it is" \ "$ZV_BASE/$2/SHA256SUMS lists no RELEASE file." \ "Every release from testnet-v0.2.0 on names its version in its signed" \ "SHA256SUMS, so that an older release cannot be passed off as a newer" \ "one. A release without it (testnet-v0.1.x) is never installed. Nothing" \ "was installed. If the download server names such a release as the" \ "latest, please report it at https://gitlab.com/zecnero or on Discord." } # zv_check_release DIR VERSION: sets ZV_RELEASE to VERSION when DIR's RELEASE names it # (see above); stops otherwise. Its checksum counts only with a good signature. zv_check_release() { local dir="$1" version="$2" lines got named zv_release_listed "$dir" "$version" lines="$(grep -E '^[0-9a-f]{64} [ *]RELEASE$' "$dir/SHA256SUMS")" [ "$(printf '%s\n' "$lines" | grep -c .)" = 1 ] || zv_die "SHA256SUMS has no single entry for RELEASE" "Nothing was installed." got="$(sha256sum "$dir/RELEASE" | cut -d' ' -f1)" [ "${lines%% *}" = "$got" ] || zv_die "RELEASE is damaged or was changed (checksum mismatch)" \ "expected ${lines%% *}" "got $got" "Nothing was installed. Try again later." if ! printf '%s\n' "$version" | cmp -s - "$dir/RELEASE"; then named="$(head -c 64 "$dir/RELEASE" | tr -cd 'A-Za-z0-9.-')" zv_die "the release in $version/ is ${named:-not named}, not $version" \ "Its signed SHA256SUMS names the release it belongs to, and it is not" \ "the one the folder and LATEST name: an older release served under a" \ "newer name. Nothing was installed. Do not run these files." \ "Please report this at https://gitlab.com/zecnero or on Discord." fi ZV_RELEASE="$version" } # True when release name $1 is older than $2 (testnet-vX.Y.Z, compared by number). zv_older() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "${1#testnet-v}" "${2#testnet-v}" | sort -V | head -1)" = "${1#testnet-v}" ] } # zv_check_sums DIR VERSION: the release is VERSION (zv_check_release), and every # archive and script matches SHA256SUMS. Run after zv_check_signature. zv_check_sums() { local dir="$1" f want got lines [ -n "${2:-}" ] || zv_die "zv_check_sums needs the release name" zv_check_release "$dir" "$2" for f in $ZV_ARCHIVES $ZV_SCRIPTS; do lines="$(grep -E "^[0-9a-f]{64} [ *]$f\$" "$dir/SHA256SUMS" || true)" [ "$(printf '%s' "$lines" | grep -c .)" = 1 ] || zv_die "SHA256SUMS has no single entry for $f" "Nothing was installed." want="${lines%% *}" got="$(sha256sum "$dir/$f" | cut -d' ' -f1)" [ "$want" = "$got" ] || zv_die "$f is damaged or was changed (checksum mismatch)" \ "expected $want" "got $got" "Nothing was installed. Try again later." done } # Unpacks the verified download into a new release folder and checks that # every program starts on this system. zv_unpack() { local dir="$1" dest="$2" a name out mkdir -p "$dest" || zv_die "could not create $dest" for a in $ZV_ARCHIVES; do name="${a%-linux-x86_64.tar.xz}" mkdir -p "$dir/x-$name" tar --no-same-owner -m -xJf "$dir/$a" -C "$dir/x-$name" || zv_die "could not unpack $a" [ -f "$dir/x-$name/$name" ] || zv_die "$a does not contain $name" install -m 0755 "$dir/x-$name/$name" "$dest/$name" || zv_die "could not install $name" if ! out="$("$dest/$name" --version 2>&1)"; then zv_die "$name does not run on this system" "$(printf '%s' "$out" | head -1)" fi done for a in $ZV_SCRIPTS; do install -m 0755 "$dir/$a" "$dest/$a" || zv_die "could not install $a" done cp "$dir/SHA256SUMS" "$dest/SHA256SUMS" cp "$dir/RELEASE" "$dest/RELEASE" } # Points bin at releases/ in one rename, and removes releases that # are neither the new one nor the one it replaces. zv_activate() { local zhome="$1" version="$2" staged="$3" previous="" r mkdir -p "$zhome/releases" if [ -L "$zhome/bin" ]; then previous="$(basename "$(readlink "$zhome/bin")")" elif [ -e "$zhome/bin" ]; then mv "$zhome/bin" "$zhome/bin.old-$(date +%Y%m%d%H%M%S)" || zv_die "could not move the old $zhome/bin aside" fi if [ -e "$zhome/releases/$version" ]; then rm -rf "$zhome/releases/.replaced-$version" mv "$zhome/releases/$version" "$zhome/releases/.replaced-$version" fi mv "$staged" "$zhome/releases/$version" || zv_die "could not move the new release into place" ln -sfn "releases/$version" "$zhome/.bin.new" && mv -T "$zhome/.bin.new" "$zhome/bin" || zv_die "could not switch to the new release" rm -rf "$zhome/releases/.replaced-$version" for r in "$zhome"/releases/*; do [ -d "$r" ] || continue case "$(basename "$r")" in "$version" | "$previous") ;; *) rm -rf "$r" ;; esac done } # <<< shared: release download and verification <<< say() { printf '%s\n' "$*"; } step() { printf '\n== %s\n' "$*"; } die() { zv_die "$@"; } have_tty() { { : /dev/null; } ask_yn() { local q="$1" def="$2" hint ans [ "$def" = y ] && hint="[Y/n]" || hint="[y/N]" if ! have_tty; then [ "$def" = y ] return fi while :; do printf '%s %s ' "$q" "$hint" >/dev/tty IFS= read -r ans /dev/null; then IS_WSL=1 fi if [ "${ZECNERO_TEST_MODE:-}" = 1 ]; then case "${ZECNERO_PLATFORM:-}" in linux) IS_WSL=0 ;; wsl) IS_WSL=1 ;; esac fi if [ "$IS_WSL" = 1 ]; then say "Windows with WSL, x86_64"; else say "Linux, x86_64"; fi if [ "$(id -u)" = 0 ] && [ -n "${SUDO_USER:-}" ]; then die "please run the installer without sudo" \ "It installs into your own home folder and asks for sudo only if it needs it:" \ "curl -fsSL $INSTALL_URL | bash" fi glibc="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" [ -n "$glibc" ] || die "this Linux has no GNU C library (glibc)" \ "Zecnero runs on Ubuntu 22.04 or newer, or Debian 12 or newer." version_ge "$glibc" "$MIN_GLIBC" || die "this Linux is too old (glibc $glibc)" \ "Zecnero needs glibc $MIN_GLIBC or newer: Ubuntu 22.04 or newer, or Debian 12 or newer." case "$PWD" in /mnt/*) say "You started this from a Windows folder ($PWD), so it works from your Linux home folder instead: Windows folders are slow and break file permissions." cd "$HOME" || die "could not go to your home folder $HOME" ;; esac case "$ZHOME" in /mnt/*) die "Zecnero must be installed on the Linux side, not in a Windows folder" \ "ZECNERO_HOME is $ZHOME. Unset it to use $HOME/zecnero." ;; esac } check_tools() { local tool pkg missing="" pkgs="" for tool in curl tar xz sha256sum awk sed grep od setsid; do command -v "$tool" >/dev/null 2>&1 && continue case "$tool" in xz) pkg=xz-utils ;; sha256sum | od) pkg=coreutils ;; awk) pkg=gawk ;; setsid) pkg=util-linux ;; *) pkg="$tool" ;; esac missing="$missing $tool" case " $pkgs " in *" $pkg "*) ;; *) pkgs="$pkgs $pkg" ;; esac done if [ -n "$missing" ]; then die "some basic tools are missing:$missing" \ "Install them with this one line, then run the installer again:" \ "sudo apt update && sudo apt install -y$pkgs" fi say "Tools: all there" } check_resources() { local avail_kb mem_kb mem_gb mkdir -p "$HOME" 2>/dev/null avail_kb="$(df -Pk "$HOME" | awk 'NR == 2 { print $4 }')" if [ -n "$avail_kb" ] && [ "$avail_kb" -lt $((MIN_FREE_MB * 1024)) ]; then die "not enough free disk space" \ "Zecnero needs about $MIN_FREE_MB MB free in $HOME; there are $((avail_kb / 1024)) MB." \ "(The programs take about 100 MB and the Testnet chain a few MB today, growing slowly.)" fi say "Disk: $((avail_kb / 1024)) MB free, about $MIN_FREE_MB MB needed" mem_kb="$(awk '/^MemTotal:/ { print $2 }' /proc/meminfo)" mem_gb="$(awk -v k="$mem_kb" 'BEGIN { printf "%.1f", k / 1048576 }')" MEM_KB="$mem_kb" if [ "$mem_kb" -lt $((2560 * 1024)) ]; then say "Memory: $mem_gb GB. That is less than the 2.5 GB the miner's fast mode needs," say " so it will mine in light mode, which works but is several times slower." [ "$IS_WSL" = 1 ] && say " (WSL gets half of Windows' memory by default.)" elif [ "$mem_kb" -lt $((3584 * 1024)) ]; then say "Memory: $mem_gb GB. Enough for fast mode (about 2.5 GB), but other programs may feel slower while mining." else say "Memory: $mem_gb GB, enough for fast mode (about 2.5 GB)" fi } download_and_verify() { step "Downloading" zv_setup_trust zv_latest VERSION="$ZV_VERSION" # install.ps1 checks LATEST against its signed INSTALLERS.SHA256SUMS and passes it on. if [ -n "${ZECNERO_EXPECT_RELEASE:-}" ] && [ "$ZECNERO_EXPECT_RELEASE" != "$VERSION" ]; then die "LATEST says $VERSION, not $ZECNERO_EXPECT_RELEASE" \ "The Windows setup checked LATEST against the signed INSTALLERS.SHA256SUMS" \ "and found $ZECNERO_EXPECT_RELEASE; $(zv_host "$ZV_BASE") now says $VERSION. Nothing was installed." \ "A release published in the last few minutes can cause this: try again later." fi say "Newest release: $VERSION" WORK="$(mktemp -d "$HOME/.zecnero-download.XXXXXX")" || die "could not make a download folder in $HOME" trap 'rm -rf "$WORK"' EXIT zv_download "$VERSION" "$WORK" step "Checking the download" local sig=0 zv_check_signature "$WORK" || sig=$? if [ "$sig" = 2 ]; then printf '\n' zv_box "WARNING: THE SIGNATURE WAS NOT CHECKED" \ "gpg is not installed, so this installer could only check the" \ "checksums. They catch a broken download, but not a download" \ "server that was tampered with. This works for files from" \ "downloads.zecnero.org only: files from mirror.zecnero.org are" \ "never accepted without gpg. For the full check, install gpg" \ "with this line and run the installer again:" \ "sudo apt update && sudo apt install -y gnupg" printf '\n' else say "Signature OK: signed by the Zecnero release key $ZV_FPR" fi zv_check_sums "$WORK" "$VERSION" # From here on the release is the one its signed RELEASE names (equal to LATEST's). VERSION="$ZV_RELEASE" say "Checksums OK; the signed release is $VERSION" zv_unpack "$WORK" "$WORK/release" say "All programs run on this computer" } install_release() { step "Installing into $ZHOME" mkdir -p "$ZHOME" || die "could not create $ZHOME" # "Up to date" only with every program there: zecnero-miner update in # testnet-v0.1.2 installs a newer release without the programs it did not know. local a complete=1 for a in $ZV_ARCHIVES; do [ -x "$ZHOME/bin/${a%-linux-x86_64.tar.xz}" ] || complete=0 done if [ "$complete" = 1 ] && [ -L "$ZHOME/bin" ] && cmp -s "$ZHOME/bin/SHA256SUMS" "$WORK/release/SHA256SUMS" && [ "$(basename "$(readlink "$ZHOME/bin")")" = "$VERSION" ]; then say "You already have $VERSION. The programs are up to date." return 0 fi if [ -f "$ZHOME/run/supervisor.pid" ] && [ -x "$ZHOME/bin/zecnero-miner" ]; then local pid pid="$(head -1 "$ZHOME/run/supervisor.pid")" if [ -r "/proc/$pid/cmdline" ] && tr '\0' ' ' <"/proc/$pid/cmdline" | grep -q "zecnero-miner supervise"; then say "Stopping the running miner to upgrade it..." "$ZHOME/bin/zecnero-miner" stop /dev/null && continue # Ubuntu's own ~/.profile already adds ~/.local/bin when it exists. if [ "$f" = "$HOME/.profile" ] && grep -q '\.local/bin' "$f"; then continue; fi { printf '\n%s\n' "$marker" printf '%s\n' 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) export PATH="$HOME/.local/bin:$PATH" ;; esac' } >>"$f" NEED_SOURCE=1 done case ":$PATH:" in *":$link_dir:"*) ;; *) NEED_SOURCE=1 export PATH="$link_dir:$PATH" ;; esac say "Installed the zecnero-miner command in $link_dir" } # The mode an existing install mines in, before this run changes anything: # pool or solo, solo for an install from before pool mode, empty for none. existing_mode() { local conf="$ZHOME/config/miner.conf" m [ -f "$conf" ] || return 0 m="$(sed -n 's/^MODE=//p' "$conf" | tail -1)" case "$m" in pool) printf 'pool' ;; *) printf 'solo' ;; esac } # Sets MODE from ZECNERO_MODE, else the answer to the question, else keeps # the existing mode, else pool. choose_mode() { local previous="$1" def ans case "${ZECNERO_MODE:-}" in pool | solo) MODE="$ZECNERO_MODE" return 0 ;; "") ;; *) die "ZECNERO_MODE must be pool or solo" "It is: $ZECNERO_MODE" ;; esac def=1 [ "$previous" = solo ] && def=2 if ! have_tty; then [ "$def" = 1 ] && MODE=pool || MODE=solo # Pool mode needs a Sapling login, which only a terminal can make. if [ "$MODE" = pool ] && [ -z "${ZECNERO_POOL_LOGIN:-}" ] && ! "$ZHOME/bin/zecnero-miner" pool-login 2>/dev/null | grep -q '(Sapling'; then say "No terminal and no ZECNERO_POOL_LOGIN, so mining solo. For the pool later: zecnero-miner mode pool" MODE=solo fi return 0 fi { printf '\nHow do you want to mine?\n' printf ' 1) Pool (easiest, no node to sync, starts in about a minute;\n' printf ' it pays a shielded Sapling address, which the next step makes for you)\n' printf ' 2) Solo with your own node\n' } >/dev/tty while :; do if [ -n "$previous" ]; then printf 'Choose 1 or 2 (Enter keeps %s, what you use now) [%s]: ' "$previous" "$def" >/dev/tty else printf 'Choose 1 or 2 [%s]: ' "$def" >/dev/tty fi IFS= read -r ans /dev/tty ;; esac done } offer_hugepages() { local want current answer="" if [ "$IS_WSL" = 1 ]; then say "Huge pages: skipped on WSL, where Windows manages that memory. Mining works without them." return 0 fi want=$((1168 + $(nproc 2>/dev/null || echo 8))) [ "$want" -lt 1280 ] && want=1280 current="$(cat /proc/sys/vm/nr_hugepages 2>/dev/null || echo 0)" if [ "$current" -ge "$want" ]; then say "Huge pages: already on ($current)" return 0 fi if [ "${MEM_KB:-0}" -lt $((6 * 1024 * 1024)) ]; then say "Huge pages: skipped, this computer has less than 6 GB of memory." return 0 fi case "${ZECNERO_HUGEPAGES:-}" in yes | y | 1) answer=y ;; no | n | 0) answer=n ;; esac if [ -z "$answer" ]; then printf '\n' say "Optional: huge pages make RandomX mining about 20 to 30% faster. This" say "reserves about $((want * 2 / 1024)) GB of memory for the miner and needs your password (sudo)." if ask_yn "Turn on huge pages?" n; then answer=y; else answer=n; fi fi if [ "$answer" != y ]; then say "Huge pages: left off" return 0 fi local conf=/etc/sysctl.d/60-zecnero-hugepages.conf if printf 'vm.nr_hugepages = %s\n' "$want" | sudo tee "$conf" >/dev/null && sudo sysctl -q -w "vm.nr_hugepages=$want" >/dev/null 2>&1; then say "Huge pages: on ($want), and kept after a restart ($conf)" else sudo rm -f "$conf" 2>/dev/null say "Huge pages: could not be turned on here. Mining works without them." fi } offer_autostart() { command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1 || return 0 local answer="" case "${ZECNERO_AUTOSTART:-}" in yes | y | 1) answer=y ;; no | n | 0) answer=n ;; esac if [ -z "$answer" ]; then if ask_yn "Start mining automatically whenever this computer (or WSL) starts?" n; then answer=y; else answer=n; fi fi if [ "$answer" = y ]; then "$ZHOME/bin/zecnero-miner" autostart on /dev/null) Watch the miner: zecnero-miner logs miner Stop mining: zecnero-miner stop Start again: zecnero-miner start Mine solo instead: zecnero-miner mode solo All commands: zecnero-miner help The pool pays balances of 0.5 ZMR or more once the blocks they come from have 100 confirmations (about two hours), to your Sapling pool login: $("$ZHOME/bin/zecnero-miner" pool-login 2>/dev/null) Your pool page shows it all. The wallet's balance: $ZHOME/bin/zecnero-wallet balance (if the installer made that wallet, its seed is in $ZHOME/POOL-WALLET-SEED.txt). EOF else [ "$started" = 1 ] && printf '\nZecnero is mining in the background, solo with your own node.\n' cat <