git / moonbite-network / release/miner/mine.sh · raw

#!/usr/bin/env bash# MoonBite solo miner - Linux & macOS.## Solo and pool-free: this runs a full MoonBite node on your machine and mines# straight to YOUR wallet address. No pool, no account, no middleman.##   ./mine.sh moon1yourwalletaddress   mine rewards to your wallet#   ./mine.sh                          confirm the saved address (Enter keeps it), or ask for one#   ./mine.sh --cores 50% [address]    mine with half your CPU cores (or --cores 4);#                                      default is every physical core (also MOONBITE_CORES)#   ./mine.sh address                  print the reward address in use#   ./mine.sh stop                     stop the node## Get your address from the MoonBite wallet app (or moonbite.org/wallet):# create a wallet, open Receive, and copy the moon1... address.set -euo pipefailHERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"DAEMON="$HERE/moonbited";     [ -x "$DAEMON" ] || DAEMON="$(command -v moonbited || true)"CLIBIN="$HERE/moonbite-cli";  [ -x "$CLIBIN" ] || CLIBIN="$(command -v moonbite-cli || true)"[ -x "$DAEMON" ] || { echo "moonbited not found next to this script or on PATH." >&2; exit 1; }# What this script will never do (docs/ADR-013-miner-distribution-policy.md):# download or execute anything, install a service / cron / login item, ask for# sudo, or change any security setting. It runs the node binary next to it (or# on PATH), only for as long as you leave it running.show_binary_hash() {  # Print the SHA-256 of the exact binaries about to run so the user can  # compare them against the published manifest before mining starts.  local tool=""  if command -v sha256sum >/dev/null 2>&1; then tool="sha256sum";  elif command -v shasum >/dev/null 2>&1; then tool="shasum -a 256"; fi  if [ -n "$tool" ]; then    local b h    for b in "$DAEMON" "$CLIBIN"; do      [ -x "$b" ] || continue      h=$($tool "$b" 2>/dev/null | awk '{print $1}')      echo " sha256 $h  $(basename "$b")"    done  fi  echo " Compare against SHA256SUMS.txt from the release page:"  echo "   https://moonbite.org/mine"  echo " (see ANTIVIRUS.md next to this script for signature + attestation checks)"}# --cores N or --cores N% (or MOONBITE_CORES): how many CPU cores to mine with.if [ "${1:-}" = "--cores" ]; then  [ -n "${2:-}" ] || { echo "--cores needs a value, e.g. --cores 4 or --cores 50%" >&2; exit 1; }  MOONBITE_CORES="$2"; shift 2fiphysical_cores() {  local n=""  if command -v lscpu >/dev/null 2>&1; then n=$(lscpu -p 2>/dev/null | grep -v '^#' | cut -d, -f2,3 | sort -u | wc -l); fi  [ "${n:-0}" -ge 1 ] 2>/dev/null || n=$(sysctl -n hw.physicalcpu 2>/dev/null || true)  [ "${n:-0}" -ge 1 ] 2>/dev/null || n=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)  echo "$n"}# Default: every physical core. RandomX gains nothing from hyperthreads (each VM# wants its own 2 MB of L3) and runs slower on them, so requests above the# physical count are capped there. Accepts a count ("4") or a share ("50%").resolve_threads() {  local phys="$1" want="${MOONBITE_CORES:-}" n  [ -z "$want" ] && { echo "$phys"; return; }  case "$want" in    *%) n="${want%\%}"; case "$n" in ''|*[!0-9]*) echo bad; return;; esac; n=$(( (phys * n + 50) / 100 ));;    ''|*[!0-9]*) echo bad; return;;    *) n="$want";;  esac  [ "$n" -lt 1 ] && n=1  [ "$n" -gt "$phys" ] && n="$phys"  echo "$n"}PHYS=$(physical_cores)THREADS=$(resolve_threads "$PHYS")[ "$THREADS" = bad ] && { echo "--cores must be a number of cores (e.g. 4) or a share of them (e.g. 50%), not '${MOONBITE_CORES}'." >&2; exit 1; }DATADIR="${MOONBITE_DATADIR:-$HOME/.moonbite}"CONF="$DATADIR/moonbite.conf"REWARD_FILE="$DATADIR/reward-address.txt"cli() { "$CLIBIN" -datadir="$DATADIR" -conf="$CONF" "$@"; }node_pid() { pgrep -f "moonbited -datadir=$DATADIR" 2>/dev/null | head -1; }is_addr() { case "$1" in moon1[0-9a-z]*) [ ${#1} -ge 26 ] && [ ${#1} -le 90 ];; *) return 1;; esac; }write_conf() {  mkdir -p "$DATADIR"  local pw; pw=$(sed -n 's/^rpcpassword=//p' "$CONF" 2>/dev/null | head -1) || true  [ -z "$pw" ] && pw=$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')  # MOONBITE_P2P_PORT / MOONBITE_RPC_PORT let more than one node run on the  # same machine at once (mine.ps1 already supports this) - the default ports  # collide the moment a second instance, e.g. a seed node, is already up.  local p2p="" rpcp=""  [ -n "${MOONBITE_P2P_PORT:-}" ] && p2p="port=$MOONBITE_P2P_PORT"$'\n'  [ -n "${MOONBITE_RPC_PORT:-}" ] && rpcp="rpcport=$MOONBITE_RPC_PORT"$'\n'  # Create the file 0600 from birth. A bare `>` here writes at the process  # umask (often 0644), leaving the RPC password world-readable until the  # chmod below runs - tighten umask around the write only, then restore it.  local old_umask; old_umask=$(umask); umask 077  cat > "$CONF" <<CONFserver=1listen=1dbcache=512rpcuser=moonminerrpcpassword=$pwminingthreads=$THREADS${p2p}${rpcp}# Live MoonBite seed nodes - how your miner finds the network.addnode=67.205.154.64:9444addnode=165.232.106.9:9444CONF  umask "$old_umask"  chmod 600 "$CONF"}wait_rpc() {  local i; for i in $(seq 1 60); do cli getblockcount >/dev/null 2>&1 && return 0; sleep 2; done  echo "node did not start (check $DATADIR/debug.log)" >&2; return 1}# True if something is already listening on the given TCP port (ss, then# netstat, then /proc as fallbacks - one of them exists on any Linux/macOS).port_in_use() {  local p="$1"  if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | grep -qE "[:.]$p\b" && return 0; fi  if command -v netstat >/dev/null 2>&1; then netstat -an 2>/dev/null | grep -qE "[:.]$p\b.*LISTEN" && return 0; fi  if command -v lsof >/dev/null 2>&1; then lsof -iTCP:"$p" -sTCP:LISTEN >/dev/null 2>&1 && return 0; fi  return 1}start_node() {  # Already have a reachable node? Use it. Its core count was fixed when it  # started, so say how to apply a different one.  if cli getblockcount >/dev/null 2>&1; then    local cur; cur=$(sed -n 's/^miningthreads=//p' "$CONF" 2>/dev/null | head -1)    [ "$cur" != "$THREADS" ] && echo " Note: the node is already running with a different core setting. To mine with $THREADS cores, run ./mine.sh stop and start again." >&2    return 0  fi  # A node we can't authenticate to may already own the ports - usually a  # second copy of this miner on the same machine. Only one node runs per  # machine; another just fails to bind and hangs. Say so and stop cleanly.  local p2p="${MOONBITE_P2P_PORT:-9444}" rpcp="${MOONBITE_RPC_PORT:-9445}"  if port_in_use "$p2p" || port_in_use "$rpcp"; then    cat >&2 <<MSG  A MoonBite node is already running on this computer (port $p2p is in use).  You are already mining - only one node runs per machine, so this isn't needed.  To run a SECOND, separate miner here on purpose, give it its own ports and  data folder first, for example:      MOONBITE_P2P_PORT=19444 MOONBITE_RPC_PORT=19445 MOONBITE_DATADIR=~/.moonbite2 ./mine.shMSG    exit 0  fi  write_conf  "$DAEMON" -datadir="$DATADIR" -conf="$CONF" -daemon >/dev/null  wait_rpc}# Shape check, then the node's own bech32 checksum check, so a mistyped# address is refused instead of silently paying a stranger.addr_valid() { is_addr "$1" && cli validateaddress "$1" 2>/dev/null | grep -q '"isvalid": true'; }# Where mining rewards go. Priority: CLI arg, env, saved file. When mining# starts with a saved address and a person at the terminal, show it and let# them press Enter to keep it or paste a new one. With no address at all, ask# for one (Enter skips). Unattended runs never wait: they use the saved# address, or fall back to the node's own built-in wallet so mining still# starts with zero setup. Messages go to stderr: stdout is the address,# captured by the caller. Pass "confirm" as $2 to enable the start-up check.resolve_reward_address() {  local cand="${1:-}" confirm="${2:-}" a in  if [ -n "$cand" ]; then    addr_valid "$cand" || { echo "'$cand' is not a valid MoonBite address. Check it and try again." >&2; exit 1; }    printf '%s' "$cand" > "$REWARD_FILE"; echo "$cand"; return  fi  if [ -n "${MOONBITE_ADDRESS:-}" ] && addr_valid "$MOONBITE_ADDRESS"; then printf '%s' "$MOONBITE_ADDRESS" > "$REWARD_FILE"; echo "$MOONBITE_ADDRESS"; return; fi  if [ -s "$REWARD_FILE" ]; then    a=$(cat "$REWARD_FILE")    if is_addr "$a"; then      if [ "$confirm" = confirm ] && [ -t 0 ]; then        { echo ""; echo " Rewards go to: $a"; echo " Press Enter to keep it, or paste a different address."; } >&2        while :; do          in=""; read -r -p " Address [Enter = keep]: " in || break          in="$(printf '%s' "$in" | tr -d '[:space:]')"          [ -z "$in" ] && break          if addr_valid "$in"; then            printf '%s' "$in" > "$REWARD_FILE"; echo " Saved. Rewards now go to $in" >&2; echo "$in"; return          fi          echo " That isn't a valid MoonBite address (typo?). Paste it again, or press Enter to keep the saved one." >&2        done      fi      echo "$a"; return    fi  fi  if [ -t 0 ]; then    {      echo ""      echo " Where should your mining rewards go?"      echo " Paste your wallet address (MoonBite wallet app > Receive, starts with moon1),"      echo " or just press Enter to create a wallet on this computer instead."    } >&2    while :; do      in=""; read -r -p " Address: " in || break      in="$(printf '%s' "$in" | tr -d '[:space:]')"      [ -z "$in" ] && break      if addr_valid "$in"; then        printf '%s' "$in" > "$REWARD_FILE"; echo " Saved. Next start just press Enter to keep it." >&2; echo "$in"; return      fi      echo " That isn't a valid MoonBite address (typo?). Paste it again, or press Enter to skip." >&2    done  fi  cli createwallet miner >/dev/null 2>&1 || cli loadwallet miner >/dev/null 2>&1 || true  a=$(cli -rpcwallet=miner getnewaddress "mining"); printf '%s' "$a" > "$REWARD_FILE"  {    echo ""    echo " Rewards will go to a wallet on THIS computer only: $a"    echo " IMPORTANT: it has no backup and no password yet. If this disk dies, the coins are gone."    echo "   Back it up, then copy the file to a USB stick:"    echo "   $CLIBIN -datadir=\"$DATADIR\" -conf=\"$CONF\" -rpcwallet=miner backupwallet \"\$HOME/moonbite-miner-wallet.dat\""    echo "   Or switch to your wallet app any time:  ./mine.sh moon1youraddress"    echo ""  } >&2  echo "$a"}case "${1:-mine}" in  address)    show_binary_hash; start_node; echo "Rewards go to: $(resolve_reward_address)"    ;;  stop)    pkill -f "moonbite-cli.*generatetoaddress" 2>/dev/null || true    cli stop 2>/dev/null || true    for _ in $(seq 1 20); do [ -z "$(node_pid)" ] && break; sleep 2; done    p=$(node_pid); [ -n "$p" ] && kill "$p" 2>/dev/null || true    for _ in $(seq 1 10); do [ -z "$(node_pid)" ] && break; sleep 1; done    p=$(node_pid); [ -n "$p" ] && kill -9 "$p" 2>/dev/null || true    echo "stopped."    ;;  mine|*)    ARG=""; case "${1:-}" in moon1*) ARG="$1";; esac    show_binary_hash    start_node    ADDR=$(resolve_reward_address "$ARG" confirm)    echo "======================================================================"    echo " MoonBite solo miner"    echo " Rewards to: $ADDR"    echo " Using $THREADS of $PHYS CPU cores (change with --cores 50% or --cores 4)."    echo " Ctrl-C to stop. Coins are spendable 100 blocks after they are mined."    echo "======================================================================"    trap 'echo; echo "Miner stopped. Node still running - ./mine.sh stop to shut it down."; exit 0' INT    echo " Syncing with the network before mining..."    stable=0    while :; do      ibd=$(cli getblockchaininfo 2>/dev/null | sed -n 's/.*"initialblockdownload": *\(true\|false\).*/\1/p' | head -1)      bl=$(cli getblockcount 2>/dev/null || echo 0)      hd=$(cli getblockchaininfo 2>/dev/null | sed -n 's/.*"headers": *\([0-9]*\).*/\1/p' | head -1)      [ "$ibd" = "false" ] && [ -n "$hd" ] && [ "$bl" -ge "$hd" ] && break      # A chain whose newest block is over a day old reports "initial download"      # until someone mines the next block, so waiting on that flag alone      # deadlocks a young or quiet chain. Holding every header our peers      # announce, with peers connected, for ~15 s means nothing is left to fetch.      peers=$(cli getconnectioncount 2>/dev/null || echo 0)      if [ -n "$hd" ] && [ "${peers:-0}" -ge 1 ] && [ "$bl" -ge "$hd" ]; then stable=$((stable+1)); else stable=0; fi      [ "$stable" -ge 5 ] && { echo "   Up to date with $peers peer(s); the chain is just quiet."; break; }      echo "   ...$bl / ${hd:-?} blocks"; sleep 3    done    # Public launch: the genesis block is stamped 2 h after the announced launch,    # and no block may be more than 2 h ahead of the clock, so block 1 becomes    # possible exactly at launch. Until then wait here instead of failing calls.    gtime=$(cli getblockheader "$(cli getblockhash 0 2>/dev/null)" 2>/dev/null | sed -n 's/.*"time": *\([0-9]*\).*/\1/p' | head -1)    if [ -n "$gtime" ] && [ "$(cli getblockcount 2>/dev/null || echo 1)" = "0" ]; then      opens=$((gtime - 7200 + 5))      if [ "$(date -u +%s)" -lt "$opens" ]; then        # Announced in Central European time (CET/CEST).        at=$(TZ=Europe/Paris date -d "@$((gtime - 7200))" '+%Y-%m-%d %H:%M %Z' 2>/dev/null || TZ=Europe/Paris date -r "$((gtime - 7200))" '+%Y-%m-%d %H:%M %Z')        echo " MoonBite opens for mining at $at. Nobody can mine before then."        echo " Leave this running: mining starts by itself at launch."        while left=$((opens - $(date -u +%s))); [ "$left" -gt 0 ]; do          printf "   launch in %dd %02dh %02dm %02ds\n" $((left/86400)) $((left%86400/3600)) $((left%3600/60)) $((left%60))          if [ "$left" -gt 3600 ]; then nap=600; elif [ "$left" -gt 120 ]; then nap=60; else nap=10; fi          [ "$nap" -gt "$left" ] && nap=$left          sleep "$nap"        done        echo " Launch! Mining is open."      fi    fi    echo " Synced at height $(cli getblockcount). Mining now."    FOUND=0    # The node hashes one block template per call and does not notice a newer    # block from someone else until the call ends, so keep each call to about    # ROUND_SECS of work: size the tries from the speed measured on the last    # call that found nothing. Long calls waste work on a stale tip and orphan    # blocks. MAXTRIES pins a fixed value instead.    ROUND_SECS=20    TRIES="${MAXTRIES:-5000}"    # Milliseconds; macOS date has no %N, so fall back to whole seconds there.    now_ms() { local n; n=$(date +%s%N); case "$n" in *N) echo $(( $(date +%s) * 1000 ));; *) echo $(( n / 1000000 ));; esac; }    while :; do      T0=$(now_ms)      OUT=$(cli generatetoaddress 1 "$ADDR" "$TRIES" 2>/dev/null || true)      MS=$(( $(now_ms) - T0 ))      if printf '%s' "$OUT" | grep -q '"[0-9a-f]\{64\}"'; then        FOUND=$((FOUND+1))        echo "  BLOCK FOUND!  height $(cli getblockcount)   (you have found $FOUND this session)   peers $(cli getconnectioncount)"      elif [ -z "${MAXTRIES:-}" ] && [ "$MS" -gt 1000 ]; then        TRIES=$(( TRIES * ROUND_SECS * 1000 / MS ))        [ "$TRIES" -lt 2000 ] && TRIES=2000        [ "$TRIES" -gt 200000 ] && TRIES=200000      fi      [ "$MS" -lt 1000 ] && sleep 1    done    ;;esac