#!/usr/bin/env bash # zecnero-miner: set up, run and watch a Zecnero Testnet miner. # # It mines in one of two modes (zecnero-miner mode pool, zecnero-miner mode solo): # # pool xmrig-zecnero mines on the Zecnero pool, stratum.zecnero.org:3334 # over TLS, and the pool pays you. No node to sync. # solo three programs run here, and a block you find pays you in full: # zecnerod the node, which follows the chain and hands out block templates # zecnero-bridge turns block templates into mining jobs (stratum on 127.0.0.1:3333) # xmrig-zecnero the RandomX miner # # Everything lives under ~/zecnero: # # bin/ the programs (a link to releases/) # config/ miner.conf (the mode and your address), xmrig-pool.json (pool # mode, written from miner.conf), and for solo mode # zecnerod.toml, zecnero-bridge.toml and xmrig.json # data/ the node's chain state and its RPC cookie # logs/ node.log, bridge.log, miner.log (each capped, one old copy kept) # run/ process ids and state, while running # MINING-KEY.txt # # Installed by install-miner.sh. Run `zecnero-miner help` for the commands. set -u set -o pipefail umask 022 ZHOME="${ZECNERO_HOME:-$HOME/zecnero}" BIN="$ZHOME/bin" CONF="$ZHOME/config" DATA="$ZHOME/data" LOGS="$ZHOME/logs" RUN="$ZHOME/run" KEYFILE="$ZHOME/MINING-KEY.txt" SETTINGS="$CONF/miner.conf" NODE_CONF="$CONF/zecnerod.toml" BRIDGE_CONF="$CONF/zecnero-bridge.toml" XMRIG_CONF="$CONF/xmrig.json" POOL_XMRIG_CONF="$CONF/xmrig-pool.json" NODE_DIR="$DATA/node" COOKIE="$NODE_DIR/.cookie" RPC_PORT=18732 P2P_PORT=18733 STRATUM_PORT=3333 DEFAULT_API_PORT=18088 # Easier-than-block shares (about 10 s each at 1 kH/s), so the miner shows # accepted shares from the start instead of only when it finds a block. The # bridge still submits only shares that meet the network target. SHARE_DIFFICULTY=10000 LOG_MAX_BYTES=$((10 * 1024 * 1024)) # The Zecnero pool. TLS on 3334 is preferred: some routers and antivirus # programs cut plain mining traffic after a few shares. POOL_HOST="stratum.zecnero.org" POOL_TLS_PORT=3334 POOL_PLAIN_PORT=3333 POOL_WEB="https://pool.zecnero.org" # ZECNERO_POOL_WEB points register-payout and the pool page at another pool # (tests): https, or http on 127.0.0.1 only. case "${ZECNERO_POOL_WEB:-}" in "") ;; https://* | http://127.0.0.1:*) POOL_WEB="${ZECNERO_POOL_WEB%/}" ;; *) printf 'Error: ZECNERO_POOL_WEB must be https://..., or http://127.0.0.1:PORT\n' >&2; exit 1 ;; esac # How long status reuses what the pool's API said. POOL_API_CACHE_SECONDS=60 UNIT_NAME="zecnero-miner.service" # >>> 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' "$*"; } warn() { printf 'Warning: %s\n' "$*" >&2; } die() { printf 'zecnero-miner: %s\n' "$1" >&2 shift local l for l in "$@"; do printf ' %s\n' "$l" >&2; done exit 1 } have_tty() { { : /dev/null; } # zecnerod reads every ZECNERO_* environment variable as a setting # (ZECNERO_SECTION__KEY), and stops on one it does not know, such as # ZECNERO_MINER_THREADS. The programs therefore run without them. drop_zecnero_env() { local v for v in $(compgen -e); do case "$v" in ZECNERO_*) unset "$v" ;; esac done } clean_run() { (drop_zecnero_env && exec "$@"); } # ask_yn QUESTION DEFAULT(y|n): reads the answer from the terminal, so it works # when this runs from a pipe. With no terminal the default is taken. 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 "$tmp" && mv "$tmp" "$file" } # Fails, naming the problem, if a TOML file repeats a table or a key in a table. toml_check_duplicates() { awk ' /^[ \t]*\[[A-Za-z0-9_.-]+\][ \t]*(#.*)?$/ { s = $0; sub(/^[ \t]*\[/, "", s); sub(/\].*$/, "", s) if (s in tables) { print "the section [" s "] appears twice"; bad = 1 } tables[s] = 1; next } /^[ \t]*[A-Za-z0-9_-]+[ \t]*=/ { k = $0; sub(/^[ \t]*/, "", k); sub(/[ \t]*=.*$/, "", k) if ((s SUBSEP k) in keys) { print "the key " k " appears twice in [" s "]"; bad = 1 } keys[s SUBSEP k] = 1 } END { exit bad } ' "$1" } # ---------------------------------------------------------------- processes pid_of() { [ -f "$RUN/$1.pid" ] && head -1 "$RUN/$1.pid"; } # pid_alive PID PATTERN: true if PID runs and its command line matches PATTERN. pid_alive() { local pid="$1" pat="$2" [ -n "$pid" ] && [ -r "/proc/$pid/cmdline" ] || return 1 tr '\0' ' ' <"/proc/$pid/cmdline" 2>/dev/null | grep -q -- "$pat" } supervisor_pid() { local pid pid="$(pid_of supervisor)" || return 1 pid_alive "$pid" "zecnero-miner supervise" && printf '%s\n' "$pid" } component_bin() { case "$1" in node) printf 'zecnerod' ;; bridge) printf 'zecnero-bridge' ;; miner) printf 'xmrig-zecnero' ;; esac } component_alive() { local pid pid="$(pid_of "$1")" || return 1 pid_alive "$pid" "$(component_bin "$1")" } port_open() { (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null; } state_of() { [ -f "$RUN/$1.state" ] && head -1 "$RUN/$1.state"; } # ---------------------------------------------------------------- node and miner queries rpc() { local method="$1" params="${2:-[]}" [ -r "$COOKIE" ] || return 1 printf 'user = "%s"\n' "$(head -1 "$COOKIE")" | curl -s --max-time "${RPC_TIMEOUT:-5}" -K - -H 'content-type: application/json' \ --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$method\",\"params\":$params}" \ "http://127.0.0.1:$RPC_PORT/" 2>/dev/null } json_num() { printf '%s' "$1" | tr -d ' \n' | sed -n "s/.*\"$2\":\([0-9.]*\).*/\1/p" | head -1; } # Sets NODE_BLOCKS, NODE_TARGET, NODE_PEERS and NODE_READY (1 when the node # serves templates, which Zebra only does once it has caught up with its peers). node_query() { local info net gbt NODE_BLOCKS="" NODE_TARGET="" NODE_PEERS="" NODE_READY=0 info="$(rpc getblockchaininfo)" || return 1 NODE_BLOCKS="$(json_num "$info" blocks)" [ -n "$NODE_BLOCKS" ] || return 1 NODE_TARGET="$(json_num "$info" estimatedheight)" [ -z "$NODE_TARGET" ] || [ "$NODE_TARGET" -lt "$NODE_BLOCKS" ] && NODE_TARGET="$NODE_BLOCKS" net="$(rpc getnetworkinfo)" && NODE_PEERS="$(json_num "$net" connections)" gbt="$(RPC_TIMEOUT=10 rpc getblocktemplate)" || gbt="" case "$gbt" in *'"result":{'*) NODE_READY=1 ;; esac return 0 } node_has_peers() { local net peers net="$(rpc getnetworkinfo)" || return 1 peers="$(json_num "$net" connections)" [ -n "$peers" ] && [ "$peers" -gt 0 ] } sync_text() { local pct=0 [ "$NODE_TARGET" -gt 0 ] && pct=$((100 * NODE_BLOCKS / NODE_TARGET)) [ "$pct" -gt 99 ] && pct=99 printf 'height %s / %s (%s%%)' "$NODE_BLOCKS" "$NODE_TARGET" "$pct" } api_summary() { local port token port="$(setting API_PORT)" token="$(setting API_TOKEN)" [ -n "$port" ] || return 1 printf 'header = "Authorization: Bearer %s"\n' "$token" | curl -s --max-time 3 -K - "http://127.0.0.1:$port/2/summary" 2>/dev/null } # "8.2 kH/s" from a hashrate in H/s. fmt_rate() { awk -v h="$1" 'BEGIN { if (h >= 1000000) printf "%.1f MH/s", h / 1000000 else if (h >= 1000) printf "%.1f kH/s", h / 1000 else printf "%d H/s", h }' } # A component that is not running: crashed if the supervisor still expects # it (it restarts it within seconds), stopped otherwise. gone_text() { if [ "$(state_of "$1")" = running ] && supervisor_pid >/dev/null; then printf 'crashed, restarting now' else printf 'stopped' fi } restart_text() { local st="$1" at now at="${st#restarting }" now="$(date +%s)" if [ "$at" -gt "$now" ] 2>/dev/null; then printf 'crashed, restarting in %s s' "$((at - now))" else printf 'crashed, restarting now' fi } # ---------------------------------------------------------------- setup keygen_new() { local out if out="$(clean_run "$BIN/zecnero-keygen" --network testnet --json 2>/dev/null)" && [ -n "$out" ]; then KEY_ADDRESS="$(printf '%s' "$out" | sed -n 's/.*"address":"\([^"]*\)".*/\1/p' | head -1)" KEY_WIF="$(printf '%s' "$out" | sed -n 's/.*"wif":"\([^"]*\)".*/\1/p' | head -1)" else out="$(clean_run "$BIN/zecnero-keygen" --network testnet 2>/dev/null)" || return 1 KEY_ADDRESS="$(printf '%s\n' "$out" | sed -n 's/^address:[ ]*//p' | head -1)" KEY_WIF="$(printf '%s\n' "$out" | sed -n 's/^private key:[ ]*//p' | head -1)" fi unset out case "$KEY_ADDRESS" in nm?*) ;; *) return 1 ;; esac [ -n "$KEY_WIF" ] } write_keyfile() { local tmp tmp="$(umask 077 && mktemp "$ZHOME/.MINING-KEY.XXXXXX")" || return 1 cat >"$tmp" </dev/tty local ans while :; do printf 'Type SAVED and press Enter once you have saved it: ' >/dev/tty IFS= read -r ans /dev/tty ;; esac done # Take the key off the screen and out of the scrollback. printf '\033[3J\033[H\033[2J' >/dev/tty say "Thanks. Your key is also in $KEYFILE" } setup_key() { if [ -f "$KEYFILE" ]; then KEY_ADDRESS="$(sed -n 's/^Address:[ ]*//p' "$KEYFILE" | head -1)" if [ -n "$KEY_ADDRESS" ] && ! ask_yn "You already have a mining key ($KEY_ADDRESS). Make a new one instead?" n; then say "Keeping your mining key: $KEY_ADDRESS" return 0 fi [ -n "$KEY_ADDRESS" ] || warn "$KEYFILE has no address in it; making a new key." local backup backup="$ZHOME/MINING-KEY-$(date +%Y%m%d-%H%M%S).txt" mv "$KEYFILE" "$backup" && say "Your old key file is kept as $backup" fi keygen_new || die "could not make a mining key with zecnero-keygen." write_keyfile || die "could not write $KEYFILE" show_key_and_confirm unset KEY_WIF } write_node_config() { local tmp="$NODE_CONF.new" out # zecnerod generate writes every section once; the values below are then # set in place. Releases before v0.1.1 have no --network flag here. if ! out="$(clean_run "$BIN/zecnerod" generate --network testnet -o "$tmp" 2>&1)" || [ ! -s "$tmp" ]; then if ! out="$(clean_run "$BIN/zecnerod" generate -o "$tmp" 2>&1)" || [ ! -s "$tmp" ]; then rm -f "$tmp" die "could not create the node's settings file." "$out" fi fi toml_set "$tmp" network network '"Testnet"' toml_set "$tmp" network listen_addr "\"[::]:$P2P_PORT\"" toml_set "$tmp" rpc listen_addr "\"127.0.0.1:$RPC_PORT\"" toml_set "$tmp" rpc enable_cookie_auth true toml_set "$tmp" rpc cookie_dir "\"$NODE_DIR\"" toml_set "$tmp" state cache_dir "\"$NODE_DIR\"" toml_set "$tmp" mining miner_address "\"$KEY_ADDRESS\"" toml_set "$tmp" tracing use_color false validate_node_config "$tmp" || { rm -f "$tmp" die "the node's settings file did not pass its check." } mv "$tmp" "$NODE_CONF" } validate_node_config() { local file="$1" toml_check_duplicates "$file" >&2 || return 1 grep -q "^miner_address = \"$KEY_ADDRESS\"" "$file" || { echo "miner_address is not set" >&2 return 1 } # check-config reads the file as start would, and checks the network, the # miner address and the folders, without opening ports or the state. local out if ! out="$(clean_run timeout 60 "$BIN/zecnerod" -c "$file" check-config 2>&1)"; then printf '%s\n' "$out" | tail -3 >&2 return 1 fi case "$out" in *"Config OK: Testnet"*"miner address set"*) return 0 ;; *) printf 'zecnerod check-config said: %s\n' "$out" >&2 return 1 ;; esac } write_bridge_config() { local tmp="$BRIDGE_CONF.new" out if ! out="$(clean_run "$BIN/zecnero-bridge" generate -o "$tmp" 2>&1)" || [ ! -s "$tmp" ]; then # Releases before v0.1.1 have no generate command and need the network. printf '[node]\nnetwork = "Testnet"\n\n[stratum]\n' >"$tmp" fi toml_set "$tmp" node rpc_url "\"http://127.0.0.1:$RPC_PORT\"" toml_set "$tmp" node cookie_path "\"$COOKIE\"" # Makes the bridge refuse a node that is not on Testnet. toml_set "$tmp" node network '"Testnet"' toml_set "$tmp" stratum listen_addr "\"127.0.0.1:$STRATUM_PORT\"" toml_set "$tmp" stratum share_difficulty "$SHARE_DIFFICULTY" validate_bridge_config "$tmp" || { rm -f "$tmp" die "the bridge's settings file did not pass its check." } mv "$tmp" "$BRIDGE_CONF" } validate_bridge_config() { local file="$1" out toml_check_duplicates "$file" >&2 || return 1 # The bridge reads its settings before anything else, so a settings problem # is the first thing it reports. Anything else (no node yet, no cookie yet) # is expected at this point. out="$(cd "$CONF" && clean_run timeout 30 "$BIN/zecnero-bridge" -c "$file" --check-template 2>&1 | sed 's/\x1b\[[0-9;]*[A-Za-z]//g')" if printf '%s\n' "$out" | grep -Eiq "could not (parse|read) [^ ]*$(basename "$file")|unknown field|duplicate key|invalid type|TOML parse"; then printf '%s\n' "$out" | tail -3 >&2 return 1 fi return 0 } random_token() { head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n'; } # The "pools" entry that sends work to the local bridge (solo mode). bridge_pools() { cat <"$tmp" <"$tmp" mv "$tmp" "$SETTINGS" } # set_setting KEY VALUE [NEXT]: sets KEY in miner.conf in place, or adds it at # the end (or just before NEXT, when miner.conf has it), keeping every other # line as it is. set_setting() { local key="$1" value="$2" next="${3:-}" tmp tmp="$(umask 077 && mktemp "$SETTINGS.XXXXXX")" || return 1 [ -f "$SETTINGS" ] || : >"$SETTINGS" grep -q "^$key=" "$SETTINGS" && next="" awk -v k="$key" -v v="$value" -v n="$next" ' index($0, k "=") == 1 { if (!done) print k "=" v; done = 1; next } n != "" && !done && index($0, n "=") == 1 { print k "=" v; done = 1 } { print } END { if (!done) print k "=" v } ' "$SETTINGS" >"$tmp" && mv "$tmp" "$SETTINGS" } # ---------------------------------------------------------------- pool or solo # The mode miner.conf asks for. An install from before pool mode existed has # no MODE, and mines solo as it always did. configured_mode() { case "$(setting MODE)" in pool) printf 'pool' ;; *) printf 'solo' ;; esac } # The mode of the running supervisor, else the configured one. A supervisor # started by a wrapper from before pool mode leaves no run/mode, and runs solo. active_mode() { local m if supervisor_pid >/dev/null; then m="$(head -1 "$RUN/mode" 2>/dev/null)" case "$m" in pool | solo) printf '%s' "$m" ;; *) printf 'solo' ;; esac else configured_mode fi } # Letters, digits, - and _ only, at most 20 characters (the pool keeps those). sanitize_worker() { printf '%s' "$1" | tr -cd 'A-Za-z0-9_-' | cut -c1-20; } # A worker name from this computer's short host name. default_worker() { local h="${HOSTNAME:-}" [ -n "$h" ] || h="$(cat /proc/sys/kernel/hostname 2>/dev/null)" h="$(sanitize_worker "${h%%.*}")" printf '%s' "${h:-rig}" } pool_worker() { local w w="$(sanitize_worker "$(setting WORKER)")" [ -n "$w" ] || w="$(default_worker)" printf '%s' "$w" } pool_tls() { case "$(setting POOL_TLS)" in no | off | 0 | false) printf 'no' ;; *) printf 'yes' ;; esac } # The pool the miner is meant to use, as status shows it. pool_target() { if [ "$(pool_tls)" = yes ]; then printf '%s:%s (TLS)' "$POOL_HOST" "$POOL_TLS_PORT" else printf '%s:%s (plain)' "$POOL_HOST" "$POOL_PLAIN_PORT" fi } miner_address() { local a a="$(setting ADDRESS)" [ -n "$a" ] || a="$(sed -n 's/^Address:[ ]*//p' "$KEYFILE" 2>/dev/null | head -1)" printf '%s' "$a" } pool_page() { printf '%s/miner/%s' "$POOL_WEB" "$1"; } # ---------------------------------------------------------------- pool login # From the shielded-only network upgrade (NU1) on, Testnet pays shielded # addresses only: a transaction that is not a coinbase may not pay a # transparent (nm...) address. Your mining key stays the solo mining address # (a coinbase may still pay nm...), but the pool cannot pay one, so it refuses # nm... logins. From the Orchard-only upgrade (NU2) on, no transaction may pay # a Sapling address either, so the pool stops taking Sapling logins at its # Sapling stop, ahead of NU2: orchardOnly.loginStopHeight in its /api/stats, # by default NU2 - 160 (its sapling_cutoff_blocks, 60, and maturity, 100), so # that what a Sapling login earns matures and is paid before NU2. Pool mode # logs in with a unified address with an Orchard receiver, POOL_LOGIN in # miner.conf. Before each stop the login it ends still works, with a # countdown warning. # # The activation heights must match zecnerod's Testnet NU1 and NU2 heights # (zebra-chain parameters/constants.rs activation_heights::testnet, and # ZECNERO_TESTNET_NU1_HEIGHT and ZECNERO_TESTNET_NU2_HEIGHT in its vendored # zcash_protocol). 52063 is the height zecnerod's NU2 line got at # the NU2 freeze; ZECNERO_TEST_ORCHARD_ONLY_HEIGHT sets it for the # offline tests. Blocks come every 75 s on average (zebra-chain # work/asert.rs BLOCK_TARGET_SPACING). SHIELDED_ONLY_ACTIVATION_HEIGHT=16529 ORCHARD_ONLY_HEIGHT="${ZECNERO_TEST_ORCHARD_ONLY_HEIGHT:-52063}" SAPLING_STOP_BLOCKS=160 BLOCK_TARGET_SECONDS=75 POOL_HEIGHT_CACHE_SECONDS=60 # miner.conf's NETWORK: testnet, or regtest for the local tests. MINER_NETWORK="$(setting NETWORK)" case "$MINER_NETWORK" in regtest) SAPLING_HRP="nregtestsapling" UNIFIED_HRP="nuregtest" ;; *) MINER_NETWORK=testnet SAPLING_HRP="ntestsapling" UNIFIED_HRP="nutest" ;; esac # The wallet the pool login is made in: zecnero-wallet's default Testnet # wallet, so `zecnero-wallet balance` finds it with no options. pool_wallet_file() { printf '%s/zecnero-wallet/testnet.wallet' "${XDG_DATA_HOME:-$HOME/.local/share}"; } SEEDFILE="$ZHOME/POOL-WALLET-SEED.txt" # login_kind ADDRESS: unified, sapling, transparent, or empty for neither. # A Sapling address is the HRP, 1, and 75 bech32 characters (43 bytes and a # checksum). A unified address is the HRP, 1, and at least 104 bech32 # characters: an Orchard receiver alone is 61 bytes with the HRP padding # (ZIP 316), and a checksum. The pool checks the checksums, and that a # unified login has an Orchard receiver. login_kind() { local a="$1" case "$a" in "$UNIFIED_HRP"1*) [[ "$a" =~ ^${UNIFIED_HRP}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{104,}$ ]] && printf 'unified' ;; "$SAPLING_HRP"1*) [[ "$a" =~ ^${SAPLING_HRP}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{75}$ ]] && printf 'sapling' ;; nm?*) printf 'transparent' ;; esac } pool_login() { setting POOL_LOGIN; } # json_obj JSON KEY: the members of the flat object "KEY":{...} in JSON, so # that json_num reads a key there and not one of the same name elsewhere # (network.height, not orchardOnly.height). json_obj() { printf '%s' "$1" | tr -d ' \n' | sed -n "s/.*\"$2\":{\([^{}]*\)}.*/\1/p" | head -1; } # The pool's /api/stats, cached a minute in run/pool-stats.json. With # ZECNERO_TEST_HEIGHT set (the offline tests) it never goes online and # answers from the cache alone. pool_stats() { local cache="$RUN/pool-stats.json" age tmp if [ -s "$cache" ]; then age=$(($(date +%s) - $(stat -c %Y "$cache" 2>/dev/null || echo 0))) if [ "$age" -lt "$POOL_HEIGHT_CACHE_SECONDS" ] || [ -n "${ZECNERO_TEST_HEIGHT:-}" ]; then cat "$cache" return 0 fi fi [ -z "${ZECNERO_TEST_HEIGHT:-}" ] || return 1 mkdir -p "$RUN" 2>/dev/null tmp="$(curl -fsS --max-time 6 --proto '=https' "$POOL_WEB/api/stats" 2>/dev/null)" || return 1 printf '%s\n' "$tmp" >"$cache" 2>/dev/null printf '%s' "$tmp" } # The chain height: from the local node when it answers, else the pool's # /api/stats (network.height), else empty. # ZECNERO_TEST_HEIGHT sets it for the offline tests. chain_height() { local h if [ -n "${ZECNERO_TEST_HEIGHT:-}" ]; then printf '%s' "$ZECNERO_TEST_HEIGHT" return 0 fi if [ -f "$COOKIE" ] && h="$(json_num "$(rpc getblockchaininfo 2>/dev/null)" blocks)" && [ -n "$h" ]; then printf '%s' "$h" return 0 fi h="$(json_num "$(json_obj "$(pool_stats)" network)" height)" [ -n "$h" ] || return 1 printf '%s' "$h" } # The pool's Sapling stop: orchardOnly.loginStopHeight from its /api/stats, or # from a pool that does not say (or does not answer) NU2 - SAPLING_STOP_BLOCKS. sapling_stop_height() { local h h="$(json_num "$(json_obj "$(pool_stats)" orchardOnly)" loginStopHeight)" case "$h" in "" | *[!0-9]*) h=$((ORCHARD_ONLY_HEIGHT - SAPLING_STOP_BLOCKS)) ;; esac printf '%s' "$h" } # reached HEIGHT: true once the chain has reached HEIGHT. An unknown height # counts as before it: the pool itself refuses what it no longer takes. reached() { local h h="$(chain_height)" || return 1 case "$h" in "" | *[!0-9]*) return 1 ;; esac [ "$h" -ge "$1" ] } shielded_only_active() { reached "$SHIELDED_ONLY_ACTIVATION_HEIGHT"; } orchard_only_active() { reached "$ORCHARD_ONLY_HEIGHT"; } # True once the pool takes no Sapling login: its /api/stats says logins # "unified", or the chain has reached its Sapling stop. sapling_refused() { [ "$(json_str "$(pool_stats)" logins)" = unified ] && return 0 reached "$(sapling_stop_height)" } # countdown_text [HEIGHT]: "at block H, in 1234 blocks (about 1 day 2 h)", or # "at block H" when the chain height is unknown. HEIGHT defaults to NU1's. countdown_text() { local at="${1:-$SHIELDED_ONLY_ACTIVATION_HEIGHT}" h left secs h="$(chain_height)" || h="" case "$h" in "" | *[!0-9]*) printf 'at block %s' "$at" return 0 ;; esac left=$((at - h)) [ "$left" -lt 0 ] && left=0 secs=$((left * BLOCK_TARGET_SECONDS)) if [ "$secs" -ge 86400 ]; then printf 'at block %s, in %s blocks (about %s days %s h)' "$at" "$left" $((secs / 86400)) $((secs % 86400 / 3600)) elif [ "$secs" -ge 3600 ]; then printf 'at block %s, in %s blocks (about %s h %s min)' "$at" "$left" $((secs / 3600)) $((secs % 3600 / 60)) else printf 'at block %s, in %s blocks (about %s min)' "$at" "$left" $(((secs + 59) / 60)) fi } # The login the pool gets: POOL_LOGIN when it is unified, or Sapling before # the pool's Sapling stop; before NU1, else the old nm... login (POOL_LOGIN or # the mining address). effective_pool_login() { local a a="$(pool_login)" case "$(login_kind "$a")" in unified) printf '%s' "$a" return 0 ;; sapling) sapling_refused || printf '%s' "$a" return 0 ;; esac shielded_only_active && return 0 [ "$(login_kind "$a")" = transparent ] || a="$(miner_address)" [ "$(login_kind "$a")" = transparent ] && printf '%s' "$a" } # The type as status shows it. login_type_text() { case "$(login_kind "$1")" in unified) printf 'unified, Orchard payouts' ;; sapling) if sapling_refused; then printf 'Sapling: the pool refuses it, ahead of NU2' else printf 'Sapling: the pool stops taking it %s, ahead of NU2' "$(countdown_text "$(sapling_stop_height)")" fi ;; transparent) if shielded_only_active; then printf 'transparent: the pool refuses it' else printf 'transparent: the pool stops taking it %s' "$(countdown_text)" fi ;; *) printf 'not a Testnet unified address' ;; esac } pool_login_ok() { [ "$(login_kind "$(pool_login)")" = unified ]; } # A login the pool takes now: unified, Sapling before the pool's Sapling stop, # or nm... before NU1. pool_login_usable() { [ -n "$(effective_pool_login)" ]; } # What to do when pool mode has no unified login: an install from before NU2 # logs in with a Sapling address, and one from before NU1 with its nm... # mining address. pool_login_notice() { local old move="" old="$(pool_login)" if [ "$(login_kind "$old")" = sapling ]; then if sapling_refused; then set -- "ACTION NEEDED: THE POOL NEEDS A UNIFIED ADDRESS" \ "Zecnero Testnet is Orchard-only from the NU2 upgrade (block $ORCHARD_ONLY_HEIGHT):" \ "no transaction can pay a Sapling address, so the pool refuses" \ "Sapling logins such as yours:" orchard_only_active || move="yes" else set -- "SWITCH SOON: THE POOL WILL NEED A UNIFIED ADDRESS" \ "The NU2 network upgrade (block $ORCHARD_ONLY_HEIGHT) makes Zecnero Testnet" \ "Orchard-only: no transaction can pay a Sapling address from then on." \ "The pool stops taking Sapling logins ahead of it," \ "$(countdown_text "$(sapling_stop_height)"), such as yours:" move="yes" fi else [ -n "$old" ] || old="$(miner_address)" if shielded_only_active; then set -- "ACTION NEEDED: THE POOL NEEDS A UNIFIED ADDRESS" \ "Zecnero Testnet now pays shielded addresses only. The pool pays" \ "a unified address (${UNIFIED_HRP}1...), so it refuses logins" \ "with a transparent nm... address such as yours:" else set -- "SWITCH SOON: THE POOL WILL NEED A UNIFIED ADDRESS" \ "A network upgrade makes Zecnero Testnet pay shielded addresses only," \ "$(countdown_text)." \ "From then on the pool pays a unified address (${UNIFIED_HRP}1...)," \ "and refuses your transparent nm... login:" fi fi set -- "$@" " ${old:-none}" "" if [ -n "$move" ]; then set -- "$@" "Move what that Sapling address holds to Orchard before NU2, or it" \ "stays there for good: zecnero-miner move-to-orchard (for the wallet" \ "pool-login new made; another wallet has its own command)." "" fi zv_box "$@" \ "Your mining key does not change, and still works for solo mining." \ "" \ "To keep mining on the pool, pick one:" \ " zecnero-miner pool-login new" \ " make a new wallet (or use the one you have), and log in with its" \ " unified address" \ " zecnero-miner pool-login ${UNIFIED_HRP}1..." \ " log in with a unified address you already have" \ "Or mine solo with your mining key: zecnero-miner mode solo" \ "(zecnero-miner update also offers to do it.)" >&2 } # ask_saved FILE: waits until the user types SAVED, then clears the screen. ask_saved() { local ans while :; do printf 'Type SAVED and press Enter once you have saved it: ' >/dev/tty IFS= read -r ans /dev/tty ;; esac done # Take it off the screen and out of the scrollback. printf '\033[3J\033[H\033[2J' >/dev/tty } # read_passphrase: a new wallet passphrase, typed twice, into PASSPHRASE. # ZECNERO_WALLET_PASSPHRASE answers it without a terminal (tests). read_passphrase() { PASSPHRASE="${ZECNERO_WALLET_PASSPHRASE:-}" [ -n "$PASSPHRASE" ] && return 0 have_tty || die "making a wallet needs a terminal, to set its passphrase and show its seed." \ "Run this in a terminal window: zecnero-miner pool-login new" local again while :; do printf 'Choose a passphrase for the new wallet (it does not show while you type): ' >/dev/tty IFS= read -rs PASSPHRASE /dev/tty IFS= read -rs again /dev/tty if [ -z "$PASSPHRASE" ]; then printf 'The passphrase cannot be empty.\n' >/dev/tty elif [ "$PASSPHRASE" != "$again" ]; then printf 'The two did not match. Once more.\n' >/dev/tty else break fi done unset again } write_seedfile() { local words="$1" address="$2" tmp tmp="$(umask 077 && mktemp "$ZHOME/.POOL-WALLET-SEED.XXXXXX")" || return 1 cat >"$tmp" </dev/tty ask_saved "$SEEDFILE" say "Thanks. Your seed is also in $SEEDFILE" } # new_pool_wallet: makes a new wallet with the bundled zecnero-wallet and # sets NEW_LOGIN to its unified address. The seed goes to # POOL-WALLET-SEED.txt and on screen, with the same SAVED step as the # mining key. An existing zecnero-wallet Testnet wallet is used instead. new_pool_wallet() { local wallet out words="" address [ -x "$BIN/zecnero-wallet" ] || die "this release has no zecnero-wallet, so it cannot make a wallet." \ "Update first (zecnero-miner update), or log in with a unified address you have:" \ "zecnero-miner pool-login ${UNIFIED_HRP}1..." wallet="$(pool_wallet_file)" if [ -e "$wallet" ]; then say "You already have a zecnero-wallet Testnet wallet ($wallet)." say "Using its unified address." PASSPHRASE="${ZECNERO_WALLET_PASSPHRASE:-}" if [ -z "$PASSPHRASE" ]; then have_tty || die "this needs a terminal, for the wallet's passphrase." printf "That wallet's passphrase: " >/dev/tty IFS= read -rs PASSPHRASE /dev/tty fi else read_passphrase [ -f "$SEEDFILE" ] && mv "$SEEDFILE" "$ZHOME/POOL-WALLET-SEED-$(date +%Y%m%d-%H%M%S).txt" if ! out="$(ZWALLET_PASSPHRASE="$PASSPHRASE" clean_run "$BIN/zecnero-wallet" --network testnet new 2>&1)"; then unset PASSPHRASE die "zecnero-wallet could not make the wallet:" "$out" fi words="$(printf '%s\n' "$out" | grep -oE '[0-9]+\. +[a-z]+' | awk '{ print $2 }' | tr '\n' ' ' | sed 's/ $//')" unset out if [ "$(printf '%s' "$words" | wc -w)" != 24 ]; then unset PASSPHRASE words die "zecnero-wallet made the wallet ($wallet), but its seed could not be read here." \ "Its seed was shown only once; if you did not see it, delete that file and run: zecnero-miner pool-login new" fi fi address="$(ZWALLET_PASSPHRASE="$PASSPHRASE" clean_run "$BIN/zecnero-wallet" --network testnet unified-address 2>/dev/null | tail -1 | tr -d ' \r')" unset PASSPHRASE if [ -n "$words" ]; then write_seedfile "$words" "${address:-see: zecnero-wallet unified-address}" || die "could not write $SEEDFILE" show_seed_and_confirm "$words" "$address" unset words fi [ "$(login_kind "$address")" = unified ] || die "zecnero-wallet did not give a unified address (got: ${address:-nothing})." \ "Check the passphrase, then run: zecnero-miner pool-login new" NEW_LOGIN="$address" } # set_pool_login ADDRESS|new: checks it and saves POOL_LOGIN. A Sapling # address is taken before the pool's Sapling stop only, with a warning. set_pool_login() { local a old kind a="$(printf '%s' "$1" | tr -d ' \t\r')" old="$(pool_login)" if [ "$a" = new ]; then new_pool_wallet a="$NEW_LOGIN" fi kind="$(login_kind "$a")" case "$kind" in unified) ;; sapling) if sapling_refused; then die "the pool takes no Sapling login now ($a)." \ "Testnet is Orchard-only from block $ORCHARD_ONLY_HEIGHT (NU2), and the pool stops Sapling logins ahead of it. Log in with a unified address (${UNIFIED_HRP}1...)," \ "or make one: zecnero-miner pool-login new" fi warn "the pool stops taking Sapling logins $(countdown_text "$(sapling_stop_height)"), ahead of NU2. Switch to a unified address before then: zecnero-miner pool-login new" ;; transparent) die "the pool cannot pay a transparent address ($a)." \ "Testnet pays shielded addresses only from block $SHIELDED_ONLY_ACTIVATION_HEIGHT. Log in with a unified address (${UNIFIED_HRP}1...)," \ "or make one: zecnero-miner pool-login new" \ "(Your nm... mining key still works for solo mining: zecnero-miner mode solo)" ;; *) die "that is not a Zecnero Testnet unified address: ${a:-empty}" \ "It starts with ${UNIFIED_HRP}1. Show yours with: zecnero-wallet unified-address. Or make one: zecnero-miner pool-login new" ;; esac [ "$(setting POOL_LOGIN)" = "$a" ] || set_setting POOL_LOGIN "$a" if [ "$kind" = unified ]; then say "Pool login: $a (unified)" else say "Pool login: $a (Sapling)" fi if [ "$kind" = unified ] && [ "$(login_kind "$old")" = sapling ] && ! orchard_only_active; then say "Move what your old Sapling login holds to Orchard before NU2 (block $ORCHARD_ONLY_HEIGHT): zecnero-miner move-to-orchard" fi } # ensure_pool_login: pool mode needs a unified login. ZECNERO_POOL_LOGIN # (an address, or new) answers without asking; else in a terminal it asks. # Before the pool's Sapling stop a Sapling login (before NU1 an nm... one) # still works: without an answer it warns and goes on. From then on, it stops. ensure_pool_login() { pool_login_ok && return 0 if [ -n "${ZECNERO_POOL_LOGIN:-}" ]; then set_pool_login "$ZECNERO_POOL_LOGIN" return 0 fi if pool_login_usable; then pool_login_notice if have_tty && ask_yn "Switch to a unified pool login now?" y; then ask_pool_login fi return 0 fi if ! have_tty; then pool_login_notice die "pool mode needs a unified address to log in with, and there is no terminal to ask for one." \ "Set one with: zecnero-miner pool-login new (or pool-login ${UNIFIED_HRP}1...)" fi ask_pool_login } # ask_pool_login: asks in the terminal for a unified login, new or entered. ask_pool_login() { local ans { printf '\nThe pool pays unified addresses (through Orchard), so pool mining needs one.\n' printf 'Your mining key stays, for solo mining.\n\n' printf ' 1) Make a new wallet with zecnero-wallet and use its unified address (easiest)\n' printf ' 2) Use a unified address I already have (%s1...)\n' "$UNIFIED_HRP" } >/dev/tty while :; do printf 'Choose 1 or 2 [1]: ' >/dev/tty IFS= read -r ans /dev/tty IFS= read -r ans /dev/tty ;; esac done } # zecnero-miner pool-login prints the login and its type # zecnero-miner pool-login new makes a wallet and logs in with it # zecnero-miner pool-login nutest1... logs in with that address cmd_pool_login() { local a="${1:-}" if [ -z "$a" ]; then a="$(pool_login)" if [ -z "$a" ]; then say "No pool login yet. Set one with: zecnero-miner pool-login new (or pool-login ${UNIFIED_HRP}1...)" else say "$a ($(login_type_text "$a"))" fi if [ "$(configured_mode)" = pool ] && ! pool_login_ok; then pool_login_notice; fi return 0 fi require_setup pool if [ "$a" = new ]; then complete_release || exit 1 else complete_release fi set_pool_login "$a" if [ "$(configured_mode)" = pool ]; then write_pool_xmrig_config || die "could not write $POOL_XMRIG_CONF" if supervisor_pid >/dev/null && [ "${ZECNERO_NO_START:-}" != 1 ]; then say "Restarting the miner with the new login." cmd_stop && cmd_start fi fi } # zecnero-miner move-to-orchard [ARGS...]: moves every Sapling note of # zecnero-wallet's Testnet wallet (the one pool-login new makes) to its # Orchard address, with the bundled zecnero-wallet (its move-to-orchard # command; ARGS go to it). From NU2 no transaction can spend a Sapling note, # so what is left in Sapling then stays there for good. The wallet asks for # its passphrase itself. cmd_move_to_orchard() { local wallet wallet="$(pool_wallet_file)" [ -e "$wallet" ] || die "there is no zecnero-wallet Testnet wallet here ($wallet)." \ "A wallet made elsewhere moves its Sapling funds with its own move-to-orchard command." complete_release || exit 1 if ! { [ -x "$BIN/zecnero-wallet" ] && clean_run "$BIN/zecnero-wallet" move-to-orchard --help >/dev/null 2>&1; }; then die "this release's zecnero-wallet cannot move Sapling funds to Orchard." "Update first: zecnero-miner update" fi if orchard_only_active; then warn "NU2 (block $ORCHARD_ONLY_HEIGHT) has passed: Sapling notes can no longer be spent. zecnero-wallet says what it holds where." fi clean_run "$BIN/zecnero-wallet" --network "$MINER_NETWORK" move-to-orchard "$@" } # ---------------------------------------------------------------- register a payout address # After the shielded-only upgrade the pool cannot pay an nm... address. A # balance the pool still owes your nm... login is paid to a unified address # (a Sapling one before the pool's Sapling stop) you register with it, signed with your mining # key so nobody else can redirect it. The format is Bitcoin's signmessage with Zecnero's prefix # ("Zecnero Signed Message:\n"): a 65-byte recoverable compact signature # (compressed key), base64, over the message below. The pool checks it # (zecnero-pool src/register.rs). payout_message() { printf 'Zecnero pool payout address\n%s\n%s' "$1" "$2"; } # The signer is the bundled zecnero-wallet: # ZWALLET_WIF= zecnero-wallet --network sign-message # reads the message on stdin and prints {"address":"nm...","signature":"..."}. # The key goes to it in the environment of that one process only, and is # never printed or sent anywhere. signer_available() { [ -x "$BIN/zecnero-wallet" ] && clean_run "$BIN/zecnero-wallet" sign-message --help >/dev/null 2>&1; } json_str() { printf '%s' "$1" | tr -d '\n' | sed -n "s/.*\"$2\":[ ]*\"\([^\"]*\)\".*/\1/p" | head -1; } # zecnero-miner register-payout nutest1... # The pool's API names the field "sapling" whatever kind of address it holds. cmd_register_payout() { local sapling="${1:-}" address wif message out got sig body resp ans sapling="$(printf '%s' "$sapling" | tr -d ' \t\r')" [ -n "$sapling" ] || die "use: zecnero-miner register-payout ${UNIFIED_HRP}1..." \ "It asks the pool to pay what it owes your nm... mining address to that unified address." case "$(login_kind "$sapling")" in unified) ;; sapling) sapling_refused && die "the pool takes no Sapling payout address now, ahead of NU2 (block $ORCHARD_ONLY_HEIGHT): $sapling" \ "Register a unified address (${UNIFIED_HRP}1...). To make one: zecnero-miner pool-login new" ;; *) die "that is not a Zecnero $MINER_NETWORK unified address: $sapling" \ "It starts with ${UNIFIED_HRP}1. To make one: zecnero-miner pool-login new" ;; esac [ -f "$KEYFILE" ] || die "there is no mining key ($KEYFILE), so there is nothing to sign with." address="$(sed -n 's/^Address:[ ]*//p' "$KEYFILE" | head -1)" [ "$(login_kind "$address")" = transparent ] || die "$KEYFILE has no nm... address in it." complete_release || exit 1 signer_available || die "this release's zecnero-wallet cannot sign messages yet." \ "Update first: zecnero-miner update. Or register on $POOL_WEB/register" message="$(payout_message "$address" "$sapling")" { printf '\nThis signs the message below with the private key of your mining address,\n' printf 'on this computer, and sends the pool only the message and the signature.\n' printf 'Your private key never leaves this computer.\n\n' printf 'Mining address: %s\n' "$address" printf 'Pool: %s/api/register\n\n' "$POOL_WEB" printf 'Message (exactly as signed):\n' printf '%s\n' "$message" | sed 's/^/ | /' printf '\nThe pool will pay what it owes %s\nto %s\n\n' "$address" "$sapling" } >&2 ans="${ZECNERO_REGISTER_CONFIRM:-}" if [ -z "$ans" ]; then have_tty || die "this needs a terminal, to confirm. (Or set ZECNERO_REGISTER_CONFIRM=REGISTER.)" printf 'Type REGISTER and press Enter to sign and send it (anything else stops): ' >/dev/tty IFS= read -r ans /dev/null)" || { unset wif; die "zecnero-wallet could not sign the message."; } unset wif got="$(json_str "$out" address)" sig="$(json_str "$out" signature)" [ "$got" = "$address" ] || die "the signature is for ${got:-no address}, not $address. Nothing was sent." [[ "$sig" =~ ^[A-Za-z0-9+/]{86}[AEIMQUYcgkosw048]=$ ]] || die "the signer gave no 65-byte signature. Nothing was sent." body="$(printf '{"address":"%s","sapling":"%s","signature":"%s"}' "$address" "$sapling" "$sig")" if ! resp="$(curl -sS --max-time 20 -H 'content-type: application/json' --data "$body" \ -w '\n%{http_code}' "$POOL_WEB/api/register" 2>&1)"; then die "could not reach $POOL_WEB: $(printf '%s' "$resp" | head -1)" "Nothing was registered. Try again later." fi case "$(printf '%s' "$resp" | tail -1)" in 200) say "Registered: the pool pays what it owes $address to $sapling" say "Check it on $(pool_page "$address")" ;; *) die "the pool refused it: $(json_str "$(printf '%s' "$resp" | sed '$d')" error)" \ "(HTTP $(printf '%s' "$resp" | tail -1)). Nothing was registered." ;; esac } # zecnero-miner migrate: run by update. A pool install still logging in with # its nm... address is told when and why that stops working and, in a # terminal, walked through the switch. cmd_migrate() { complete_release [ -f "$SETTINGS" ] || return 0 [ "$(configured_mode)" = pool ] || return 0 pool_login_ok && return 0 pool_login_notice if [ -n "${ZECNERO_POOL_LOGIN:-}" ]; then set_pool_login "$ZECNERO_POOL_LOGIN" elif have_tty && ask_yn "Set up a unified pool login now?" y; then ask_pool_login elif pool_login_usable; then if [ "$(login_kind "$(pool_login)")" = sapling ]; then say "Your Sapling login keeps working until block $(sapling_stop_height), ahead of NU2. Your settings were not changed." else say "Your nm... login keeps working until block $SHIELDED_ONLY_ACTIVATION_HEIGHT. Your settings were not changed." fi return 0 else say "Pool mining cannot start until you do. Your settings were not changed." return 0 fi write_pool_xmrig_config || die "could not write $POOL_XMRIG_CONF" return 0 } # Writes config/xmrig-pool.json from miner.conf: your pool login with this # computer's worker name, and the same status API and threads as solo mode. # Without a login the pool takes now it writes nothing and fails. write_pool_xmrig_config() { local address port token threads address="$(effective_pool_login)" [ -n "$address" ] || return 1 port="$(setting API_PORT)" token="$(setting API_TOKEN)" threads="$(setting THREADS)" case "$threads" in "" | *[!0-9]* | 0) threads="" ;; esac [ -n "$port" ] || port="$DEFAULT_API_PORT" if [ -z "$token" ]; then token="$(random_token)" set_setting API_TOKEN "$token" fi write_xmrig_config "$port" "$token" "$threads" "$POOL_XMRIG_CONF" \ "$(pool_pools "$address.$(pool_worker)" "$(pool_tls)")" } # Writes xmrig-pool.json when it is missing or miner.conf changed since. ensure_pool_config() { if [ ! -f "$POOL_XMRIG_CONF" ] || [ "$SETTINGS" -nt "$POOL_XMRIG_CONF" ]; then write_pool_xmrig_config fi } cmd_setup() { [ -x "$BIN/zecnerod" ] || die "the Zecnero programs are not installed in $BIN." \ "Install them with: curl -fsSL https://downloads.zecnero.org/install-miner.sh | bash" mkdir -p "$CONF" "$DATA" "$NODE_DIR" "$LOGS" "$RUN" || die "could not create folders in $ZHOME" chmod 700 "$DATA" "$NODE_DIR" 2>/dev/null setup_key local replace=0 if [ -f "$NODE_CONF" ] || [ -f "$BRIDGE_CONF" ] || [ -f "$XMRIG_CONF" ]; then if ask_yn "Your settings files already exist. Replace them with fresh ones?" n; then replace=1 else say "Keeping your settings files." fi fi local port token threads port="$(setting API_PORT)" token="$(setting API_TOKEN)" threads="${ZECNERO_MINER_THREADS-$(setting THREADS)}" case "$threads" in "" | *[!0-9]* | 0) threads="" ;; esac [ -n "$port" ] || port="$DEFAULT_API_PORT" [ -n "$token" ] || token="$(random_token)" if [ "$replace" = 1 ] || [ ! -f "$NODE_CONF" ]; then write_node_config && say "Wrote $NODE_CONF" elif ! grep -q "^miner_address = \"$KEY_ADDRESS\"" "$NODE_CONF"; then toml_set "$NODE_CONF" mining miner_address "\"$KEY_ADDRESS\"" say "Set your mining address in $NODE_CONF" fi if [ "$replace" = 1 ] || [ ! -f "$BRIDGE_CONF" ]; then write_bridge_config && say "Wrote $BRIDGE_CONF" fi if [ "$replace" = 1 ] || [ ! -f "$XMRIG_CONF" ]; then write_xmrig_config "$port" "$token" "$threads" && say "Wrote $XMRIG_CONF" else port="$(sed -n 's/.*"port":[ ]*\([0-9]*\).*/\1/p' "$XMRIG_CONF" | head -1)" token="$(sed -n 's/.*"access-token":[ ]*"\([^"]*\)".*/\1/p' "$XMRIG_CONF" | head -1)" fi write_settings "$port" "$token" "$threads" } # ---------------------------------------------------------------- supervisor log_event() { printf '[zecnero-miner %s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$2" >>"$LOGS/$1.log"; } set_state() { printf '%s\n' "$2" >"$RUN/$1.state"; } # Keeps each log under LOG_MAX_BYTES, with one older copy. The writer appends, # so truncating in place is safe. rotate_logs() { local f size for f in "$LOGS"/node.log "$LOGS"/bridge.log "$LOGS"/miner.log "$LOGS"/supervisor.log; do [ -f "$f" ] || continue size="$(stat -c %s "$f" 2>/dev/null || echo 0)" if [ "$size" -gt "$LOG_MAX_BYTES" ]; then cp "$f" "$f.1" && : >"$f" fi done } strip_ansi() { exec sed -u 's/\x1b\[[0-9;]*[A-Za-z]//g'; } launch() { local c="$1" pid shift LOG_OFFSET[$c]="$(stat -c %s "$LOGS/$c.log" 2>/dev/null || echo 0)" ( cd "$CONF" || exit 1 exec "$@" >(strip_ansi >>"$LOGS/$c.log") 2>&1 ) & pid=$! printf '%s\n' "$pid" >"$RUN/$c.pid" STARTED_AT[$c]="$(date +%s)" set_state "$c" "running" log_event "$c" "started $(component_bin "$c") (pid $pid)" } start_component() { case "$1" in node) launch node "$BIN/zecnerod" -c "$NODE_CONF" start ;; bridge) launch bridge "$BIN/zecnero-bridge" -c "$BRIDGE_CONF" ;; miner) launch miner "$BIN/xmrig-zecnero" -c "${MINER_CONF:-$XMRIG_CONF}" --no-color ;; esac } stop_component() { local c="$1" grace="$2" pid i pid="$(pid_of "$c")" || return 0 if component_alive "$c"; then kill -TERM "$pid" 2>/dev/null for ((i = 0; i < grace * 2; i++)); do component_alive "$c" || break sleep 0.5 done if component_alive "$c"; then kill -KILL "$pid" 2>/dev/null log_event "$c" "did not stop in ${grace} s; killed" fi wait "$pid" 2>/dev/null log_event "$c" "stopped" fi rm -f "$RUN/$c.pid" set_state "$c" "stopped" } # Called every loop: notices a component that exited and restarts it with a # growing delay (2, 4, 8 ... 60 s), reset after five minutes of good running. watch_component() { local c="$1" pid now st rc delay now="$(date +%s)" st="$(state_of "$c")" case "$st" in running) component_alive "$c" && return 0 pid="$(pid_of "$c")" wait "$pid" 2>/dev/null rc=$? if [ "$c" = node ] && node_said_legacy_chain; then log_event node "zecnerod refuses its chain data, which holds a forked chain; not restarting it. Run: zecnero-miner repair-chain" rm -f "$RUN/node.pid" set_state node forked return 0 fi if [ $((now - ${STARTED_AT[$c]:-0})) -gt 300 ]; then FAILS[$c]=0; fi FAILS[$c]=$((${FAILS[$c]:-0} + 1)) delay=$((2 ** FAILS[$c])) [ "$delay" -gt 60 ] && delay=60 log_event "$c" "$(component_bin "$c") stopped unexpectedly (exit $rc); restarting in $delay s" rm -f "$RUN/$c.pid" set_state "$c" "restarting $((now + delay))" ;; restarting*) [ "$now" -ge "${st#restarting }" ] && start_component "$c" ;; esac } # Runs the bridge's template check: true once the node is synced and hands out # a well-formed template. The one-line result goes to run/check. template_ready() { local out rc out="$(cd "$CONF" && NO_COLOR=1 timeout 120 "$BIN/zecnero-bridge" -c "$BRIDGE_CONF" --check-template 2>/dev/null | tail -1)" rc=$? printf '%s %s\n' "$rc" "$out" | sed 's/\x1b\[[0-9;]*[A-Za-z]//g' >"$RUN/check" [ "$rc" = 0 ] } # A node that mined or followed blocks under the old rules after a network # upgrade (a v0.1.x node past the shielded-only upgrade) has that forked # chain on disk. A newer zecnerod then refuses to start: zebra-state's legacy # chain check panics with "Cached state contains a legacy chain. ... Hint: # Delete your database". Restarting cannot help (and on WSL each crash leaves # a dump of hundreds of MB), so the supervisor stops restarting it, sets the # node's state to forked, and status and start explain the way out. LEGACY_CHAIN_TEXT="Cached state contains a legacy chain" # True when what the node wrote to node.log since it was started (from byte # LOG_OFFSET[node] on; all of it after a rotation) holds that message. Its # last lines pass through strip_ansi after it has exited, so this allows # them up to 3 s to arrive. node_said_legacy_chain() { local i from="${LOG_OFFSET[node]:-0}" f="$LOGS/node.log" for ((i = 0; i < 6; i++)); do [ "$(stat -c %s "$f" 2>/dev/null || echo 0)" -ge "$from" ] || from=0 tail -c +"$((from + 1))" "$f" 2>/dev/null | grep -v '^\[zecnero-miner ' | grep -qF "$LEGACY_CHAIN_TEXT" && return 0 sleep 0.5 done return 1 } # The node's chain data, the two folders zebra-state keeps under the # [state] cache_dir that setup writes (NODE_DIR): the database, and its copy # of the last blocks. Nothing else lives there but the RPC cookie. chain_data_dirs() { printf '%s\n' "$NODE_DIR/state" "$NODE_DIR/non_finalized_state"; } # ~ for the home folder, as the runbook writes the paths. tilde() { case "$1" in "$HOME"/*) printf '~/%s' "${1#"$HOME"/}" ;; *) printf '%s' "$1" ;; esac; } forked_chain_notice() { local wsl=() if [ -n "${WSL_DISTRO_NAME:-}" ] || grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null; then wsl=("" "Windows: WSL may have saved a crash dump of 200-500 MB in" "%TEMP%\\wsl-crashes. You can delete those files.") fi zv_box "THE NODE STOPPED: ITS CHAIN DATA HOLDS A FORKED CHAIN" \ "zecnerod says: $LEGACY_CHAIN_TEXT." \ "An older release of this node mined or followed blocks under the old" \ "rules after the network upgrade (block $SHIELDED_ONLY_ACTIVATION_HEIGHT), on a chain nobody else" \ "follows. This zecnerod refuses to start on it, so it is not restarted." \ "Blocks mined there after the upgrade do not exist on the Testnet, and" \ "neither do their rewards. Coins mined before it are unaffected." \ "" \ "To fix it, delete the chain data (only the chain: your mining key," \ "settings and wallets are elsewhere and stay). The node then syncs the" \ "Testnet again from the seeds, which takes minutes:" \ " zecnero-miner repair-chain" \ "or by hand:" \ " zecnero-miner stop" \ " rm -rf $(tilde "$NODE_DIR/state") $(tilde "$NODE_DIR/non_finalized_state")" \ " zecnero-miner start" \ "${wsl[@]}" } # The last thing a crashed node said, when it is waiting to be restarted. node_failure() { case "$(state_of node)" in restarting*) ;; *) return 1 ;; esac grep -v '^\[zecnero-miner' "$LOGS/node.log" 2>/dev/null | grep -v '^[[:space:]]*$' | tail -1 | cut -c1-200 } # The template check's message when it failed for a reason other than # syncing (exit 1: a settings problem, not something waiting fixes). check_problem() { local line line="$(cat "$RUN/check" 2>/dev/null)" case "$line" in "1 "?*) printf '%s' "${line#1 }" ;; *) return 1 ;; esac } cmd_supervise() { [ -f "$SETTINGS" ] || die "not set up yet." mkdir -p "$RUN" "$LOGS" if supervisor_pid >/dev/null; then die "already running."; fi if [ "$(configured_mode)" = pool ]; then supervise_pool return fi printf '%s\n' "$$" >"$RUN/supervisor.pid" printf 'solo\n' >"$RUN/mode" local peerless_first="${ZECNERO_PEERLESS_SECONDS:-180}" drop_zecnero_env export NO_COLOR=1 declare -gA STARTED_AT=() FAILS=() LOG_OFFSET=() STOPPING=0 trap 'STOPPING=1' TERM INT trap '' HUP rm -f "$RUN/check" set_state bridge "waiting" set_state miner "waiting" printf 'waiting\n' >"$RUN/phase" log_event supervisor "supervisor started (pid $$)" start_component node local phase=waiting last_check=0 now i last_peer last_peer_check=0 peerless_limit="$peerless_first" last_peer="$(date +%s)" while [ "$STOPPING" = 0 ]; do watch_component node now="$(date +%s)" # A node whose first connections to the seeds fail does not try them # again, and a seed refuses an address it saw in the last two minutes. # So a node with no peers for a while is restarted, a little later each # time (3, 6, 12, then every 20 minutes), which makes it dial them again. if [ $((now - last_peer_check)) -ge 30 ] && [ "$(state_of node)" = running ]; then last_peer_check="$now" if node_has_peers; then last_peer="$now" peerless_limit="$peerless_first" elif [ $((now - last_peer)) -ge "$peerless_limit" ] && [ -r "$COOKIE" ]; then log_event node "no peers for $(((now - last_peer) / 60)) minutes; restarting the node so it tries the seeds again" stop_component node 60 start_component node last_peer="$(date +%s)" peerless_limit=$((peerless_limit * 2)) [ "$peerless_limit" -gt 1200 ] && peerless_limit=1200 fi fi if [ "$phase" = waiting ]; then if component_alive node && [ $((now - last_check)) -ge 10 ] && [ -r "$COOKIE" ]; then last_check="$now" if template_ready && node_has_peers; then phase=mining printf 'mining\n' >"$RUN/phase" log_event supervisor "node synced and template check passed; starting the bridge and the miner" start_component bridge for ((i = 0; i < 30; i++)); do port_open "$STRATUM_PORT" && break sleep 1 done start_component miner fi fi else watch_component bridge watch_component miner fi rotate_logs sleep 2 & wait $! 2>/dev/null done log_event supervisor "stopping" stop_component miner 15 stop_component bridge 10 stop_component node 60 printf 'stopped\n' >"$RUN/phase" rm -f "$RUN/supervisor.pid" "$RUN/check" log_event supervisor "supervisor stopped" exit 0 } # Pool mode: only the miner runs, against the pool. The node and the bridge # stay installed for solo mode, and stay stopped. supervise_pool() { ensure_pool_config || die "could not write $POOL_XMRIG_CONF" printf '%s\n' "$$" >"$RUN/supervisor.pid" printf 'pool\n' >"$RUN/mode" drop_zecnero_env export NO_COLOR=1 declare -gA STARTED_AT=() FAILS=() LOG_OFFSET=() MINER_CONF="$POOL_XMRIG_CONF" STOPPING=0 trap 'STOPPING=1' TERM INT trap '' HUP rm -f "$RUN/check" "$RUN/fallback" set_state node stopped set_state bridge stopped printf 'mining\n' >"$RUN/phase" log_event supervisor "supervisor started in pool mode (pid $$)" start_component miner local now last_look=0 while [ "$STOPPING" = 0 ]; do watch_component miner now="$(date +%s)" if [ $((now - last_look)) -ge 20 ]; then last_look="$now" note_fallback fi rotate_logs sleep 2 & wait $! 2>/dev/null done log_event supervisor "stopping" stop_component miner 15 printf 'stopped\n' >"$RUN/phase" rm -f "$RUN/supervisor.pid" "$RUN/fallback" log_event supervisor "supervisor stopped" exit 0 } # Writes a note in the miner's log when XMRig gave up on TLS and mines over # plain 3333, and another when it is back on TLS. run/fallback marks it. note_fallback() { [ "$(pool_tls)" = yes ] || return 0 component_alive miner || return 0 xmrig_read "$(api_summary)" [ "${X_UPTIME:-0}" -gt 0 ] 2>/dev/null || return 0 case "$X_POOL" in *":$POOL_PLAIN_PORT") if [ ! -f "$RUN/fallback" ]; then : >"$RUN/fallback" log_event miner "Note: TLS to $POOL_HOST:$POOL_TLS_PORT kept failing, so the miner now uses plain $POOL_HOST:$POOL_PLAIN_PORT. It goes back to TLS by itself when that works. If mining stops after a few shares, a router or antivirus program is cutting plain mining traffic." fi ;; *":$POOL_TLS_PORT") if [ -f "$RUN/fallback" ]; then rm -f "$RUN/fallback" log_event miner "Back on TLS: $POOL_HOST:$POOL_TLS_PORT" fi ;; esac } # ---------------------------------------------------------------- commands require_setup() { local mode="${1:-$(configured_mode)}" [ -x "$BIN/xmrig-zecnero" ] || die "Zecnero is not installed." \ "Install it with: curl -fsSL https://downloads.zecnero.org/install-miner.sh | bash" [ -f "$SETTINGS" ] || die "Zecnero is not set up yet. Run: zecnero-miner setup" if [ "$mode" = pool ]; then [ -n "$(miner_address)" ] || die "there is no mining address yet. Run: zecnero-miner setup" return 0 fi [ -x "$BIN/zecnerod" ] || die "Zecnero is not installed." \ "Install it with: curl -fsSL https://downloads.zecnero.org/install-miner.sh | bash" [ -f "$NODE_CONF" ] && [ -f "$BRIDGE_CONF" ] && [ -f "$XMRIG_CONF" ] || die "Zecnero is not set up yet. Run: zecnero-miner setup" } stop_leftovers() { local c found=0 for c in miner bridge node; do if component_alive "$c"; then found=1 kill -TERM "$(pid_of "$c")" 2>/dev/null fi done if [ "$found" = 1 ]; then say "Stopping programs left over from an earlier run..." for c in miner bridge node; do local i for ((i = 0; i < 120; i++)); do component_alive "$c" || break sleep 0.5 done component_alive "$c" && kill -KILL "$(pid_of "$c")" 2>/dev/null done fi for c in miner bridge node; do rm -f "$RUN/$c.pid"; done } check_ports_free() { local mode="$1" p what api ports api="$(setting API_PORT)" if [ "$mode" = pool ]; then ports="${api:-$DEFAULT_API_PORT}" else ports="$RPC_PORT $P2P_PORT $STRATUM_PORT ${api:-}" fi for p in $ports; do if port_open "$p"; then case "$p" in "$RPC_PORT" | "$P2P_PORT") what="Another Zecnero node (zecnerod) seems to be running already" ;; "$STRATUM_PORT") what="Another mining bridge or pool proxy seems to be running already" ;; *) what="Another program is using the miner's status port" ;; esac die "$what: port $p on this computer is in use." \ "Stop that program first (press Ctrl+C in its window, or close it), then run: zecnero-miner start" fi done } cmd_start() { local mode running mode="$(configured_mode)" require_setup "$mode" # Right after an update by testnet-v0.1.2's zecnero-miner, this is the first # command of the new release to run. Mining goes on without what is missing. complete_release running="" if supervisor_pid >/dev/null; then running="$(active_mode)" if [ "$running" = "$mode" ]; then say "Zecnero is already running ($mode mining)." printf '\n' cmd_status return 0 fi fi # The pool login is checked before anything running is stopped, so a refused # switch leaves the miner mining as it was. if [ "$mode" = pool ]; then if ! pool_login_usable; then if have_tty && [ -z "${ZECNERO_POOL_LOGIN:-}" ]; then pool_login_notice ask_yn "Set up a unified pool login now?" y || die "not starting: pool mode needs a unified login." ask_pool_login else ensure_pool_login fi write_pool_xmrig_config || die "could not write $POOL_XMRIG_CONF" elif ! pool_login_ok; then # Before the upgrade that ends it: warn, and mine with the old login. pool_login_notice write_pool_xmrig_config || die "could not write $POOL_XMRIG_CONF" fi ensure_pool_config || die "could not write $POOL_XMRIG_CONF" fi if [ -n "$running" ]; then say "Switching from $running mining to $mode mining." cmd_stop fi remove_update_leftovers if unit_enabled; then systemctl --user start "$UNIT_NAME" || die "systemd could not start $UNIT_NAME" else stop_leftovers check_ports_free "$mode" mkdir -p "$RUN" "$LOGS" setsid -f "$BIN/zecnero-miner" supervise >"$LOGS/supervisor.log" 2>&1 || die "could not start the background process." fi local i for ((i = 0; i < 20; i++)); do supervisor_pid >/dev/null && break sleep 0.5 done supervisor_pid >/dev/null || die "the background process did not start." "See $LOGS/supervisor.log" if [ "$mode" = pool ]; then say "Zecnero is starting in the background, mining on the pool." else say "Zecnero is starting in the background." fi if [ "${ZECNERO_NO_WAIT:-}" = 1 ]; then mode_hint return 0 fi if [ "$mode" = pool ]; then watch_pool_until_mining else watch_until_mining fi } # Shows sync progress until the miner has a hashrate. Ctrl+C only stops the # watching, never the mining. watch_until_mining() { local deadline shown="" shown_problem="" problem line tty=0 summary rate deadline=$(($(date +%s) + ${ZECNERO_WATCH_SECONDS:-1800})) [ -t 1 ] && tty=1 trap 'printf "\n"; say "Stopped watching. Mining continues in the background. Check it with: zecnero-miner status"; exit 0' INT say "Waiting for the node to sync with the network. You can press Ctrl+C: it keeps going in the background." while [ "$(date +%s)" -lt "$deadline" ]; do supervisor_pid >/dev/null || { printf '\n' die "the background process stopped." "See: zecnero-miner logs" } if problem="$(check_problem)" && [ "$problem" != "$shown_problem" ]; then [ "$tty" = 1 ] && printf '\r\033[K' say "The bridge's check says: $problem" shown_problem="$problem" fi if [ "$(state_of node)" = forked ]; then [ "$tty" = 1 ] && printf '\r\033[K' trap - INT forked_chain_notice return 1 fi if problem="$(node_failure)" && [ -n "$problem" ] && [ "$problem" != "$shown_problem" ]; then [ "$tty" = 1 ] && printf '\r\033[K' say "The node stopped and will be restarted. It said: $problem" shown_problem="$problem" fi if [ "$(cat "$RUN/phase" 2>/dev/null)" = mining ]; then summary="$(api_summary)" || summary="" rate="$(printf '%s' "$summary" | tr -d ' \n' | sed -n 's/.*"hashrate":{"total":\[\([0-9.]*\).*/\1/p')" if [ -n "$rate" ] && [ "${rate%.*}" -gt 0 ] 2>/dev/null; then [ "$tty" = 1 ] && printf '\r\033[K' say "Node OK, template OK, mining." break fi line="Node synced. Miner starting (preparing RandomX, about a minute)..." elif node_query 2>/dev/null; then if [ "${NODE_PEERS:-0}" = 0 ]; then line="Node started, looking for peers..." elif [ "$NODE_BLOCKS" -ge $((NODE_TARGET - 2)) ]; then line="Node synced at height $NODE_BLOCKS, getting ready to hand out work (about 2 minutes)..." else line="Waiting for node to sync: $(sync_text)" fi else line="Node starting..." fi if [ "$tty" = 1 ]; then printf '\r\033[K%s' "$line" elif [ "$line" != "$shown" ]; then say "$line" fi shown="$line" sleep 3 done trap - INT printf '\n' cmd_status } # Pool mode: shows the connection and the miner until the pool has accepted # a share (or a minute after the miner has a hashrate). Ctrl+C only stops the # watching, never the mining. watch_pool_until_mining() { local deadline shown="" line tty=0 hashing_since=0 now deadline=$(($(date +%s) + ${ZECNERO_WATCH_SECONDS:-600})) [ -t 1 ] && tty=1 trap 'printf "\n"; say "Stopped watching. Mining continues in the background. Check it with: zecnero-miner status"; exit 0' INT say "Connecting to the pool and preparing the miner (about a minute). You can press Ctrl+C: it keeps going in the background." while [ "$(date +%s)" -lt "$deadline" ]; do supervisor_pid >/dev/null || { printf '\n' die "the background process stopped." "See: zecnero-miner logs miner" } now="$(date +%s)" if component_alive miner; then xmrig_read "$(api_summary)" else xmrig_read "" fi if [ -z "$X_FLAT" ]; then line="Miner starting..." elif ! [ "${X_UPTIME:-0}" -gt 0 ] 2>/dev/null; then if [ "${X_FAILURES:-0}" -gt 0 ] 2>/dev/null; then line="Still trying to reach $(pool_target). Check the internet connection..." else line="Connecting to $(pool_target)..." fi elif [ -z "$X_RATE" ] || [ "${X_RATE%.*}" = 0 ]; then line="Connected to the pool. Preparing RandomX (about a minute)..." else [ "$hashing_since" = 0 ] && hashing_since="$now" if [ "${X_ACCEPTED:-0}" -gt 0 ] || [ $((now - hashing_since)) -ge 60 ]; then [ "$tty" = 1 ] && printf '\r\033[K' if [ "${X_ACCEPTED:-0}" -gt 0 ]; then say "Connected to the pool, mining, and the pool accepts your shares." else say "Connected to the pool and mining. The first accepted share can take a few minutes on a slow computer." fi break fi line="Connected to the pool, mining at $(fmt_rate "$X_RATE"). Waiting for the first accepted share..." fi if [ "$tty" = 1 ]; then printf '\r\033[K%s' "$line" elif [ "$line" != "$shown" ]; then say "$line" fi shown="$line" sleep 2 done trap - INT printf '\n' cmd_status } # zecnero-miner repair-chain: after a typed DELETE, stops mining, deletes the # node's chain data (state and non_finalized_state in NODE_DIR, nothing # else) and starts again (ZECNERO_NO_START=1: does not start). The node then # syncs the Testnet again from the seeds. ZECNERO_REPAIR_CONFIRM=DELETE # answers without a terminal. cmd_repair_chain() { local d found=() ans [ -f "$SETTINGS" ] || die "Zecnero is not set up yet. Run: zecnero-miner setup" while IFS= read -r d; do [ -e "$d" ] && found+=("$d") done < <(chain_data_dirs) if [ "${#found[@]}" = 0 ]; then say "There is no chain data to delete in $(tilde "$NODE_DIR")." return 0 fi { printf '\nThis deletes the node'"'"'s chain data:\n' for d in "${found[@]}"; do printf ' %s (%s)\n' "$(tilde "$d")" "$(du -sh "$d" 2>/dev/null | cut -f1)"; done printf '\nIt stops mining first, and starts again afterwards. The node then syncs\n' printf 'the Testnet again from the seeds, which takes minutes. Blocks it mined on\n' printf 'a forked chain are lost with it (they do not exist on the Testnet).\n' printf 'Your mining key, settings and wallets are elsewhere, and are not touched.\n\n' } >&2 ans="${ZECNERO_REPAIR_CONFIRM:-}" if [ -z "$ans" ]; then have_tty || die "this needs a terminal, to confirm. (Or set ZECNERO_REPAIR_CONFIRM=DELETE.)" printf 'Type DELETE and press Enter to delete it (anything else stops): ' >/dev/tty IFS= read -r ans /dev/null || component_alive node; then cmd_stop; fi component_alive node && die "the node is still running, so nothing was deleted. Try again in a minute." rm -rf "${found[@]}" || die "could not delete all of it: $(printf '%s ' "${found[@]}")" [ -d "$LOGS" ] && log_event node "chain data deleted by zecnero-miner repair-chain; the node syncs again" [ -d "$RUN" ] && set_state node stopped say "Deleted the node's chain data. Your mining key, settings and wallets were not touched." if [ "${ZECNERO_NO_START:-}" = 1 ]; then say "Start mining again with: zecnero-miner start" return 0 fi [ "$(configured_mode)" = pool ] && say "(Pool mode does not run the node; it syncs again when you mine solo.)" cmd_start } cmd_stop() { local pid i mode mode="$(active_mode)" if unit_active; then systemctl --user stop "$UNIT_NAME" fi pid="$(supervisor_pid)" if [ -n "$pid" ]; then if [ "$mode" = pool ]; then printf 'Stopping the miner' else printf 'Stopping Zecnero (this can take up to a minute while the node saves its state)' fi kill -TERM "$pid" 2>/dev/null for ((i = 0; i < 180; i++)); do pid_alive "$pid" "zecnero-miner supervise" || break [ $((i % 4)) = 0 ] && printf '.' sleep 0.5 done printf '\n' if pid_alive "$pid" "zecnero-miner supervise"; then kill -KILL "$pid" 2>/dev/null rm -f "$RUN/supervisor.pid" fi fi stop_leftovers rm -f "$RUN/supervisor.pid" [ -d "$RUN" ] && printf 'stopped\n' >"$RUN/phase" say "Stopped. Nothing is mining now. Run zecnero-miner start to mine again." } # Reads XMRig's /2/summary (given as text) into X_FLAT (all of it, on one # line), X_POOL (host:port), X_UPTIME (seconds connected, 0 when not # connected), X_FAILURES, X_TLS (the TLS version, empty for plain), X_RATE # (the 60 s average once it exists, else the 10 s one; empty while it is # still measuring) and X_ACCEPTED. xmrig_read() { local flat conn flat="$(printf '%s' "$1" | tr -d ' \n\r')" X_FLAT="$flat" conn="$(printf '%s' "$flat" | sed -n 's/.*"connection":{\([^}]*\)}.*/\1/p')" X_POOL="$(printf '%s' "$conn" | sed -n 's/.*"pool":"\([^"]*\)".*/\1/p')" X_UPTIME="$(printf '%s' "$conn" | sed -n 's/.*"uptime":\([0-9]*\).*/\1/p')" X_FAILURES="$(printf '%s' "$conn" | sed -n 's/.*"failures":\([0-9]*\).*/\1/p')" X_TLS="$(printf '%s' "$conn" | sed -n 's/.*"tls":"\([^"]*\)".*/\1/p')" X_RATE="$(printf '%s' "$flat" | sed -n 's/.*"hashrate":{"total":\[[^],]*,\([0-9.]*\)[],].*/\1/p')" [ -n "$X_RATE" ] || X_RATE="$(printf '%s' "$flat" | sed -n 's/.*"hashrate":{"total":\[\([0-9.]*\).*/\1/p')" X_ACCEPTED="$(printf '%s' "$flat" | sed -n 's/.*"shares_good":\([0-9]*\).*/\1/p')" } # Prints what the pool's API says about ADDRESS, fetched at most once a # minute (run/pool-api.json holds the answer, run/pool-api.failed a failed # try). Fails when the pool's API cannot be reached. pool_api_miner() { local address="$1" cache="$RUN/pool-api.json" failed="$RUN/pool-api.failed" now age tmp mkdir -p "$RUN" 2>/dev/null now="$(date +%s)" if [ -s "$cache" ] && [ "$(head -c 200 "$cache" | tr -d ' \n' | sed -n 's/.*"address":"\([^"]*\)".*/\1/p')" = "$address" ]; then age=$((now - $(stat -c %Y "$cache" 2>/dev/null || echo 0))) if [ "$age" -lt "$POOL_API_CACHE_SECONDS" ]; then cat "$cache" return 0 fi fi if [ -f "$failed" ]; then age=$((now - $(stat -c %Y "$failed" 2>/dev/null || echo 0))) [ "$age" -lt "$POOL_API_CACHE_SECONDS" ] && return 1 fi tmp="$(mktemp "$RUN/.pool-api.XXXXXX")" || return 1 if curl -fsS --max-time 6 --proto '=https' -o "$tmp" "$POOL_WEB/api/miner/$address" 2>/dev/null && grep -q '"balance"' "$tmp"; then mv "$tmp" "$cache" rm -f "$failed" cat "$cache" return 0 fi rm -f "$tmp" : >"$failed" return 1 } # "0.42" from an amount in zatoshis (1 ZMR = 100,000,000). fmt_zmr() { awk -v z="$1" 'BEGIN { printf "%.2f", z / 100000000 }'; } # The Balance line: pending is everything earned and not received yet (your # share of blocks still maturing, the credited balance, and a payout being # prepared); paid is what was sent to you, confirmed or confirming. pool_balance_text() { local address="$1" json flat balance immature paid in_flight confirming pending sent json="$(pool_api_miner "$address")" || { printf 'pool page unavailable' return } flat="$(printf '%s' "$json" | tr -d ' \n')" balance="$(json_num "$flat" balance)" immature="$(json_num "$flat" immature)" paid="$(json_num "$flat" paid)" in_flight="$(json_num "$flat" in_flight)" confirming="$(json_num "$flat" confirming)" [ -n "$balance" ] || { printf 'pool page unavailable' return } pending=$((${immature:-0} + balance + ${in_flight:-0} - ${confirming:-0})) sent=$((${paid:-0} + ${confirming:-0})) printf '%s ZMR pending, %s ZMR paid' "$(fmt_zmr "$pending")" "$(fmt_zmr "$sent")" } cmd_status() { local missing if [ -f "$SETTINGS" ] && [ "$(active_mode)" = pool ]; then status_pool else status_solo fi mode_hint # Only says so: status never downloads. missing="$(release_missing)" if [ -n "$missing" ]; then printf '\nThis install is missing %s. Add it with: zecnero-miner update\n' "$(archive_names "$missing")" fi } status_pool() { local address pool miner st sup=0 address="$(effective_pool_login)" [ -n "$address" ] || address="$(pool_login)" supervisor_pid >/dev/null && sup=1 if [ "$sup" = 0 ] && ! component_alive miner; then pool="not connected (mining is stopped)" miner="stopped (start it with: zecnero-miner start)" else st="$(state_of miner)" xmrig_read "" component_alive miner && xmrig_read "$(api_summary)" case "$st" in restarting*) pool="not connected (the miner is restarting)" miner="$(restart_text "$st")" ;; *) if ! component_alive miner; then pool="not connected" miner="$(gone_text miner)" elif [ -z "$X_FLAT" ]; then pool="connecting to $(pool_target)" miner="starting" else if [ "${X_UPTIME:-0}" -gt 0 ] 2>/dev/null; then case "$X_POOL" in *":$POOL_PLAIN_PORT") if [ "$(pool_tls)" = yes ]; then pool="connected to $X_POOL (plain, because TLS on $POOL_TLS_PORT failed)" else pool="connected to $X_POOL (plain)" fi ;; *) if [ -n "$X_TLS" ]; then pool="connected to $X_POOL (TLS)"; else pool="connected to $X_POOL"; fi ;; esac elif [ "${X_FAILURES:-0}" -gt 0 ] 2>/dev/null; then pool="connection lost, reconnecting to $(pool_target) (check the internet connection)" else pool="connecting to $(pool_target)" fi if ! [ "${X_UPTIME:-0}" -gt 0 ] 2>/dev/null && [ "${X_ACCEPTED:-0}" -gt 0 ] 2>/dev/null; then miner="paused until the pool is back (${X_ACCEPTED} accepted so far)" elif ! [ "${X_UPTIME:-0}" -gt 0 ] 2>/dev/null; then miner="waiting for the pool" elif [ -z "$X_RATE" ] || [ "${X_RATE%.*}" = 0 ]; then miner="starting (preparing RandomX, about a minute)" else miner="running, $(fmt_rate "$X_RATE"), ${X_ACCEPTED:-0} accepted" fi fi ;; esac fi printf 'Zecnero Testnet Miner (pool)\n' if [ -n "$address" ]; then printf 'Login: %s (%s)\n' "$address" "$(login_type_text "$address")" else printf 'Login: none yet (set one: zecnero-miner pool-login new)\n' fi printf 'Pool: %s\n' "$pool" printf 'Miner: %s\n' "$miner" if [ -n "$address" ] && [ "$address" = "$(effective_pool_login)" ]; then printf 'Balance: %s\n' "$(pool_balance_text "$address")" printf 'Page: %s\n' "$(pool_page "$address")" fi } status_solo() { local address node bridge miner phase summary rate accepted sup=0 st problem address="$(setting ADDRESS)" supervisor_pid >/dev/null && sup=1 phase="$(cat "$RUN/phase" 2>/dev/null)" if [ ! -f "$SETTINGS" ]; then node="not set up yet (run: zecnero-miner setup)" bridge="not set up yet" miner="not set up yet" elif [ "$sup" = 0 ] && ! component_alive node; then node="stopped (start it with: zecnero-miner start)" [ "$(state_of node)" = forked ] && node="stopped: its chain data holds a forked chain (see below)" bridge="stopped" miner="stopped" else st="$(state_of node)" case "$st" in restarting*) node="$(restart_text "$st")" ;; forked) node="stopped: its chain data holds a forked chain, so it is not restarted (see below)" ;; *) if ! component_alive node; then node="$(gone_text node)" elif ! node_query; then node="starting" elif [ "${NODE_PEERS:-0}" = 0 ]; then # A node with no peers cannot know the tip, whatever else it says. node="no peers right now, looking for some (height $NODE_BLOCKS)" elif [ "$NODE_READY" = 1 ]; then node="synced (height $NODE_BLOCKS / $NODE_BLOCKS)" elif [ "$NODE_BLOCKS" -ge $((NODE_TARGET - 2)) ]; then node="almost synced, getting ready ($(sync_text))" else node="syncing, $(sync_text)" fi ;; esac summary="" component_alive miner && summary="$(api_summary)" local flat flat="$(printf '%s' "$summary" | tr -d ' \n')" st="$(state_of bridge)" case "$st" in restarting*) bridge="$(restart_text "$st")" ;; waiting) if problem="$(check_problem)"; then bridge="not started: $problem" else bridge="waiting for the node to sync" fi ;; *) if ! component_alive bridge; then bridge="$(gone_text bridge)" elif printf '%s' "$flat" | grep -q "\"connection\":{\"pool\":\"127.0.0.1:$STRATUM_PORT\"" && ! printf '%s' "$flat" | grep -q '"connection":{[^}]*"uptime":0,'; then bridge="connected" else bridge="running, waiting for the miner" fi ;; esac st="$(state_of miner)" case "$st" in restarting*) miner="$(restart_text "$st")" ;; waiting) miner="waiting for the node to sync" ;; *) if ! component_alive miner; then miner="$(gone_text miner)" elif [ -z "$flat" ]; then miner="starting" else # The 60 s average once it exists, else the 10 s one; null means # still measuring. rate="$(printf '%s' "$flat" | sed -n 's/.*"hashrate":{"total":\[[^],]*,\([0-9.]*\)[],].*/\1/p')" [ -n "$rate" ] || rate="$(printf '%s' "$flat" | sed -n 's/.*"hashrate":{"total":\[\([0-9.]*\).*/\1/p')" accepted="$(printf '%s' "$flat" | sed -n 's/.*"shares_good":\([0-9]*\).*/\1/p')" if [ -z "$rate" ] || [ "${rate%.*}" = 0 ]; then miner="starting (preparing RandomX, about a minute)" else miner="running, $(fmt_rate "$rate"), ${accepted:-0} accepted" fi fi ;; esac fi printf 'Zecnero Testnet Miner (solo)\n' if [ -n "$address" ]; then printf 'Wallet: %s (transparent mining key, solo)\n' "$address" else printf 'Wallet: none yet\n' fi printf 'Node: %s\n' "$node" printf 'Bridge: %s\n' "$bridge" printf 'Miner: %s\n' "$miner" if [ -f "$SETTINGS" ] && [ "$(state_of node)" = forked ]; then printf '\n' forked_chain_notice fi } # One line for an install set up before pool mode existed (no MODE in # miner.conf yet). It goes away once a mode is chosen either way. mode_hint() { [ -f "$SETTINGS" ] || return 0 if [ "$(setting MODE)" = pool ] && ! pool_login_ok; then printf '\n' pool_login_notice 2>&1 return 0 fi [ -n "$(setting MODE)" ] && return 0 printf '\nNew: mine on the pool, with no node to sync. Switch with: zecnero-miner mode pool\n' } cmd_logs() { local which="${1:-}" files=() if [ -z "$which" ]; then if [ -f "$SETTINGS" ] && [ "$(active_mode)" = pool ]; then which=miner; else which=all; fi fi case "$which" in node | bridge | miner) files=("$LOGS/$which.log") ;; all) files=("$LOGS/node.log" "$LOGS/bridge.log" "$LOGS/miner.log") ;; *) die "unknown log '$which'. Use: zecnero-miner logs [node|bridge|miner]" ;; esac if [ "$which" != miner ] && [ "$which" != all ] && [ "$(active_mode)" = pool ]; then say "Note: the $which does not run in pool mode. These are its lines from earlier solo mining." >&2 fi mkdir -p "$LOGS" 2>/dev/null local f for f in "${files[@]}"; do [ -f "$f" ] || : >>"$f" 2>/dev/null; done if [ -t 1 ]; then say "Showing ${which} logs. Press Ctrl+C to stop watching (mining keeps going)." exec tail -n 40 -F "${files[@]}" fi tail -n "${ZECNERO_LOG_LINES:-40}" "${files[@]}" } cmd_address() { local a a="$(miner_address)" [ -n "$a" ] || die "no mining address yet. Run: zecnero-miner setup" say "$a" } cmd_pool() { local a a="$(effective_pool_login)" if [ -z "$a" ]; then pool_login_notice die "no pool login the pool takes yet, so there is no pool page." fi say "$(pool_page "$a")" } # zecnero-miner mode prints the mode # zecnero-miner mode pool|solo switches, restarting whatever runs, and starts # mining (ZECNERO_NO_START=1: only saves it) cmd_mode() { local want="${1:-}" current address current="$(configured_mode)" case "$want" in "") if [ "$current" = pool ]; then say "pool: mining on $POOL_WEB with xmrig-zecnero only, no node." say "To mine with your own node instead: zecnero-miner mode solo" else say "solo: mining with your own node, bridge and miner on this computer." say "To mine on the pool instead: zecnero-miner mode pool" fi return 0 ;; pool | solo) ;; *) die "use: zecnero-miner mode pool, or zecnero-miner mode solo" ;; esac require_setup "$want" # The pool login is checked before MODE is saved: a refused switch leaves # miner.conf as it was. if [ "$want" = pool ]; then complete_release ensure_pool_login fi # MODE, then WORKER, both before a POOL_LOGIN saved just now: the order setup # writes them in. [ "$(setting MODE)" = "$want" ] || set_setting MODE "$want" POOL_LOGIN if [ "$want" = pool ]; then if [ -n "$(sanitize_worker "${ZECNERO_WORKER:-}")" ]; then [ "$(setting WORKER)" = "$(sanitize_worker "$ZECNERO_WORKER")" ] || set_setting WORKER "$(sanitize_worker "$ZECNERO_WORKER")" POOL_LOGIN elif [ -z "$(setting WORKER)" ]; then set_setting WORKER "$(default_worker)" POOL_LOGIN fi write_pool_xmrig_config || die "could not write $POOL_XMRIG_CONF" address="$(effective_pool_login)" else address="$(miner_address)" fi if [ "$want" = "$current" ] && [ "$want" = "$(active_mode)" ] && supervisor_pid >/dev/null; then say "Already mining in $want mode." [ "$want" = pool ] && say "Your pool page: $(pool_page "$address")" return 0 fi if [ "$want" = pool ]; then say "Mode: pool. xmrig-zecnero mines on $(pool_target) as $address.$(pool_worker)" case "$(login_kind "$address")" in unified) say "The pool pays that unified address, through Orchard. Your mining key stays for solo mining." ;; sapling) say "That Sapling login works until block $(sapling_stop_height) only, ahead of NU2: switch with zecnero-miner pool-login new" ;; *) say "That nm... login works until block $SHIELDED_ONLY_ACTIVATION_HEIGHT only: switch with zecnero-miner pool-login new" ;; esac say "The node and the bridge stay installed, and do not run in pool mode." else say "Mode: solo. Your own node, bridge and miner run here, and blocks you find pay $address" fi [ "$want" != "$current" ] && say "Your mining key was not changed." if [ "${ZECNERO_NO_START:-}" = 1 ]; then return 0 fi printf '\n' cmd_start } installed_version() { [ -L "$BIN" ] && basename "$(readlink "$BIN")"; } # Download folders an earlier update left in ~/zecnero (about 65 MB each). # While an update runs, only folders over ten minutes old, so it keeps its own. remove_update_leftovers() { local f age=() for f in /proc/[0-9]*/cmdline; do if tr '\0' ' ' 2>/dev/null <"$f" | grep -q 'zecnero-miner update'; then age=(-mmin +10) break fi done find "$ZHOME" -maxdepth 1 -type d -name '.update.*' "${age[@]}" -exec rm -rf {} + 2>/dev/null return 0 } # ---------------------------------------------------------------- a complete release # zecnero-miner update in testnet-v0.1.2 and earlier downloads only the files # it knows of. Updating with it to a release that added a program (such as # zecnero-wallet, which pool mode makes its unified login with) installs the # new zecnero-miner without that program. The new zecnero-miner then adds it: # # fetches (only when something is missing; a complete install never goes online here): # hint and start, which the old update runs right after installing; # migrate, which update runs; start and restart in either mode; # mode pool, pool-login ADDRESS|new, register-payout; and update, which # no longer says "latest" while something is missing # only reports: status # never: supervise (what systemd and start run in the background), and # everything else # # release_missing prints the archives of ZV_ARCHIVES whose program is not in # bin, leaving out any the installed SHA256SUMS does not list (a release that # does not ship it). Nothing for an install not made by the installer. release_missing() { local a [ -L "$BIN" ] || return 0 for a in $ZV_ARCHIVES; do [ -x "$BIN/${a%-linux-x86_64.tar.xz}" ] && continue if [ -f "$BIN/SHA256SUMS" ] && ! grep -Eq "^[0-9a-f]{64} [ *]$a\$" "$BIN/SHA256SUMS"; then continue; fi printf '%s ' "$a" done } # 1 once update has run zv_setup_trust (never from the environment). ZV_CHOSEN=0 archive_names() { local a out="" for a in $1; do out="$out ${a%-linux-x86_64.tar.xz}"; done printf '%s' "${out# }" } # fetch_missing ARCHIVE...: run in a subshell (zv_die exits). Downloads # SHA256SUMS, its signature and only these archives from the installed # release's folder, from the same hosts, fallback and proxy as update, checks # them as update and the installer do (the pinned key's signature, then that its # RELEASE names the installed release, the checksums, then that each program # runs), and installs each into the release # folder with one rename. Exits 3, installing nothing, when the server's # SHA256SUMS is not the one installed (update then installs the release again). fetch_missing() { local version names a name sig=0 host version="$(installed_version)" names="$(archive_names "$*")" [[ "$version" =~ ^testnet-v[0-9]+\.[0-9]+\.[0-9]+$ ]] || zv_die "cannot tell which release is installed ($BIN)" for a in curl tar xz sha256sum; do command -v "$a" >/dev/null 2>&1 || zv_die "$a is missing. Install it with: sudo apt install -y curl tar xz-utils coreutils" done # update has chosen the host already (and may have switched to the mirror). [ "$ZV_CHOSEN" = 1 ] || zv_setup_trust local ZV_ARCHIVES="$*" ZV_SCRIPTS="" REPAIR_WORK="$(mktemp -d "$ZHOME/.update.XXXXXX")" || zv_die "could not make a download folder in $ZHOME" trap 'rm -rf "${REPAIR_WORK:-}"' EXIT zv_proxy_note # One line in the end, not one per file: only a switch to the mirror is # said. (This runs in a subshell, so the quieter zv_say stays in it.) zv_say() { case "$*" in " downloading "*) ;; *) printf '%s\n' "$*" ;; esac; } zv_download "$version" "$REPAIR_WORK" zv_check_signature "$REPAIR_WORK" || sig=$? if [ "$sig" = 2 ]; then zv_box "WARNING: THE SIGNATURE WAS NOT CHECKED" \ "gpg is not installed, so only the checksums were checked." \ "Checksums catch broken downloads, not a tampered server." \ "That works for downloads.zecnero.org only: files from" \ "mirror.zecnero.org are never accepted without gpg." \ "To check signatures next time: sudo apt install -y gnupg" fi if ! cmp -s "$REPAIR_WORK/SHA256SUMS" "$BIN/SHA256SUMS"; then say "The $version on $(zv_host "$ZV_BASE") is not the build installed here, so $names cannot be added to it." exit 3 fi # The folder's signed RELEASE must name the installed release (zv_check_release). zv_check_sums "$REPAIR_WORK" "$version" zv_unpack "$REPAIR_WORK" "$REPAIR_WORK/add" for a in $ZV_ARCHIVES; do name="${a%-linux-x86_64.tar.xz}" install -m 0755 "$REPAIR_WORK/add/$name" "$BIN/.$name.new" && mv -f "$BIN/.$name.new" "$BIN/$name" || zv_die "could not install $name into $(readlink -f "$BIN")" done host="$(zv_host "$ZV_BASE")" if [ "$sig" = 2 ]; then say "Added $names, which this $version install was missing (from $host; checksums OK, signature not checked)." else say "Added $names, which this $version install was missing (from $host; signature and checksums OK)." fi } # complete_release: adds what release_missing names (see above). 0 when the # install is complete (at once, with no network, when nothing is missing); # else it says so and returns 1 (3: the server has another build). Callers # that cannot go on without the program stop; the others go on. complete_release() { local missing rc=0 missing="$(release_missing)" [ -n "$missing" ] || return 0 # shellcheck disable=SC2086 # a list of archive names (fetch_missing $missing) || rc=$? [ "$rc" = 0 ] && return 0 warn "this install is still missing $(archive_names "$missing"). To add it, run: zecnero-miner update" return "$rc" } cmd_update() { local force=0 [ "${1:-}" = "--force" ] && force=1 [ -L "$BIN" ] || die "Zecnero is not installed by the installer in $ZHOME." \ "Install it with: curl -fsSL https://downloads.zecnero.org/install-miner.sh | bash" local tool for tool in curl tar xz sha256sum; do command -v "$tool" >/dev/null 2>&1 || die "$tool is missing. Install it with: sudo apt install -y curl tar xz-utils coreutils" done zv_setup_trust ZV_CHOSEN=1 local current latest missing rc=0 current="$(installed_version)" zv_latest latest="$ZV_VERSION" # An older release named as the latest is refused (a download server rolling # installs back). The files must then say, signed, that they are that release # (zv_check_release), so the name compared here is the one installed. if [ "$force" = 0 ] && [[ "$current" =~ ^testnet-v[0-9]+\.[0-9]+\.[0-9]+$ ]] && zv_older "$latest" "$current"; then die "$(zv_host "$ZV_BASE") names $latest as the latest release, older than the $current installed here." \ "Nothing was changed. To install it anyway: zecnero-miner update --force" fi if [ "$current" = "$latest" ] && [ "$force" = 0 ]; then # Latest, but perhaps installed by an older update that left programs # out: add them, or install the release again when the server's build # is not the one installed. missing="$(release_missing)" if [ -n "$missing" ]; then # shellcheck disable=SC2086 # a list of archive names (fetch_missing $missing) || rc=$? [ "$rc" = 0 ] || [ "$rc" = 3 ] || exit 1 fi if [ "$rc" = 0 ]; then say "You have the latest version ($current)." return 0 fi fi if [ "$rc" = 3 ]; then say "Installing $latest again." else say "Updating from $current to $latest." fi remove_update_leftovers # A global, not a local: the EXIT trap runs after this function has # returned. (zecnero-miner before testnet-v0.1.2 used a local here, so # its update stopped with "work: unbound variable" and left the download # folder behind; remove_update_leftovers cleans those up.) UPDATE_WORK="$(mktemp -d "$ZHOME/.update.XXXXXX")" || die "could not make a download folder" trap 'rm -rf "${UPDATE_WORK:-}"' EXIT local work="$UPDATE_WORK" zv_download "$latest" "$work" local sig=0 zv_check_signature "$work" || sig=$? if [ "$sig" = 2 ]; then zv_box "WARNING: THE SIGNATURE WAS NOT CHECKED" \ "gpg is not installed, so only the checksums were checked." \ "Checksums catch broken downloads, not a tampered server." \ "That works for downloads.zecnero.org only: files from" \ "mirror.zecnero.org are never accepted without gpg." \ "To check signatures next time: sudo apt install -y gnupg" else say " signature OK (Zecnero release key $ZV_FPR)" fi zv_check_sums "$work" "$latest" # The release installed and recorded is the one its signed RELEASE names. latest="$ZV_RELEASE" say " checksums OK; the signed release is $latest" zv_unpack "$work" "$work/release" local was_running=0 supervisor_pid >/dev/null && was_running=1 [ "$was_running" = 1 ] && cmd_stop zv_activate "$ZHOME" "$latest" "$work/release" say "Installed $latest. Your mining key and settings were not changed." # The new zecnero-miner starts mining again, and (for an install that has # not chosen pool or solo yet) says in one line that pool mode exists. An # update run by a wrapper from before pool mode ends the same way, since # it hands over to the new one to start. # Pool installs that log in with an nm... or a Sapling address switch to a # unified login here (Testnet pays shielded addresses only from NU1, and # no transaction pays a Sapling address from NU2). "$BIN/zecnero-miner" migrate if [ "$was_running" = 1 ]; then ZECNERO_NO_WAIT=1 "$BIN/zecnero-miner" start else "$BIN/zecnero-miner" hint fi } # ---------------------------------------------------------------- optional systemd service systemd_user_ok() { command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; } unit_path() { printf '%s/systemd/user/%s' "${XDG_CONFIG_HOME:-$HOME/.config}" "$UNIT_NAME"; } unit_enabled() { [ -f "$(unit_path)" ] && systemd_user_ok && systemctl --user is-enabled --quiet "$UNIT_NAME" 2>/dev/null; } unit_active() { [ -f "$(unit_path)" ] && systemd_user_ok && systemctl --user is-active --quiet "$UNIT_NAME" 2>/dev/null; } cmd_autostart() { case "${1:-}" in on) require_setup systemd_user_ok || die "this system has no systemd user session, so automatic start is not available." \ "Start mining yourself with: zecnero-miner start" mkdir -p "$(dirname "$(unit_path)")" cat >"$(unit_path)" </dev/null && ! unit_active; then cmd_stop; fi systemctl --user daemon-reload && systemctl --user enable --now "$UNIT_NAME" || die "systemd could not enable $UNIT_NAME" loginctl enable-linger "$(id -un)" 2>/dev/null || say "Note: mining starts when you log in. To start it at boot without logging in, run: sudo loginctl enable-linger $(id -un)" say "Mining now starts automatically. Turn it off with: zecnero-miner autostart off" ;; off) if [ -f "$(unit_path)" ]; then systemctl --user disable --now "$UNIT_NAME" 2>/dev/null rm -f "$(unit_path)" systemctl --user daemon-reload 2>/dev/null fi say "Automatic start is off. Start mining yourself with: zecnero-miner start" ;; *) die "use: zecnero-miner autostart on, or zecnero-miner autostart off" ;; esac } cmd_help() { local mode mode="$(configured_mode)" cat <&2 cmd_help >&2 exit 2 ;; esac } main "$@"